Search code examples
pythonlistpygamenested-lists

How to draw bubbles and turn them animated into circles


I am trying to make a python program to draw a line and turn it into a circle with an animation using pygame, yet I haven't even gotten through the drawing-the-line code. I have noticed that python is changing the wrong or both items in a list that contains the starting point when the user presses down the left click, stored as the first item, and the current point of the user's mouse as the second.

This is generally what I want it to do: https://youtu.be/vlqZ0LubXCA

Here are the outcomes with and without the lines that update the 2nd item:

with:

The code will draw a single pixel at the mouse's position every frame instead of a line from the starting position to the mouse's position

without:

Without the lines, the previous frame isn't covered and continuously draws new lines over top every frame from the starting position to the mouse

As you can see, or read in the descriptions, the line is necessary to cover the previous frame.

I have marked the lines that change the outcome with arrows:

import pygame, PIL, random
print('\n')

#data
bubbles = []
color_options = [[87, 184, 222]]

pressed = False
released = False
bubline_start = []

background = [50, 25, 25]
size = [500, 500]

#pygame
display = pygame.display.set_mode(size)
pygame.init()

#functions
def new_bub_color():
    color_index = random.randint(0, len(color_options)-1)
    lvl = random.randrange(85, 115)

    bub_color = []
    for val in color_options[color_index]:
        bub_color.append(val*(lvl/100))
    return bub_color


def bubble_line():
    global display, pressed, bubline_start, released, bubbles, color_options
    
    if len(bubbles) > 0:
        if not bubbles[-1][0] == 0:
            #first frame of click
            bub_color = new_bub_color()

            bubbles.append([0, bub_color, [bubline_start, list(pygame.mouse.get_pos())]])
            pygame.draw.line(display, bub_color, bubline_start, pygame.mouse.get_pos())
        else:
            #draw after drags
            pygame.draw.line(display, bubbles[-1][1], bubbles[-1][2][0], list(pygame.mouse.get_pos()))            
            bubbles[-1][2][1] = list(pygame.mouse.get_pos())# <-- HERE
    else:
        #first bubble
        bub_color = new_bub_color()
        
        bubbles.append([0, bub_color, [bubline_start, list(pygame.mouse.get_pos())]])
        pygame.draw.line(display, bub_color, bubline_start, pygame.mouse.get_pos())

    if released:
        bubbles[-1][0] = 1
        bubbles[-1][2][1] = list(pygame.mouse.get_pos())# <-- HERE
        released = False


def cover_prev_frame():
    global bubbles, background, size
    min_pos = []
    max_pos = []

    for bubble in bubbles:
        min_pos = bubble[2][0]
        max_pos = bubble[2][0]

        for point in bubble[2]:
            #x min and max
            if point[0] < min_pos[0]:
                min_pos[0] = point[0]
            elif point[0] > max_pos[0]:
                max_pos[0] = point[0]

            #y min and max
            if point[1] < min_pos[1]:
                min_pos[1] = point[1]
            elif point[1] > max_pos[1]:
                max_pos[1] = point[1]
        max_pos = [max_pos[0]-min_pos[0]+1, max_pos[1]-min_pos[1]+1]

        if type(background) == str:
            #image background
            later = True

        elif type(background) == list:
            #solid color background
            pygame.draw.rect(display, background, pygame.Rect(min_pos, max_pos))


while True:
    pygame.event.pump()
    events = pygame.event.get()

    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()

        elif event.type == pygame.MOUSEBUTTONDOWN and not pressed:
            bubline_start = list(pygame.mouse.get_pos())
            pressed = True
            
        elif event.type == pygame.MOUSEBUTTONUP and pressed:
            pressed = False
            released = True

    cover_prev_frame()
    if pressed or released:
        bubble_line()

    try:
        pygame.display.update()
    except:
        break


