How to slowly draw a line in Python

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


You must display.flip() to update the display and let the window events to be processed using event.get():

def draw_red_line(i):
    y = 0
    while y < 300:
        pygame.draw.line(screen, RED, (i*100+50, 0), (i*100+50, y))
        pygame.display.flip()
        pygame.event.get()
        y+=1

If you want to make a drawing visible on the screen you've to update the display (e.g. pygame.display.flip()), and you've to handel the events by either pygame.event.get() or pygame.event.pump().
Also note that the parameters for pygame.draw.line must be integral. Use round to convert a floating point value to an integral value.

Drawing the line in a loop and refreshing the display doesn't do what you want because the line is drawn without delay. I do not recommend creating animations in a separate loop within the main application loop. Use the application's main loop to draw the line.

Create a function that can draw a line from a start point to an end point, dependent on a value p in range [0.0, 1.0]. If the value is 0, no line is drawn. If the value is 1, the full line is drawn. Otherwise part of the line will be drawn:

def draw_red_line(surf, color, start, end, w):
    xe = start[0] * (1-w) + end[0] * w
    ye = start[1] * (1-w) + end[1] * w
    pygame.draw.line(surf, color, start, (round(xe), round(ye)))

Use this function in the main application loop:

w = 0
while True:
    # [...]

    draw_red_line(window, (255, 0, 0), line_start[i], line_end[i], w)
    if w < 1:
        w += 0.01

See also Shape and contour


Minimal example:

import pygame

pygame.init()
window = pygame.display.set_mode((300,300))
clock = pygame.time.Clock()

line_start = [(100, 0),   (200, 0),   (0, 100),   (0, 200)]
line_end   = [(100, 300), (200, 300), (300, 100), (300, 200)]

def draw_red_line(surf, color, start, end, w):
    xe = start[0] * (1-w) + end[0] * w
    ye = start[1] * (1-w) + end[1] * w
    pygame.draw.line(surf, color, start, (round(xe), round(ye)))

count=0
run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    window.fill(0)

    for i in range(int(count)):
        draw_red_line(window, (255, 255, 255), line_start[i], line_end[i], 1)
    if count < 4:
        i = int(count)
        draw_red_line(window, (255, 0, 0), line_start[i], line_end[i], count-i)
        count += 0.01
    else:
        count = 0
        
    pygame.display.flip()

pygame.quit()
exit()