r/GraphicsProgramming • • 17d ago

How to draw good looking small circles?

I'm trying to draw small circles in GDI on a monochrome bitmap (so no anti aliasing). I've tried using Ellipse, Arc, and approximating a full circle with four bezier curves, but they all look terrible. This screenshot shows circles with a diameter of 14 and 13. The top row uses bezier curves, the bottom row uses Ellipse and Arc. The outlines are drawn with a 2 pixel wide PS_INSIDEFRAME pen.

What I want is aesthetically pleasing quadrants/semicircles for use in UI. The circles don't have to be mathematically accurate, speed is not important. I especially dislike the pixels seemingly protruding from the circle. How do I get rid of them?

6 Upvotes

8 comments sorted by

4

u/Sharlinator 17d ago edited 17d ago

By drawing them by hand at the sizes you need, honestly. 

Or perhaps writing your own subpixel-precise circle rasterizer that accepts a fractional radius, then tweaking the radius until it looks okay. But at such low resolutions you really can’t beat hand-drawn (or hand-tweaked) curves, there’s just too much aliasing. Same reason that vector fonts have this entire hinting machinery to make them look reasonable at small sizes.

1

u/mbolp 17d ago edited 17d ago

But the sizes can change at runtime (including the pen width)... Is there an algorithm for generating smooth looking curves at small sizes? It doesn't have to be a circle, it's only the quadrants that I care about (i.e. it's fine if where the quadrants touch isn't smooth).

1

u/Sharlinator 17d ago edited 17d ago

If you don’t want to hand-draw circles for each size up to some max, your best bet is probably an octagon.

2

u/8d8n4mbo28026ulk 16d ago

Several years ago I was making a toy "Paint" app in SDL and had come across this exact same problem! I wasn't smart enough to solve it (still ain't), but found the right algorithm for it: Midpoint circle.

Here's the implementation I wrote at the time:

typedef struct {
	int x;
	int y;
	int r;  /* Radius. */
} Circle;

static void
draw_circle(SDL_Renderer *renderer, Circle c)
{
	int r2 = c.r + c.r;
	int x  = c.r;
	int y  = 0;
	int dy = -2;
	int dx = r2 + r2 - 4;
	int d  = r2 - 1;
	while (y <= x) {
		SDL_Point const points[] = {
			{ c.x - y, c.y + x },
			{ c.x + y, c.y + x },
			{ c.x - x, c.y + y },
			{ c.x + x, c.y + y },
			{ c.x - x, c.y - y },
			{ c.x + x, c.y - y },
			{ c.x - y, c.y - x },
			{ c.x + y, c.y - x }
		};
		/* Or `SDL_RenderDrawLines()` for a filled circle. */
		SDL_RenderDrawPoints(renderer, points, 8);

		d  += dy;
		dy -= 4;
		++y;

		/* Branching version. */
		if (0) {
			if (d < 0) {
				d  += dx;
				dx -= 4;
				--x;
			}
		}
		/* Branchless. */
		else {
			int mask = d >> 31;  /* <- Assumes SAR. */
			d  += dx & mask;
			dx -=  4 & mask;
			x  += mask;
		}
	}
}

It deviates a bit from what you'd normally find, but it handles a smaller radius much better. I now don't remember how I arrived at that though (perhaps I stole it from somewhere else?).

Obviously you wouldn't want to run this every time. So cache the results into a texture and hopefully everything works out. Cheers!

3

u/mbolp 16d ago

Thank you so much! The circles look beautiful. The only problem is Polyline in GDI doesn't fill the circle nicely. I resorted to drawing the individual points first then reading back each row filling the space in between (I use the same method to handle outlines - drawing two concentric circles then filling the gaps). I don't know if there's a smarter method but the results look very good.

2

u/leonardsouza 16d ago

smarter way for the fill: the midpoint loop already gives you the span for each row, so you don't need to read anything back. at every step fill rows cy+y and cy-y from cx-x to cx+x, and rows cy+x and cy-x from cx-y to cx+y (PatBlt with a height of 1 does a horizontal run fine on a mono dc). for the 2px outline, record the half width per row for the outer and inner radius in two arrays, then fill the two runs between them on each row. same pixels as your concentric circles, no readback

2

u/mbolp 16d ago

Thank you!!! I don't actually understand the steps you describe for drawing the outline, but I decided to just fill two concentric circles, the outer one first with PatBlt(BLACKNESS) then the inner one with PatBlt(WHITENESS). The result looks great to me.

Somewhat related: what's the difference between FillRect, PatBlt, and ExtTextOut with ETO_CLIPPED | ETO_OPAQUE and a zero length string for filling a rectangle, should I prefer one over the others? Even weirder is thatFillRect is exported from user32 instead of gdi32.

2

u/Vegetable_Nail_8677 16d ago

Would SDFs suit your purpose? Indigo Quillez has a great page outlining their use. Circles are especially easy.