Bezier Curves – Can we Draw one With Pygame?

A Bezier curve is a curve defined by three points, a start, an end and one the line curves toward.
But can we programmatically draw one using Pygame? The answer is Yes! And this is what I wrote to do it!
Here is an online version you can interact with: https://replit.com/@d1ddle/Bezier-Curve

And here is the python code. Make sure you have python 3.9 and pygame 2.0.1 + installed:


# BEZIER CURVE SIMULATION WITH PYGAME - https://d1ddle.com #

import pygame, sys

#global variable initialisations.
pygame.init()
pygame.display.set_caption("Bezier Curve - d1ddle")
WIDTH, HEIGHT = 500, 500
if WIDTH - HEIGHT > 0: HEIGHT = WIDTH
else: WIDTH = HEIGHT
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.Font(None, WIDTH//16)

paused = False
done = False
drag = False
coord_show = False
counter = 0
glob_counter = 0

#co-ordinates of first line
P0 = [50 * WIDTH//400, 300* WIDTH//400]
P1 = [200* WIDTH//400, 50* WIDTH//400]
line_1 = P0, P1

#co-ordinates of second line
#P1 = [200, 50] isn't needed since both lines join at a shared point.
P2 = [300* WIDTH//400, 350* WIDTH//400]
line_2 = P1, P2
co_ords = P0, P1, P2


#these look like matrix transformations, but they're actually
#calculations for the X and Y co-ordinates for any point on a curve/ y = mx + c / ax^2 + bx + c.

#this first one uses P(t) = (1-t)*P0 + t*P1
#twice: once for X, once for Y.
#t is the percentage of the length of the line that you request coordinates for.
#so it varies from 0 to 1, gathering and plotting pixels across the screen.
def P(t, line):
    PtX = (1-t)*line[0][0] + t*line[1][0]
    PtY = (1-t)*line[0][1] + t*line[1][1]
    return PtX, PtY

#this is the quadratic part. Unfortunately this is hard
#coded so we can't really have more than one curve on screen at once.
# Qt = (1-t)^2 * P0 + 2*(1-t)*t*P1 + t^2 * P2
def Q(t):
    QtX = ((1-t)**2)*P0[0] + 2*(1-t)*t*P1[0] + (t**2)*P2[0]
    QtY = ((1-t)**2)*P0[1] + 2*(1-t)*t*P1[1] + (t**2)*P2[1]
    return QtX, QtY


#main loop
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                paused = not paused

            if event.key == pygame.K_TAB:
                coord_show = not coord_show

            if event.key == pygame.K_ESCAPE:
                pygame.quit()
                sys.exit()

        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                drag = True
                mouse_x, mouse_y = event.pos

        elif event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                drag = False

        #this whole block checks for mouse click to move the points.
        elif event.type == pygame.MOUSEMOTION:
            if drag:
                mouse_x, mouse_y = event.pos
                if abs(P0[0] - mouse_x) <= WIDTH//20 and abs(P0[1] - mouse_y) <= WIDTH//20:
                    line_1 = [mouse_x, mouse_y], line_1[1]
                    P0 = mouse_x, mouse_y
                if abs(P1[0] - mouse_x) <= WIDTH//20 and abs(P1[1] - mouse_y) <= WIDTH//20:
                    line_1 = line_1[0], [mouse_x, mouse_y]
                    P1 = mouse_x, mouse_y
                    line_2 = [mouse_x, mouse_y], line_2[1]
                if abs(P2[0] - mouse_x) <= WIDTH//20 and abs(P2[1] - mouse_y) <= WIDTH//20:
                    line_2 = line_2[0], [mouse_x, mouse_y]
                    P2 = mouse_x, mouse_y

    #drawing grid
    screen.fill((255,255,255))

    for i in range(0, WIDTH//10):
        pygame.draw.line(screen, (211,211,211), (i*10, WIDTH), (i*10,0))
        pygame.draw.line(screen, (211,211,211), (0, i*10), (HEIGHT, i*10))
    pygame.draw.line(screen, (105,105,105), (WIDTH//2,0), (WIDTH//2, HEIGHT))
    pygame.draw.line(screen, (105,105,105), (0, WIDTH//2), (HEIGHT, WIDTH//2)) 


    #important loop for drawing curved lines.
    for i in range(1, 1000):
        i*=0.001

        #straight line - inefficient. They're drawn above.
##        Pi = (int(P(i, line_1)[0]), int(P(i, line_1)[1]))
##        screen.set_at(Pi, (0,0,0)")
##        
##        Pi2 = (int(P(i, line_2)[0]), int(P(i, line_2)[1]))
##        screen.set_at(Pi2, (0,0,0)")

        #curved quadratic line
        Qi = (int(Q(i)[0]), int(Q(i)[1]))
        screen.set_at(Qi, (0,0,255))

    #calculating red points
    Red = (int(P(counter*0.005, line_1)[0]), int(P(counter*0.005, line_1)[1]))
    Red2 = (int(P(counter*0.005, line_2)[0]), int(P(counter*0.005, line_2)[1]))

    #drawing Blue point
    Qi = (int(Q(counter*0.005)[0]), int(Q(counter*0.005)[1]))
    pygame.draw.circle(screen, (0,0,255), Qi, WIDTH//80)
    
    #efficient way of drawing the black & red line/s
    widt = WIDTH//400
    if widt < 1: widt = 1
    pygame.draw.lines(screen, (0,0,0), False, [(int(P(0, line_1)[0]), int(P(0, line_1)[1])), (int(P(1, line_1)[0]), int(P(1, line_1)[1])), (int(P(1, line_2)[0]), int(P(1, line_2)[1]))] , width = widt)
    pygame.draw.line(screen, (255,0,0), Red, Red2, width = widt)

    #drawing P points & red points
    pygame.draw.circle(screen, (105,105,105), P0, WIDTH//80)
    pygame.draw.circle(screen, (105,105,105), P1, WIDTH//80)
    pygame.draw.circle(screen, (105,105,105), P2, WIDTH//80)
    pygame.draw.circle(screen, (255,0,0), Red2, WIDTH//80)
    pygame.draw.circle(screen, (255,0,0), Red, WIDTH//80)

    #drawing text co-ords. Not enough to make me systematically draw them.
    textsurf = font.render(str((P0[0]-WIDTH//2, (P0[1]-HEIGHT//2)*-1)), False, (105,105,105))
    
    textsurf2 = font.render(str((P1[0]-WIDTH//2, (P1[1]-HEIGHT//2)*-1)), False, (105,105,105))
    
    textsurf3 = font.render(str((P2[0]-WIDTH//2, (P2[1]-HEIGHT//2)*-1)), False, (105,105,105))
    
    textsurf4 = font.render(str((Red[0]-WIDTH//2, (Red[1]-HEIGHT//2)*-1)), False, (255,0,0))
    
    textsurf5 = font.render(str((Red2[0]-WIDTH//2, (Red2[1]-HEIGHT//2)*-1)), False, (255,0,0))
    
    textsurf6 = font.render(str((Qi[0]-WIDTH//2, (Qi[1]-HEIGHT//2)*-1)), False, (0,0,255))

    
    textsurf7 = font.render("TAB toggle coord", False, (50,50,50))
    screen.blit(textsurf7, (WIDTH//2 + 60, WIDTH//4 * 3 + WIDTH//8 - 25))
    textsurf8 = font.render("SPACE to Pause", False, (50,50,50))
    screen.blit(textsurf8, (WIDTH//2 + 60, WIDTH//4 * 3 + WIDTH//8 + WIDTH//80 * 3 - 25))
    textsurf9 = font.render("Scale 1:10", False, (50,50,50))
    screen.blit(textsurf9, (WIDTH//2 + 60, WIDTH//4 * 3 + WIDTH//8 + WIDTH//80 * 6 - 25))

    if coord_show:
        screen.blit(textsurf, (P0))
        screen.blit(textsurf2, (P1))
        screen.blit(textsurf3, (P2))
        screen.blit(textsurf4, (Red))
        screen.blit(textsurf5, (Red2))
        screen.blit(textsurf6, (Qi))
    else:
        screen.blit(textsurf, (0, 0))
        screen.blit(textsurf2, (0, WIDTH // 16))
        screen.blit(textsurf3, (0, WIDTH // 8))
        screen.blit(textsurf4, (0, WIDTH // 5.3))
        screen.blit(textsurf5, (0, WIDTH // 4))
        screen.blit(textsurf6, (0, WIDTH // 3.2))
    
    #counter and update.
    if counter > 199:
        counter = -1
    if not paused:
        counter += 1

    pygame.display.set_caption("Bezier Curve - d1ddle - Speed: " + str(int(clock.get_fps())))

    glob_counter += 1
    pygame.display.flip()
    clock.tick(60)

Leave a Reply