Search code examples
pythonpygamedrawing

Get smooth diagonal movement in Pygame


I'm working on this decal creator using pygame. You basically have a bunch of sliders which controls the angle, radius, offset, etc. The sliders are incrementing in integers.

As I'm increasing the offset from the center you can notice the dots are not following a perfect straight path.

enter image description here

This is the code I'm using for drawing the dots:

def drawAADot(surf, center, radius, color):
    gfx.aacircle(surf, center[0], center[1], radius, color)
    gfx.filled_circle(surf, center[0], center[1], radius, color)

And this code for distributing the dots along a circular path:

for i in range(divisions+1):
    i *= (angle/(divisions))
    move_vec = pygame.math.Vector2(center)
    move_vec.from_polar((radius + DotOffset, i+90-counter_angle))
    pos = center[0] + round(move_vec.x), center[1] + round(move_vec.y)
    drawAADot(screen, pos, DotRadius, (255,255,255))

I've searched and read about problems regarding movement with float accuracy, since the coordinates of the objects needs to be in integers. What I don't understand is if it really matters in this situation as soon as the positions are being rounded. I can see that a 45 degree angle could run smooth as long as your increasing one pixel sideways and one up. But how about something like lets say 15 degrees?

It doesn't have to be AutoCad precision, I'm aware of the limitations, I'm just wondering of I'm not overlooking something or that this is just as good as it gets.


Solution

  • I've found an old blog post that provides a module that works pretty well.

    Sub-pixel movement by Will McGugan: https://www.willmcgugan.com/blog/tech/post/going-sub-pixel-with-pygame/

    As mentioned, the code was quite old, and I got issues with premultiplied alpha blending. I came up with this solution that now works for me. I took the difference between the rounded position and the actual float position, scaled it up by 10, corrected the position and smoothscaled it back to normal:

    def drawDot(surface, roundPos, realPos, radius, color, scale=10):
    # original size
    w = radius * 2 
    h = radius * 2 
    # difference between real and rounded position
    dif_x = realPos[0] - roundPos[0]
    dif_y = realPos[1] - roundPos[1]
    add = 0
    if dif_x > dif_y:
        add += abs(dif_x)
    else:
        add += abs(dif_y)
    
    # adjusted width and height
    aw = w + math.ceil(add*2)
    ah = h + math.ceil(add*2)
    if aw % 2 != 0:
        aw += 1
    if ah % 2 != 0:
        ah += 1
    # scaled width and height
    sw = aw * scale
    sh = ah * scale
    # create scaled surface
    s = pygame.Surface((sw, sh), pygame.SRCALPHA)
    # offset
    offset = (round(dif_x*scale), round(dif_y*scale)) 
    
    # draw circle on scaled surface
    gfx.aacircle(s, int(sw/2 + offset[0]), int(sh/2 + offset[1]), radius*scale, color)
    gfx.filled_circle(s, int(sw/2 + offset[0]), int(sh/2 + offset[1]), radius*scale, color)
    # scale down surface to target size for supersampling effect
    s2 = pygame.transform.smoothscale(s, (aw, ah))
    # blit on screen
    surface.blit(s2, (roundPos[0]-aw/2, roundPos[1]-ah/2), special_flags=pygame.BLEND_PREMULTIPLIED)
    

    This is currently working for me. If anyone got a more descent/optimized version, I'm all ears.