Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 925
dzi Avatar asked Aug 22 '19 23:08

dzi


2 Answers

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

like image 153
Kingsley Avatar answered Nov 03 '22 01:11

Kingsley


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
like image 34
Selcuk Avatar answered Nov 03 '22 02:11

Selcuk