Q:

How do I make pygame draw along all points on a line between 2 points?

I am trying to make a brush function that draws onto a canvas with a calligraphy style of brush, thin one way, thick the other. Right now, the actual drawing of the brush footprint works, but the code does not run fast enough, and the actual line keeps cutting (as shown in the gif).
This is my code right now:
import pygame
import os
import random
from pygame.locals import *

flags = DOUBLEBUF

pygame.init()
pygame.event.set_allowed([QUIT])

current_path = os.path.dirname(__file__) #The directory the main file is in
iconPath = os.path.join(current_path, 'images') #The icon folder path

displayWidth = 1280
displayHeight = 720

gameDisplay = pygame.display.set_mode((displayWidth, displayHeight), flags)
gameDisplay.set_alpha(None)
pygame.display.set_caption('PyPaint')

black = (0, 0, 0)
white = (255, 255, 255)
grey = (200, 200, 200)
cyan = (0, 200, 255)
green = (0, 150, 0)
lightGreen = (0, 255, 0)
red = (150, 0, 0)
lightRed = (255, 0, 0)

smallfont = pygame.font.SysFont("arial", 40)
medfont = pygame.font.SysFont("arial", 60)
largefont = pygame.font.SysFont("arial", 80)

airbrushIcon = pygame.image.load(os.path.join(iconPath, "airbrush.png"))
pencilIcon = pygame.image.load(os.path.join(iconPath, "pencil.png"))
calligraphyIcon = pygame.image.load(os.path.join(iconPath, "calligraphy.png"))
eraserIcon = pygame.image.load(os.path.join(iconPath, "eraser.png"))

clock = pygame.time.Clock()
FPS = 60

airbrushMode = False
calligraphyMode = False
eraserMode = False

def paintScreen():
    global airbrushMode
    global calligraphyMode
    global eraserMode
    airbrushMode = False
    paint = True
    gameDisplay.fill(cyan)
    message_to_screen('Welcome to PyPaint', black, -300, 'large')
    click = pygame.mouse.get_pressed()
    pygame.draw.rect(gameDisplay, white, (50, 120, displayWidth - 100, displayHeight - 240))
    while paint:
        cur = pygame.mouse.get_pos()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()

        button('X', 20, 20, 50, 50, red, lightRed, action = 'quit')
        icon(airbrushIcon, white, 50, displayHeight - 101, 51, 51, white, grey, 'airbrush')
        icon(pencilIcon, white, 140, displayHeight - 101, 51, 51, white, grey, 'pencil')
        icon(calligraphyIcon, white, 230, displayHeight - 101, 51, 51, white, grey, 'calligraphy')
        icon(eraserIcon, white, 320, displayHeight - 101, 51, 51, white, grey, 'eraser')
        pygame.draw.rect(gameDisplay, cyan, (0, 120, 50, displayHeight - 100))#to clean up the left border of the canvas
        pygame.draw.rect(gameDisplay, cyan, (displayWidth - 50, 120, 50, displayHeight - 100))#to clean up the right border of the canvas
        pygame.draw.rect(gameDisplay, cyan, (0, displayHeight - 120, displayWidth, 20))#to clean up the bottom of the canvas
        pygame.draw.rect(gameDisplay, cyan, (0, 100, displayWidth, 20))#to clean up the top of the canvas
        if airbrushMode == True:
            airbrush()
        elif calligraphyMode == True:
            calligraphy()
        elif eraserMode == True:
            eraser()
        pygame.display.update()

def icon(icon, colour, x, y, width, height, inactiveColour, activeColour, action = None):
    global airbrushMode
    global calligraphyMode
    global eraserMode
    cur = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    if x + width > cur[0] > x and y + height > cur[1] > y:#if the cursor is over the button
        pygame.draw.rect(gameDisplay, activeColour, (x, y, width, height))
        gameDisplay.blit(icon, (x, y))
        if click[0] == 1 and action != None: #if clicked
            if action == 'quit':
                pygame.quit()
                quit()
            elif action == 'pencil':
                pencilMode = True
                airbrushMode = False
                calligraphyMode = False
                eraserMode = False
            elif action == 'airbrush':
                airbrushMode = True
                calligraphyMode = False
                pencilMode = False
                eraserMode = False
            elif action == 'calligraphy':
                calligraphyMode = True
                airbrushMode = False
                pencilMode = False
                eraserMode = False
            elif action == 'eraser':
                eraserMode = True
                airbrushMode = False
                pencilMode = False
                calligraphyMode = False
    else:
        pygame.draw.rect(gameDisplay, inactiveColour, (x, y, width, height))
        gameDisplay.blit(icon, (x, y))

