Let me deconstruct your implementation to see what mistakes you made:
def draw_antialiased_circle(renderer, position, radius):
First question, what does radius mean? It is impossible to draw a circle, you can draw a ring (ring / donut), because if you do not have thickness, you cannot see it. Therefore, the radius is ambiguous, is it the inner radius, the radius of the midpoint, or the outer radius? If you do not specify a variable name, this will be confusing. Perhaps we can find out.
def _draw_point(renderer, offset, x, y): sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y + y) sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y + y) sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y - y) sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y - y)
OK, we are going to make four places at once, since the circle is symmetrical. Why not 8 places at once?
i = 0 j = radius d = 0 T = 0
OK, initialize i to 0 and j radius, these should be the x and y coordinates. What is d and T ? Descriptive variable names are not supported. This is normal when copying scientists' algorithms to make them understandable with longer variable names that you know!
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, sdl2.SDL_ALPHA_OPAQUE) _draw_point(renderer, position, i, j)
BUT? Are we drawing a special case? Why are we doing this? Not sure. What does it mean? This means filling the square in (0, radius) full opacity. So, now we know what radius , this is the outer radius of the ring, and the width of the ring is apparently one pixel. Or at least what this special case tells us ... let's see if this is preserved in the general code.
while i < j + 1:
We are going to go in cycles until we reach a point on the circle, where i > j , and then stop. That is, we draw an octant.
i += 1
So, we have already drawn all the pixels that we care about, in position i = 0 , we will move to the next.
s = math.sqrt(max(radius * radius - i * i, 0.0))
those. s is a floating point number that is the y-component of the octant at point i along the x axis. That is, the height above the x axis. For some reason we have max , maybe we are worried about very small circles ... but this does not tell us if the point is on the outer / inner radius.
d = math.floor(sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) + 0.5)
Breaking it, (math.ceil(s) - s) gives us a number from 0 to 1.0, which. This number will increase as s decreases, as i increases, and then, as soon as it reaches 1.0, it will reset to 0.0, therefore, in a sawtooth pattern.
sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) We use a sawtooth input to create an opacity level, i.e. to smooth the circle. Put a constant addition of 0.5. floor() assumes that we are only interested in integer values - probably the API accepts only integer values. All this shows that d ends as an integer level between 0 and SDL_ALPHA_OPAQUE , which starts from the beginning with 0, then gradually increases with i , and then when ceil(s) - s moves from 1 to 0, it falls back again.
So what does that matter in the beginning? s almost radius , since i is equal to 1 (provided that the circle does not have a trivial size), so we assume that we have an integral radius (which explicitly assumed the first special random code) ceil(s) - s is 0
if d < T: j -= 1 d = T
Here we noticed that d moved from high to low, and we are moving down the screen so that our position j close to where the ring should theoretically be.
But now we understand that the word from the previous equation is wrong. Imagine that in one iteration, d is 100.9. Then at the next iteration it fell, but only to 100.1. Since d and T are equal, since the gender eliminated their difference, we do not reduce j in this case, and this is crucial. I think probably explains the odd curves at the ends of your octant.
if d < 0:
Just optimization does not draw if we use alpha value 0
alpha = d sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha) _draw_point(renderer, position, i, j)
But the freeze on the first iteration d low, and therefore it refers to the (1, radius) almost completely transparent element. But a special case, to start with drawing a completely opaque element (0, radius) , it is so clear that there will be a graphic glitch. This is what you see.
Performance:
if i != j:
Another small optimization in which we will not paint again if we paint in the same place
_draw_point(renderer, position, j, i)
And that explains why we only make 4 points in _draw_point() , because here you are doing a different symmetry. This will simplify your code, not.
if (sdl2.SDL_ALPHA_OPAQUE - d) > 0:
Another optimization (you should not prematurely optimize your code!):
alpha = sdl2.SDL_ALPHA_OPAQUE - d sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha) _draw_point(renderer, position, i, j + 1)
So, at the first iteration, we write above, but with full opacity. This means that this is actually the internal radius that we use, and not the external radius used by the special case error. That is a kind of mistake "one after another."
My implementation (I did not have an installed library, but you can see from the output, checking the boundary conditions, that this makes sense):
import math def draw_antialiased_circle(outer_radius): def _draw_point(x, y, alpha):
Finally, would there be gotchas if you want to pass the beginning to a floating point?
Yes it would. The algorithm assumes that the origin is in an integer / integer place and otherwise completely ignores it. As we saw, if you pass an integer outer_radius , the algorithm draws a 100% opaque square at the location (0, outer_radius - 1) . However, if you want to convert to a location (0, 0.5) , you probably want the circle to be smoothly smoothed to 50% opacity in places (0, outer_radius - 1) and (0, outer_radius) , which this algorithm does not give you because he ignores the origin. Therefore, if you want to use this algorithm accurately, you must round off your origin before passing it on, so nothing comes of using float.