GPU Particle Fireworks.¶
A fireworks show built from one-shot GPUParticles2D bursts, cycling through the velocity patterns that give each burst its silhouette.
Tags: 2d particles gpu effects fireworks
spread_pattern decides how a grain’s launch velocity is sampled:
"star": every direction, with speed following a star curve ofspread_pointspoints."ring": every direction at the same speed, a clean circle."gaussian": a soft normal-distributed spray arounddirection."disc": a uniform cone of half-anglespreadarounddirection.
star and ring ignore direction; gaussian and disc aim along it, here
straight up. With one_shot=True an emitter runs for one lifetime, stops
itself and emits finished; with explosiveness=1.0 every grain spawns on the
same frame, so the burst is one crisp shell that falls under gravity and fades
from start_colour to end_colour. speed is in pixels per second and
gravity in pixels per second squared. restart() re-arms a spent emitter, so
a shell is a reusable object: the scene keeps a pool of spent shells, returns
each one to it on finished, and builds a new shell only when every one is
still in the air.
A repeating Timer launches a shell every 0.45 seconds at a random spot in the
upper part of the window. Press Space for an extra burst, or click or tap to
fire one where you pressed.
Source¶
1# /// simvx
2# tags = ["particles", "gpu", "effects", "fireworks"]
3# web = { width = 800, height = 600, responsive = true }
4# ///
5"""GPU Particle Fireworks.
6
7A fireworks show built from one-shot `GPUParticles2D` bursts, cycling through the
8velocity patterns that give each burst its silhouette.
9
10`spread_pattern` decides how a grain's launch velocity is sampled:
11
12- `"star"`: every direction, with speed following a star curve of
13 `spread_points` points.
14- `"ring"`: every direction at the same speed, a clean circle.
15- `"gaussian"`: a soft normal-distributed spray around `direction`.
16- `"disc"`: a uniform cone of half-angle `spread` around `direction`.
17
18`star` and `ring` ignore `direction`; `gaussian` and `disc` aim along it, here
19straight up. With `one_shot=True` an emitter runs for one `lifetime`, stops
20itself and emits `finished`; with `explosiveness=1.0` every grain spawns on the
21same frame, so the burst is one crisp shell that falls under `gravity` and fades
22from `start_colour` to `end_colour`. `speed` is in pixels per second and
23`gravity` in pixels per second squared. `restart()` re-arms a spent emitter, so
24a shell is a reusable object: the scene keeps a pool of spent shells, returns
25each one to it on `finished`, and builds a new shell only when every one is
26still in the air.
27
28A repeating `Timer` launches a shell every 0.45 seconds at a random spot in the
29upper part of the window. Press Space for an extra burst, or click or tap to
30fire one where you pressed.
31"""
32
33import random
34from itertools import cycle
35
36from simvx.core import (
37 Anchor,
38 CanvasLayer,
39 GPUParticles2D,
40 Key,
41 Label,
42 Node2D,
43 Pointer,
44 Timer,
45 Vec2,
46 on_input,
47)
48from simvx.graphics import App
49
50# (spread_pattern, start_colour, end_colour), launched in this order, round and round.
51SHELLS = [
52 ("star", (1.0, 0.85, 0.30, 1.0), (1.0, 0.30, 0.10, 0.0)),
53 ("ring", (0.45, 0.95, 1.0, 1.0), (0.10, 0.35, 0.85, 0.0)),
54 ("gaussian", (1.0, 0.45, 0.85, 1.0), (0.55, 0.10, 0.55, 0.0)),
55 ("star", (0.65, 1.0, 0.45, 1.0), (0.15, 0.55, 0.20, 0.0)),
56 ("disc", (1.0, 0.75, 0.30, 1.0), (0.8, 0.25, 0.05, 0.0)),
57 ("ring", (0.9, 0.6, 1.0, 1.0), (0.4, 0.2, 0.7, 0.0)),
58]
59
60
61class FireworksScene(Node2D):
62 input_context = {"fire": [Key.SPACE]}
63
64 def on_ready(self):
65 self._shells = cycle(SHELLS)
66 self._spent: list[GPUParticles2D] = []
67
68 launcher = self.add_child(Timer(duration=0.45, one_shot=False, autostart=True))
69 launcher.timeout.connect(self._fire)
70
71 hud = self.add_child(CanvasLayer())
72 hud.add_child(Label("GPU Particle Fireworks", font_size=24, anchor=Anchor.TOP_LEFT, inset=10))
73 self._pattern = hud.add_child(Label("", anchor=Anchor.TOP_LEFT, inset=(10, 44)))
74 hud.add_child(Label("Space, click or tap: extra burst", anchor=Anchor.BOTTOM_LEFT, inset=10))
75 self._fire()
76
77 def _new_shell(self) -> GPUParticles2D:
78 shell = self.add_child(
79 GPUParticles2D(
80 amount=1300,
81 lifetime=1.0,
82 speed=300.0,
83 speed_variance=35.0,
84 spread=0.5,
85 direction=(0.0, -1.0, 0.0),
86 gravity=(0.0, 340.0, 0.0),
87 emission_radius=3.0,
88 start_scale=3.0,
89 end_scale=0.4,
90 one_shot=True,
91 explosiveness=1.0,
92 emitting=False,
93 )
94 )
95 shell.finished.connect(lambda: self._spent.append(shell))
96 return shell
97
98 def _fire(self, at: Vec2 | None = None):
99 """Launch a burst at `at`, or at a random spot in the sky."""
100 if at is None:
101 w, h = self.viewport_size
102 at = Vec2(random.uniform(0.2, 0.8) * w, random.uniform(0.25, 0.55) * h)
103 pattern, start, end = next(self._shells)
104 shell = self._spent.pop() if self._spent else self._new_shell()
105 shell.position = at
106 shell.spread_pattern = pattern
107 shell.spread_points = random.choice((5, 6))
108 shell.start_colour = start
109 shell.end_colour = end
110 shell.restart()
111 self._pattern.text = f"pattern: {pattern}"
112
113 @on_input("fire")
114 def _on_fire(self, event):
115 self._fire()
116
117 @on_input(Pointer.PRIMARY)
118 def _on_press(self, event):
119 self._fire(at=Vec2(event.position))
120
121
122if __name__ == "__main__":
123 App(width=800, height=600, title="GPU Particle Fireworks").run(FireworksScene())