def button(text, x, y, width, height, inactiveColour, activeColour, action = None):
    cur = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    if x + width > cur[0] > x and y + height > cur[1] > y:
        pygame.draw.rect(gameDisplay, activeColour, (x, y, width, height))
        if click[0] == 1 and action != None:
            if action == 'quit':
                pygame.quit()
                quit()
    else:
        pygame.draw.rect(gameDisplay, inactiveColour, (x, y, width, height))
    text_to_button(text, black, x, y, width, height)

def text_to_button(msg, colour, buttonx, buttony, buttonwidth, buttonheight, size = 'small'):
    textSurf, textRect = text_objects (msg, colour, size)
    textRect.center = ((buttonx + (buttonwidth/2)), buttony + (buttonheight/2))
    gameDisplay.blit(textSurf, textRect)

def message_to_screen(msg, colour, y_displace = 0, size = 'small'):
    textSurf, textRect = text_objects (msg, colour, size)
    textRect.center = (displayWidth / 2), (displayHeight / 2) + y_displace
    gameDisplay.blit(textSurf, textRect)

def airbrush(brushSize = 3):
    cur = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    if cur[0] >= 50 and cur[0] <= displayWidth - 50 and cur[1] >= 120 and cur[1] <= displayHeight - 120:
        if click[0] == 1:
            pygame.draw.circle(gameDisplay, black, (cur[0] + random.randrange(-brushSize * 2, brushSize * 2), cur[1] + random.randrange(-brushSize * 2, brushSize * 2)), random.randrange(1, brushSize * 2))

def calligraphy(brushSize = 3):
    cur = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    if cur[0] >= 50 and cur[0] <= displayWidth - 50 and cur[1] >= 120 and cur[1] <= displayHeight - 120:#if cursor is on the canvas
        if click[0] == 1:
            pygame.draw.rect(gameDisplay, black, (cur[0] - brushSize / 2, cur[1] - brushSize / 4, brushSize, brushSize * 3))

def eraser(brushSize = 3):
    cur = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    if cur[0] >= 50 and cur[0] <= displayWidth - 50 and cur[1] >= 120 and cur[1] <= displayHeight - 120:#if cursor is on the canvas
        if click[0] == 1:
            pygame.draw.rect(gameDisplay, white, (cur[0] - brushSize / 2, cur[1] - brushSize / 2, brushSize * 6, brushSize * 6))

def text_objects(text, colour, size):
    if size == 'small':
        textSurface = smallfont.render (text, True, colour)
    elif size == 'medium':
        textSurface = medfont.render (text, True, colour)
    elif size == 'large':
        textSurface = largefont.render (text, True, colour)
    return textSurface, textSurface.get_rect() 

paintScreen()

I tried adding clock.tick() to several different functions to try to run it as fast as possible, but it still cuts the same way. I even tested it on a newer, faster computer, and there was no difference, which means that the issue lies within python and not the computer. How can I allow pygame to draw on all points on a line between 2 points?

A:

I created minimal working example using method from my comment.
I remember previous point (or None) to draw missing points between new point and previous one.
I calculate how many points I will have to add
steps = max(abs(x-prev_x), abs(y-prev_y))

and distance between points
dx = (x - prev_x)/steps
dy = (y - prev_y)/steps

and then I can loop to draw missing points
for _ in range(steps):
    prev_x += dx
    prev_y += dy
    pygame.draw.circle(display, BLACK, (round(prev_x - 5), round(prev_y - 5)), 10)

Full code
import pygame

# --- constants --- (uppercase)
BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)

WIDTH = 800
HEIGHT = 600
FPS = 60

# --- functions --- (lowercase)

def airbrush(brushSize = 3):
    global prev_x
    global prev_y

    click = pygame.mouse.get_pressed()
    if click[0] == 1:
        x, y = pygame.mouse.get_pos()
        if x >= 0 and x <= WIDTH and y >= 0 and y <= HEIGHT0:
            pygame.draw.circle(display, BLACK, (x - 5, y - 5), 10)

        # if there is previous point then draw missing points 
        if prev_x is not None:
            diff_x = x - prev_x
            diff_y = y - prev_y
            steps = max(abs(diff_x), abs(diff_y))

            # skip if distance is zero (error: dividing by zero)
            if steps > 0:
                dx = diff_x / steps
                dy = diff_y / steps
                for _ in range(steps):
                    prev_x += dx
                    prev_y += dy
                    pygame.draw.circle(display, BLACK, (round(prev_x - 5), round(prev_y - 5)), 10)
        prev_x = x # remeber previous point
        prev_y = y # remeber previous point
    else:
        prev_x = None # there is no previous point
        prev_y = None # there is no previous point

# --- main ---

pygame.init()

display = pygame.display.set_mode((WIDTH, HEIGHT), pygame.DOUBLEBUF)

prev_x = None # at start there is no previous point
prev_y = None # at start there is no previous point

display.fill(WHITE)
clock = pygame.time.Clock()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

    airbrush()
    pygame.display.update()
    clock.tick(FPS)

if you remove lines
prev_x = x
prev_y = y

then you get version without missing points.

