GPU Particle Fireworks

a sequential burst show of every spread_pattern.

▶ Run in browser

Tags: 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())