15

As I see it, there are two ways to handle mouse events to draw a picture.

The first is to detect when the mouse moves and draw a line to where the mouse is, shown here. However, the problem with this is that with a large brush size, many gaps appear between each "line" that is not straight since it is using the line's stroke size to create thick lines.

The other way is to draw circles when the mouse moves as is shown here. The problem with this is that gaps appear between each circle if the mouse moves faster than the computer detects mouse input.

Here's a screenshot with my issues with both:

http://imgur.com/32DXN.jpg

What is the best way to implement a brush like MS Paint's, with a decently-big brush size with no gaps in the stroke of the line or no gaps between each circle?

4
  • I don't understand your problem with lines. Are you talking about the lack of endcaps, or...? Feb 28, 2009 at 2:24
  • Just combine the two. Thick lines + circles. Feb 28, 2009 at 2:33
  • Here's a screenshot of my issues with both: imgur.com/32DXN Using both thick lines and circles seem like it could fix the issue, but it doesn't sound very elegant, so I'll think about it some more and use that as a last resort.
    – Johnston
    Feb 28, 2009 at 2:45
  • +1 cause I had to implement one... gah! Feb 28, 2009 at 4:03

5 Answers 5

16

Why not do both?

Draw a circle at each endpoint and a line between the two.

EDIT rofl, just couldn't stop myself.

Actually, you don't want to use pygame.draw.line because it cheats. It fills a 1 pixel wide row or column (depending on angle of attack) of pixels. If you do go at a roughly perpendicular angle, 0 deg or 90 deg, this isn't an issue, but at 45's, you'll notice a sort of string bean effect.

The only solution is to draw a circle at every pixel's distance. Here...

import pygame, random

screen = pygame.display.set_mode((800,600))

draw_on = False
last_pos = (0, 0)
color = (255, 128, 0)
radius = 10

def roundline(srf, color, start, end, radius=1):
    dx = end[0]-start[0]
    dy = end[1]-start[1]
    distance = max(abs(dx), abs(dy))
    for i in range(distance):
        x = int( start[0]+float(i)/distance*dx)
        y = int( start[1]+float(i)/distance*dy)
        pygame.draw.circle(srf, color, (x, y), radius)

try:
    while True:
        e = pygame.event.wait()
        if e.type == pygame.QUIT:
            raise StopIteration
        if e.type == pygame.MOUSEBUTTONDOWN:
            color = (random.randrange(256), random.randrange(256), random.randrange(256))
            pygame.draw.circle(screen, color, e.pos, radius)
            draw_on = True
        if e.type == pygame.MOUSEBUTTONUP:
            draw_on = False
        if e.type == pygame.MOUSEMOTION:
            if draw_on:
                pygame.draw.circle(screen, color, e.pos, radius)
                roundline(screen, color, e.pos, last_pos,  radius)
            last_pos = e.pos
        pygame.display.flip()

except StopIteration:
    pass

pygame.quit()
0
3

Not blitting at each loop step can improve the speed of the drawing (using this code adapted from the previous one allow to remove lag problem on my machine)

import pygame, random

screen = pygame.display.set_mode((800,600))

draw_on = False
last_pos = (0, 0)
color = (255, 128, 0)
radius = 10

def roundline(srf, color, start, end, radius=1):
    dx = end[0]-start[0]
    dy = end[1]-start[1]
    distance = max(abs(dx), abs(dy))
    for i in range(distance):
        x = int( start[0]+float(i)/distance*dx)
        y = int( start[1]+float(i)/distance*dy)
        pygame.display.update(pygame.draw.circle(srf, color, (x, y), radius))

try:
    while True:
        e = pygame.event.wait()
        if e.type == pygame.QUIT:
            raise StopIteration
        if e.type == pygame.MOUSEBUTTONDOWN:
            color = (random.randrange(256), random.randrange(256), random.randrange(256))
            pygame.draw.circle(screen, color, e.pos, radius)
            draw_on = True
        if e.type == pygame.MOUSEBUTTONUP:
            draw_on = False
        if e.type == pygame.MOUSEMOTION:
            if draw_on:
                pygame.display.update(pygame.draw.circle(screen, color, e.pos, radius))
                roundline(screen, color, e.pos, last_pos,  radius)
            last_pos = e.pos
        #pygame.display.flip()

except StopIteration:
    pass

pygame.quit()
2

For the first problem, you need have a background, even if its just a color. I had the same problem with a replica pong game i made. Here is an example of a replica paint program I made, left click to draw, right click to erase, click on the color image to choose a color, and up button to clear screen:

import os
os.environ['SDL_VIDEO_CENTERED'] = '1'
from pygamehelper import *
from pygame import *
from pygame.locals import *
from vec2d import *
from math import e, pi, cos, sin, sqrt
from random import uniform

