GPU Particle Fireworks¶
a sequential burst show of every spread_pattern.
▶ Run in browserTags: 2d particles gpu effects fireworks
What it demonstrates¶
GPUParticles2D.spread_pattern: how the launch velocity is sampled, which is what gives each burst its silhouette – star - speed follows an N-point star curve (spread_points) ring - omnidirectional, uniform on a circle gaussian - a soft normal-distributed spray around the aim direction disc - a uniform circular cone
one_shot: the emitter runs for exactly one lifetime then stops itself, so a burst spawns, arcs out and fades without ever respawning a dead grain.
restart(): re-arms a spent shell and fires it again from its new position.
The finished signal: it fires the moment a one_shot cycle ends, which is precisely when that shell is free to be launched again.
A small pool of shells is recycled off finished, so several bursts hang in
the air at once like a real fireworks show. With explosiveness = 1.0 every
grain of a shell spawns on the same frame -> one clean burst that drifts under
gravity and fades. speed/gravity are in screen pixels (per second).
Controls: SPACE - fire an extra burst at a random spot click / tap - fire an extra burst at the pointer ESC - Quit
Source¶
1"""GPU Particle Fireworks: a sequential burst show of every spread_pattern.
2
3# /// simvx
4# tags = ["particles", "gpu", "effects", "fireworks"]
5# web = { root = "FireworksScene", width = 800, height = 600, responsive = true }
6# ///
7
8## What it demonstrates
9 - GPUParticles2D.spread_pattern: how the launch velocity is sampled, which is
10 what gives each burst its silhouette --
11 star - speed follows an N-point star curve (spread_points)
12 ring - omnidirectional, uniform on a circle
13 gaussian - a soft normal-distributed spray around the aim direction
14 disc - a uniform circular cone
15 - one_shot: the emitter runs for exactly one lifetime then stops itself, so a
16 burst spawns, arcs out and fades without ever respawning a dead grain.
17 - restart(): re-arms a spent shell and fires it again from its new position.
18 - The finished signal: it fires the moment a one_shot cycle ends, which is
19 precisely when that shell is free to be launched again.
20
21A small pool of shells is recycled off ``finished``, so several bursts hang in
22the air at once like a real fireworks show. With ``explosiveness = 1.0`` every
23grain of a shell spawns on the same frame -> one clean burst that drifts under
24gravity and fades. speed/gravity are in screen pixels (per second).
25
26Controls:
27 SPACE - fire an extra burst at a random spot
28 click / tap - fire an extra burst at the pointer
29 ESC - Quit
30"""
31
32import random
33from collections import deque
34from functools import partial
35
36from simvx.core import GPUParticles2D, Input, InputMap, Key, MouseButton, Node2D, Text2D, Vec2
37from simvx.graphics import App
38
39WIDTH, HEIGHT = 800, 600
40
41# Each shell in the show: (pattern, label, start_colour, end_colour). star/ring
42# are radial bursts (aim direction ignored); gaussian/disc spray along the aim.
43SHELLS = [
44 ("star", "star", (1.0, 0.85, 0.30, 1.0), (1.0, 0.30, 0.10, 0.0)),
45 ("ring", "ring", (0.45, 0.95, 1.0, 1.0), (0.10, 0.35, 0.85, 0.0)),
46 ("gaussian", "gaussian", (1.0, 0.45, 0.85, 1.0), (0.55, 0.10, 0.55, 0.0)),
47 ("star", "star", (0.65, 1.0, 0.45, 1.0), (0.15, 0.55, 0.20, 0.0)),
48 ("disc", "disc", (1.0, 0.75, 0.30, 1.0), (0.8, 0.25, 0.05, 0.0)),
49 ("ring", "ring", (0.9, 0.6, 1.0, 1.0), (0.4, 0.2, 0.7, 0.0)),
50]
51
52POOL_SIZE = 5 # bursts that can hang in the air at once
53BEAT = 0.45 # seconds between launches
54
55
56class FireworksScene(Node2D):
57 """A pool of one_shot GPU shells, restarted for each crisp burst."""
58
59 def on_ready(self):
60 InputMap.add_action("fire", [Key.SPACE])
61 InputMap.add_action("fire_at", [MouseButton.LEFT])
62 InputMap.add_action("quit", [Key.ESCAPE])
63
64 self._rng = random.Random(7)
65 # one_shot + emitting=False: each shell is armed but silent until restart().
66 self._pool = [
67 self.add_child(
68 GPUParticles2D(
69 amount=1300,
70 lifetime=1.0,
71 speed=300.0,
72 speed_variance=35.0,
73 spread=0.5,
74 spread_points=5,
75 direction=(0.0, -1.0, 0.0),
76 gravity=(0.0, 340.0, 0.0),
77 emission_radius=3.0,
78 start_scale=3.0,
79 end_scale=0.4,
80 one_shot=True,
81 emitting=False,
82 explosiveness=1.0, # all grains spawn together -> one crisp shell
83 position=Vec2(-200, -200),
84 name=f"Shell{i}",
85 )
86 )
87 for i in range(POOL_SIZE)
88 ]
89 # finished carries no payload, so bind the shell it belongs to.
90 for shell in self._pool:
91 shell.finished.connect(partial(self._on_shell_finished, shell))
92 self._idle = deque(self._pool) # spent, ready to fire
93 self._burning: deque[GPUParticles2D] = deque() # in flight, oldest first
94 self._next_shell_def = 0
95 self._timer = 0.0
96
97 self.add_child(Text2D(text="GPU Particle Fireworks", position=(10, 10), font_scale=1.5, name="Title"))
98 self._label = self.add_child(Text2D(text="", position=(10, 40), name="Hud"))
99 self.add_child(
100 Text2D(
101 text="SPACE / click = extra burst ESC = quit",
102 position=(10, HEIGHT - 28),
103 colour=(0.7, 0.7, 0.7, 1.0),
104 name="Help",
105 )
106 )
107 self._fire()
108
109 def _on_shell_finished(self, shell: GPUParticles2D):
110 """A one_shot cycle ended: every grain is dead, so the shell can fire again."""
111 self._burning.remove(shell)
112 self._idle.append(shell)
113
114 def _fire(self, at: Vec2 | None = None):
115 """Relaunch a shell as a fresh burst at `at`, or a random sky position."""
116 pattern, label, start_c, end_c = SHELLS[self._next_shell_def % len(SHELLS)]
117 # Prefer a spent shell; if every one is still burning, cut the oldest short.
118 shell = self._idle.popleft() if self._idle else self._burning.popleft()
119 if at is None:
120 at = Vec2(self._rng.uniform(150, WIDTH - 150), self._rng.uniform(140, 340))
121 shell.position = at
122 shell.spread_pattern = pattern
123 shell.spread_points = self._rng.choice((5, 6))
124 shell.start_colour = start_c
125 shell.end_colour = end_c
126 shell.restart() # re-arms one_shot: every grain respawns at once, then the shell stops itself
127 self._burning.append(shell)
128 self._label.text = f"pattern: {label}"
129 self._next_shell_def += 1
130
131 def on_update(self, dt: float):
132 if Input.is_action_just_pressed("quit"):
133 self.app.quit()
134 if Input.is_action_just_pressed("fire"):
135 self._fire()
136 if Input.is_action_just_pressed("fire_at"): # touch on web maps to a left click
137 self._fire(at=Vec2(Input.mouse_position))
138
139 self._timer += dt
140 if self._timer >= BEAT:
141 self._timer -= BEAT
142 self._fire()
143
144
145if __name__ == "__main__":
146 App(width=WIDTH, height=HEIGHT, title="GPU Particle Fireworks").run(FireworksScene())