Nanfeng

Notes on software development, code, and curious ideas

Drawing Animated Fireworks with Python and Pygame

With only a few hours left before 2023, I wrote a small Pygame fireworks animation to celebrate.

Install Pygame first:

1
python -m pip install pygame

The animation launches a particle upward under gravity. When its vertical velocity becomes non-negative, it explodes into many colored particles. Each particle decelerates, falls, and leaves a short trail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import math
import random
import pygame

WIDTH, HEIGHT = 800, 700
BACKGROUND = (20, 20, 30)
GRAVITY = pygame.Vector2(0, 0.12)


class Spark:
def __init__(self, position, velocity, color, life=90):
self.position = pygame.Vector2(position)
self.velocity = pygame.Vector2(velocity)
self.color = color
self.life = life
self.trail = []

def update(self):
self.trail.insert(0, self.position.copy())
del self.trail[8:]
self.velocity += GRAVITY
self.velocity *= 0.985
self.position += self.velocity
self.life -= 1

def draw(self, surface):
for index, point in enumerate(self.trail):
radius = max(1, 3 - index // 3)
pygame.draw.circle(surface, self.color, point, radius)
pygame.draw.circle(surface, self.color, self.position, 3)

@property
def finished(self):
return self.life <= 0


class Firework:
def __init__(self):
self.position = pygame.Vector2(random.randint(80, WIDTH - 80), HEIGHT)
self.velocity = pygame.Vector2(random.uniform(-0.8, 0.8), random.uniform(-13, -10))
self.color = tuple(random.randint(80, 255) for _ in range(3))
self.exploded = False
self.sparks = []

def explode(self):
self.exploded = True
for _ in range(random.randint(90, 150)):
angle = random.uniform(0, math.tau)
speed = random.uniform(2, 8)
velocity = pygame.Vector2(math.cos(angle), math.sin(angle)) * speed
self.sparks.append(Spark(self.position, velocity, self.color))

def update(self):
if not self.exploded:
self.velocity += GRAVITY
self.position += self.velocity
if self.velocity.y >= 0:
self.explode()
else:
for spark in self.sparks:
spark.update()
self.sparks = [spark for spark in self.sparks if not spark.finished]

def draw(self, surface):
if not self.exploded:
pygame.draw.circle(surface, self.color, self.position, 4)
for spark in self.sparks:
spark.draw(surface)

@property
def finished(self):
return self.exploded and not self.sparks


def main():
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Pygame Fireworks')
clock = pygame.time.Clock()
fireworks = [Firework(), Firework()]
running = True

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
fireworks.append(Firework())

if random.randint(0, 20) == 0:
fireworks.append(Firework())

screen.fill(BACKGROUND)
for firework in fireworks:
firework.update()
firework.draw(screen)
fireworks = [item for item in fireworks if not item.finished]

pygame.display.flip()
clock.tick(60)

pygame.quit()


if __name__ == '__main__':
main()

Press Space to launch an extra firework. The original version also rendered a background image and greeting with a custom font; if you add those assets, update their paths to match your project.

+