class Starter(PygameHelper):
    def __init__(self):
        self.w, self.h = 800, 600
        PygameHelper.__init__(self, size=(self.w, self.h), fill=((255,255,255)))

        self.img= pygame.image.load("colors.png")
        self.screen.blit(self.img, (0,0))

        self.drawcolor= (0,0,0)
        self.x= 0

    def update(self):
        pass

    def keyUp(self, key):
        if key==K_UP:
            self.screen.fill((255,255,255))
            self.screen.blit(self.img, (0,0))




    def mouseUp(self, button, pos):
        pass

    def mouseMotion(self, buttons, pos, rel):
        if pos[1]>=172: 
            if buttons[0]==1:
                #pygame.draw.circle(self.screen, (0,0,0), pos, 5)
                pygame.draw.line(self.screen, self.drawcolor, pos, (pos[0]-rel[0], pos[1]-rel[1]),5)                
            if buttons[2]==1:
                pygame.draw.circle(self.screen, (255,255,255), pos, 30)
            if buttons[1]==1:
                #RAINBOW MODE
                color= self.screen.get_at((self.x, 0))
                pygame.draw.line(self.screen, color, pos, (pos[0]-rel[0], pos[1]-rel[1]), 5)

                self.x+= 1
                if self.x>172: self.x=0

        else:
            if pos[0]<172:
                if buttons[0]==1:
                    self.drawcolor= self.screen.get_at(pos)
                    pygame.draw.circle(self.screen, self.drawcolor, (250, 100), 30)

    def draw(self):
        pass
        #self.screen.fill((255,255,255))
        #pygame.draw.circle(self.screen, (0,0,0), (50,100), 20)

s = Starter()
s.mainLoop(40)
1

Here's a simplified version of Matthew's example which is unfortunately not runnable.

When the mouse is moved, pygame.MOUSEMOTION events are added to the event queue which contain the position and the relative movement. You can use these to calculate the previous position and then pass the two points to pygame.draw.line.

pygame.MOUSEMOTION events also have a buttons attribute which you can use to check which mouse button is currently down.

import os
import random

import pygame as pg


class App:

    def __init__(self):
        os.environ['SDL_VIDEO_CENTERED'] = '1'
        pg.init()
        self.w, self.h = 800, 600
        self.screen = pg.display.set_mode((self.w, self.h))
        self.screen.fill(pg.Color('white'))
        self.clock = pg.time.Clock()
        self.drawcolor = (0, 0, 0)

    def mainloop(self):
        while True:
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    return
                elif event.type == pg.MOUSEBUTTONDOWN:
                    if event.button == 2:  # Color picker (middle mouse button).
                        self.drawcolor = self.screen.get_at(pos)
                        # Pick a random color.
                        # self.drawcolor = [random.randrange(256) for _ in range(3)]
                elif event.type == pg.MOUSEMOTION:
                    pos, rel = event.pos, event.rel
                    if event.buttons[0]:  # If the left mouse button is down.
                        # Draw a line from the pos to the previous pos.
                        pg.draw.line(self.screen, self.drawcolor, pos, (pos[0]-rel[0], pos[1]-rel[1]), 5)
                    elif event.buttons[2]:  # If the right mouse button is down.
                        # Erase by drawing a circle.
                        pg.draw.circle(self.screen, (255, 255, 255), pos, 30)

            pg.display.flip()
            self.clock.tick(30)


if __name__ == '__main__':
    app = App()
    app.mainloop()
    pg.quit()
0

I have made a more efficient version of the roundline funktion.

import pygame as pg,random,math


screen = pg.display.set_mode((800,600))

draw_on = False
last_pos = (0, 0)
color = (255, 128, 0)
radius = 10

def roundline(srf, color, start, end, radius=1):
    pg.display.update([drawline(srf,color,start,end,radius),
                       pg.draw.circle(srf, color, end, radius),
                       pg.draw.circle(srf, color, start, radius)])

def drawline(srf, color,start,end,width):
    v=(start[0]- end[0],start[1]- end[1])
    betrag=width/math.sqrt(v[0]*v[0]+v[1]*v[1])
    v_=(v[1]*betrag,-betrag*v[0])
    return pg.draw.polygon(srf, color,(
            (int(start[0]+v_[0]),int(start[1]+v_[1])),
            (int(start[0]-v_[0]),int(start[1]-v_[1])),
            (int(end[0]-v_[0]),int(end[1]-v_[1])),
            (int(end[0]+v_[0]),int(end[1]+v_[1]))
            ))

try:
    while True:
        e = pg.event.wait()
        if e.type == pg.QUIT:
            raise StopIteration
        elif e.type == pg.MOUSEBUTTONDOWN:
            color = (random.randrange(256), random.randrange(256), random.randrange(256))
            #pg.draw.circle(screen, color, e.pos, radius)
            draw_on = True
            start=e.pos
        elif e.type == pg.MOUSEBUTTONUP:
            draw_on = False
        elif e.type == pg.MOUSEMOTION:
            if draw_on:
                roundline(screen, color, e.pos, last_pos,  radius)
            last_pos = e.pos
        elif e.type == pg.MOUSEWHEEL:
            radius=max(1,radius+e.y)
finally:
    pg.quit()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.