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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With