Solution

  • if not bubbles[-1][0] == 0: is False as long as the mouse is not released. Therefore add many line segments, each starting at bubline_start and ending at the current mouse position.
    You must redraw the scene in each frame. bubbles is a list of bubbles and each bubble has a list of points. Add a new point to the last bubble in the list while the mouse is held down. Start a new bubble when the mouse is pressed and end a bubble when it is released. This greatly simplifies your code.

    Minimal example

    import pygame, random
    
    size = [500, 500]
    pygame.init()
    display = pygame.display.set_mode(size)
    clock = pygame.time.Clock()
    
    pressed = False
    bubbles = []
    background = [50, 25, 25]
    
    run = True
    while run:
        clock.tick(100)
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                run = False
    
            elif event.type == pygame.MOUSEBUTTONDOWN:
                start_pos = list(event.pos)
                bubble_color = pygame.Color(0)
                bubble_color.hsla = (random.randrange(0, 360), 100, 50, 100)
                bubbles.append((bubble_color, [start_pos]))
                pressed = True
    
            elif event.type == pygame.MOUSEMOTION and pressed:
                new_pos = list(event.pos)
                if len(bubbles[-1][1]) > 0 and bubbles[-1][1] != new_pos:  
                    bubbles[-1][1].append(new_pos)
                
            elif event.type == pygame.MOUSEBUTTONUP:
                pressed = False
                end_pos = list(event.pos)
                if len(bubbles[-1][1]) > 0 and bubbles[-1][1] != end_pos:  
                    bubbles[-1][1].append(list(event.pos))
    
        display.fill(background)
        for i, bubble in enumerate(bubbles):
            if len(bubble[1]) > 1:
                closed = not pressed or i < len(bubbles) - 1
                pygame.draw.lines(display, bubble[0], closed, bubble[1], 3)
        pygame.display.update()
    
    pygame.quit()
    

    For the animation I propose to create a class that represents a bubble and a method animate that slowly turns the polygon into a circle.

    Minimal example

    import pygame, random
    
    size = [500, 500]
    pygame.init()
    display = pygame.display.set_mode(size)
    clock = pygame.time.Clock()
    
    class Bubble:
        def __init__(self, start):
            self.color = pygame.Color(0)
            self.color.hsla = (random.randrange(0, 360), 100, 50, 100)
            self.points = [list(start)]
            self.closed = False
            self.finished = False
        def add_point(self, point, close):
            self.points.append(list(point))
            self.closed = close
            if self.closed:
                x_, y_ = list(zip(*self.points))
                x0, y0, x1, y1 = min(x_), min(y_), max(x_), max(y_)
                rect = pygame.Rect(x0, y0, x1-x0, y1-y0)
                self.center = rect.center
                self.radius = max(*rect.size) // 2
        def animate(self):
            if self.closed and not self.finished:
                cpt = pygame.math.Vector2(self.center) + (0.5, 0.5)
                self.finished = True
                for i, p in enumerate(self.points):
                    pt = pygame.math.Vector2(p)
                    v = pt - cpt
                    l = v.magnitude()
                    if l + 0.5 < self.radius:
                        self.finished = False
                    v.scale_to_length(min(self.radius, l+0.5))
                    pt = cpt + v
                    self.points[i] = [pt.x, pt.y]
                    
        def draw(self, surf):
            if self.finished:
                pygame.draw.circle(surf, self.color, self.center, self.radius, 3)
            elif len(self.points) > 1:
                pygame.draw.lines(surf, self.color, self.closed, self.points, 3)
    
    bubbles = []
    pressed = False
    background = [50, 25, 25]
    
    run = True
    while run:
        clock.tick(100)
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                run = False
    
            elif event.type == pygame.MOUSEBUTTONDOWN:
                bubbles.append(Bubble(event.pos))
                pressed = True
    
            elif event.type == pygame.MOUSEMOTION and pressed:
                bubbles[-1].add_point(event.pos, False)
                
            elif event.type == pygame.MOUSEBUTTONUP:
                bubbles[-1].add_point(event.pos, True)
                pressed = False
    
        for bubble in bubbles:
            bubble.animate()
    
        display.fill(background)
        for bubble in bubbles:
            bubble.draw(display)
        pygame.display.update()
    
    pygame.quit()