Search code examples
python-3.xpygamedraw

How to slowly draw a line in Python


I want to slowly draw a line in python so the act of drawing is actually visible to the naked eye.

I tried making it by putting it in a loop and increasing the distance each time but i never had any success with it. The thing is nothing would appear for 3 sec and then the whole line would appear which is the opposite of what i want to accomplish. I didn't have success with a pygame.display.delay() function either. The only thing that kind of worked is having clock.tick set to some abysmal values such as clock.tick(300000) but this would just make the entire program really laggy.

def draw_red_line(i):
    y = 0
    while y < 300:
        pygame.draw.line(screen, RED, (i*100+50, 0), (i*100+50, y))
        y+=0.01

Solution

  • Using a sleep is not a good idea in this sort of situation, since it slows the whole thread (which is the entire program in a single-thread model).

    It's better to keep some kind of state information about the line, and based on real-time timings (e.g.: elapsed milliseconds) progress the "growth" of the line, second by second.

    This means the line needs to be broken into segments, and the smallest line segment is a single pixel. Using the Midpoint Line Algorithm, is an efficient way to determine all the pixels that lay on a line. Once all the "line parts" have been determined, it's possible to simply update the end-point of the line based on the elapsed time.

    Here's some code I wrote earlier that, given a pair of points, returns a list of pixels.

    midpoint.py:

    def __plotLineLow( x0,y0, x1,y1 ):
        points = []
        dx = x1 - x0
        dy = y1 - y0
        yi = 1
        if dy < 0:
            yi = -1
            dy = -dy
        D = 2*dy - dx
        y = y0
    
        for x in range( x0, x1 ):
            points.append( (x,y) )
            if D > 0:
               y = y + yi
               D = D - 2*dx
            D = D + 2*dy
        return points
    
    def __plotLineHigh( x0,y0, x1,y1 ):
        points = []
        dx = x1 - x0
        dy = y1 - y0
        xi = 1
        if dx < 0:
            xi = -1
            dx = -dx
        D = 2*dx - dy
        x = x0
    
        for y in range( y0, y1 ):
            points.append( (x,y) )
            if D > 0:
                x = x + xi
                D = D - 2*dy
            D = D + 2*dx
        return points
    
    def linePoints( pointA, pointB ):
        """ Generate a list of integer points on the line pointA -> pointB """
        x0, y0 = pointA
        x1, y1 = pointB
        points = []
        if ( abs(y1 - y0) < abs(x1 - x0) ):
            if ( x0 > x1 ):
                points += __plotLineLow( x1, y1, x0, y0 )
            else:
                points += __plotLineLow( x0, y0, x1, y1 )
        else:
            if ( y0 > y1 ):
                points += __plotLineHigh( x1, y1, x0, y0 )
            else:
                points += __plotLineHigh( x0, y0, x1, y1 )
    
        return points
    
    
    if __name__ == "__main__":
        #midPoint( (597, 337), (553, 337) )
        print( str( linePoints( (135, 295), (135, 304) ) ) )
    

    And some demonstration code which implements a SlowLine class.

    import pygame
    import random
    import time
    import sys
    
    from midpoint import linePoints  # Midpoint line algorithm
    
    # Window size
    WINDOW_WIDTH      = 400
    WINDOW_HEIGHT     = 400
    
    SKY_BLUE = ( 30,  30,  30)
    SKY_RED  = (200, 212,  14)
    
    # Global millisecond count since start
    NOW_MS = 0
    
    class SlowLine():
        def __init__( self, pixels_per_second, x0,y0, x1,y1, colour=SKY_RED ):
            self.points       = linePoints( ( x0, y0 ), ( x1, y1 ) )
            self.pixel_count  = len( self.points )
            self.speed        = pixels_per_second
            self.start_point  = self.points[0]     # start with a single-pixel line
            self.end_point    = self.points[0]
            self.pixel_cursor = 0                  # The current end-pixel
            self.last_update  = 0                  # Last time we updated
            self.colour       = colour
            self.fully_drawn  = False
    
        def update(self):
            global NOW_MS
    
            if ( self.fully_drawn == True ):
                # nothing to do
                pass
            else:
                # How many milliseconds since the last update() call?
                if ( self.last_update == 0 ):
                    self.last_update = NOW_MS
                    time_delta = 0
                else:
                    time_delta = NOW_MS - self.last_update
                    self.last_udpate = NOW_MS
    
                # New pixels to add => speed * time
                new_pixel_count = time_delta * self.speed / 1000   # this may loose precision with very small speeds
    
                if ( new_pixel_count + self.pixel_cursor > self.pixel_count ):
                    # We're out of pixels
                    self.end_point  = self.points[-1]   
                    self.full_drawn = True
                else:
                    # Grow the line by <new_pixel_count> pixels
                    self.pixel_cursor += new_pixel_count
                    self.end_point     = self.points[ int( self.pixel_cursor ) ]
    
        def draw( self, screen ):
            pygame.draw.line( screen, self.colour, self.start_point, self.end_point )
    
    
    
    
    ### MAIN
    pygame.init()
    SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
    WINDOW  = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), SURFACE )
    pygame.display.set_caption("Slow Line Movement")
    
    
    # Create some random lines
    lines = []
    for i in range( 20 ):
        rand_speed = random.randint( 1, 50 )
        rand_x0    = random.randint( 0, WINDOW_WIDTH )
        rand_y0    = random.randint( 0, WINDOW_HEIGHT )
        rand_x1    = random.randint( 0, WINDOW_WIDTH )
        rand_y1    = random.randint( 0, WINDOW_HEIGHT )
        lines.append( SlowLine( rand_speed, rand_x0, rand_y0, rand_x1, rand_y1 ) )
    
    
    # Main event loop
    clock = pygame.time.Clock()
    done = False
    while not done:
        NOW_MS = pygame.time.get_ticks()
    
        # Update the line lengths
        for l in lines:
            l.update()
    
        # Handle user-input
        for event in pygame.event.get():
            if ( event.type == pygame.QUIT ):
                done = True
    
        # Movement keys
        keys = pygame.key.get_pressed()
        if ( keys[pygame.K_UP] ):
            print("up")
        elif ( keys[pygame.K_DOWN] ):
            print("down")
        elif ( keys[pygame.K_LEFT] ):
            print("left")
        elif ( keys[pygame.K_RIGHT] ):
            print("right")
        elif ( keys[pygame.K_q] and ( keys[pygame.K_RCTRL] or keys[pygame.K_LCTRL] ) ):
            print("^Q")
            done = True
    
        # Update the window, but not more than 60fps
        WINDOW.fill( SKY_BLUE )
        for l in lines:
            l.draw( WINDOW )
    
        pygame.display.flip()
    
        # Clamp FPS
        clock.tick_busy_loop(60)
    
    pygame.quit()
    

    In this animation the progress is a bit jerky, but that's the animation, not the demo. slow_lines.gif