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
Firing crisp one-shot bursts by pulsing
emitting: a paused emitter lets its live grains finish their arc and fade, then never respawns dead ones.A small pool of reusable shells relaunched round-robin, so several bursts hang in the air at once like a real fireworks show.
Each burst recycles a shell whose grains have all died since it last fired, so the pulse repopulates every grain at once -> one clean burst that drifts under gravity and fades. speed/gravity are in screen pixels (per second).
Controls: SPACE - fire an extra burst now 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 - Firing crisp one-shot bursts by pulsing ``emitting``: a paused emitter lets
16 its live grains finish their arc and fade, then never respawns dead ones.
17 - A small pool of reusable shells relaunched round-robin, so several bursts
18 hang in the air at once like a real fireworks show.
19
20Each burst recycles a shell whose grains have all died since it last fired, so
21the pulse repopulates every grain at once -> one clean burst that drifts under
22gravity and fades. speed/gravity are in screen pixels (per second).
23
24Controls:
25 SPACE - fire an extra burst now
26 ESC - Quit
27"""
28
29import random
30
31from simvx.core import GPUParticles2D, Input, InputMap, Key, Node2D, Text2D, Vec2
32from simvx.graphics import App
33
34WIDTH, HEIGHT = 800, 600
35
36# Each shell in the show: (pattern, label, start_colour, end_colour). star/ring
37# are radial bursts (aim direction ignored); gaussian/disc spray along the aim.
38SHELLS = [
39 ("star", "star", (1.0, 0.85, 0.30, 1.0), (1.0, 0.30, 0.10, 0.0)),
40 ("ring", "ring", (0.45, 0.95, 1.0, 1.0), (0.10, 0.35, 0.85, 0.0)),
41 ("gaussian", "gaussian", (1.0, 0.45, 0.85, 1.0), (0.55, 0.10, 0.55, 0.0)),
42 ("star", "star", (0.65, 1.0, 0.45, 1.0), (0.15, 0.55, 0.20, 0.0)),
43 ("disc", "disc", (1.0, 0.75, 0.30, 1.0), (0.8, 0.25, 0.05, 0.0)),
44 ("ring", "ring", (0.9, 0.6, 1.0, 1.0), (0.4, 0.2, 0.7, 0.0)),
45]
46
47POOL_SIZE = 5 # >= ceil(grain_lifetime / BEAT) so a relaunched shell is fully dead
48BEAT = 0.45 # seconds between launches
49
50
51class FireworksScene(Node2D):
52 """Round-robin pool of GPU shells, each pulsed for one crisp burst."""
53
54 def on_ready(self):
55 InputMap.add_action("fire", [Key.SPACE])
56 InputMap.add_action("quit", [Key.ESCAPE])
57
58 self._rng = random.Random(7)
59 # All shells start paused (emitting=False) so nothing renders until fired.
60 self._pool = [
61 self.add_child(GPUParticles2D(
62 amount=1300, lifetime=1.0, speed=300.0, speed_variance=35.0,
63 spread=0.5, spread_points=5, direction=(0.0, -1.0, 0.0),
64 gravity=(0.0, 340.0, 0.0), emission_radius=3.0,
65 start_scale=3.0, end_scale=0.4, emitting=False,
66 position=Vec2(-200, -200), name=f"Shell{i}",
67 ))
68 for i in range(POOL_SIZE)
69 ]
70 self._next_shell = 0
71 self._next_shell_def = 0
72 self._timer = 0.0
73 self._cooldown: list[tuple[GPUParticles2D, float]] = [] # (shell, off_at) pulses
74
75 self.add_child(Text2D(text="GPU Particle Fireworks", position=(10, 10), font_scale=1.5, name="Title"))
76 self._label = self.add_child(Text2D(text="", position=(10, 40), name="Hud"))
77 self.add_child(Text2D(
78 text="SPACE = extra burst ESC = quit",
79 position=(10, HEIGHT - 28), colour=(0.7, 0.7, 0.7, 1.0), name="Help",
80 ))
81 self._fire()
82
83 def _fire(self):
84 """Relaunch the next pooled shell as a fresh burst at a new sky position."""
85 pattern, label, start_c, end_c = SHELLS[self._next_shell_def % len(SHELLS)]
86 shell = self._pool[self._next_shell % POOL_SIZE]
87 shell.position = Vec2(self._rng.uniform(150, WIDTH - 150), self._rng.uniform(140, 340))
88 shell.spread_pattern = pattern
89 shell.spread_points = self._rng.choice((5, 6))
90 shell.start_colour = start_c
91 shell.end_colour = end_c
92 shell.emitting = True # pulse on: this frame repopulates every grain
93 self._cooldown.append((shell, self.tree.now + 0.06)) # ...then off, so it fires once
94 self._label.text = f"pattern: {label}"
95 self._next_shell += 1
96 self._next_shell_def += 1
97
98 def on_update(self, dt: float):
99 if Input.is_action_just_pressed("quit"):
100 self.app.quit()
101 if Input.is_action_just_pressed("fire"):
102 self._fire()
103
104 # End each pulse shortly after it starts: the burst has spawned, so stop
105 # emitting and let the grains arc out and fade.
106 now = self.tree.now
107 still = []
108 for shell, off_at in self._cooldown:
109 if now >= off_at:
110 shell.emitting = False
111 else:
112 still.append((shell, off_at))
113 self._cooldown = still
114
115 self._timer += dt
116 if self._timer >= BEAT:
117 self._timer -= BEAT
118 self._fire()
119
120
121if __name__ == "__main__":
122 App(width=WIDTH, height=HEIGHT, title="GPU Particle Fireworks").run(FireworksScene())