Particle System - Fireworks

Fireworks
particle_fireworks.py
  1"""
  2Particle Fireworks
  3
  4Use a fireworks display to demonstrate "real-world" uses of Emitters and Particles
  5
  6If Python and Arcade are installed, this example can be run from the command line with:
  7python -m arcade.examples.particle_fireworks
  8"""
  9import random
 10from typing import Optional
 11
 12import pyglet
 13from pyglet.math import Vec2
 14
 15import arcade
 16from arcade.types import Point, PathOrTexture
 17from arcade.math import rand_in_rect, clamp, lerp, rand_in_circle, rand_on_circle
 18from arcade.particles import (
 19    Emitter,
 20    LifetimeParticle,
 21    FadeParticle,
 22    EmitterIntervalWithTime,
 23    EmitMaintainCount,
 24    EmitBurst,
 25)
 26
 27
 28SCREEN_WIDTH = 800
 29SCREEN_HEIGHT = 600
 30SCREEN_TITLE = "Particle based fireworks"
 31LAUNCH_INTERVAL_MIN = 1.5
 32LAUNCH_INTERVAL_MAX = 2.5
 33TEXTURE = "images/pool_cue_ball.png"
 34RAINBOW_COLORS = (
 35    arcade.color.ELECTRIC_CRIMSON,
 36    arcade.color.FLUORESCENT_ORANGE,
 37    arcade.color.ELECTRIC_YELLOW,
 38    arcade.color.ELECTRIC_GREEN,
 39    arcade.color.ELECTRIC_CYAN,
 40    arcade.color.MEDIUM_ELECTRIC_BLUE,
 41    arcade.color.ELECTRIC_INDIGO,
 42    arcade.color.ELECTRIC_PURPLE,
 43)
 44SPARK_TEXTURES = [arcade.make_circle_texture(8, clr) for clr in RAINBOW_COLORS]
 45SPARK_PAIRS = [
 46    [SPARK_TEXTURES[0], SPARK_TEXTURES[3]],
 47    [SPARK_TEXTURES[1], SPARK_TEXTURES[5]],
 48    [SPARK_TEXTURES[7], SPARK_TEXTURES[2]],
 49]
 50ROCKET_SMOKE_TEXTURE = arcade.make_soft_circle_texture(15, arcade.color.GRAY)
 51PUFF_TEXTURE = arcade.make_soft_circle_texture(80, (40, 40, 40, 255))
 52FLASH_TEXTURE = arcade.make_soft_circle_texture(70, (128, 128, 90, 255))
 53CLOUD_TEXTURES = [
 54    arcade.make_soft_circle_texture(50, arcade.color.WHITE),
 55    arcade.make_soft_circle_texture(50, arcade.color.LIGHT_GRAY),
 56    arcade.make_soft_circle_texture(50, arcade.color.LIGHT_BLUE),
 57]
 58STAR_TEXTURES = [
 59    arcade.make_soft_circle_texture(8, arcade.color.WHITE),
 60    arcade.make_soft_circle_texture(8, arcade.color.PASTEL_YELLOW),
 61]
 62SPINNER_HEIGHT = 75
 63
 64
 65def make_spinner():
 66    spinner = Emitter(
 67        center_xy=(SCREEN_WIDTH / 2, SPINNER_HEIGHT - 5),
 68        emit_controller=EmitterIntervalWithTime(0.025, 2.0),
 69        particle_factory=lambda emitter: FadeParticle(
 70            filename_or_texture=random.choice(STAR_TEXTURES),
 71            change_xy=(0, 6.0),
 72            lifetime=0.2
 73        )
 74    )
 75    spinner.change_angle = 16.28
 76    return spinner
 77
 78
 79def make_rocket(emit_done_cb):
 80    """Emitter that displays the smoke trail as the firework shell climbs into the sky"""
 81    rocket = RocketEmitter(
 82        center_xy=(random.uniform(100, SCREEN_WIDTH - 100), 25),
 83        emit_controller=EmitterIntervalWithTime(0.04, 2.0),
 84        particle_factory=lambda emitter: FadeParticle(
 85            filename_or_texture=ROCKET_SMOKE_TEXTURE,
 86            change_xy=rand_in_circle((0.0, 0.0), 0.08),
 87            scale=0.5,
 88            lifetime=random.uniform(1.0, 1.5),
 89            start_alpha=100,
 90            end_alpha=0,
 91            mutation_callback=rocket_smoke_mutator
 92        ),
 93        emit_done_cb=emit_done_cb
 94    )
 95    rocket.change_x = random.uniform(-1.0, 1.0)
 96    rocket.change_y = random.uniform(5.0, 7.25)
 97    return rocket
 98
 99
100def make_flash(prev_emitter):
101    """Return emitter that displays the brief flash when a firework shell explodes"""
102    return Emitter(
103        center_xy=prev_emitter.get_pos(),
104        emit_controller=EmitBurst(3),
105        particle_factory=lambda emitter: FadeParticle(
106            filename_or_texture=FLASH_TEXTURE,
107            change_xy=rand_in_circle((0.0, 0.0), 3.5),
108            lifetime=0.15
109        )
110    )
111
112
113def make_puff(prev_emitter):
114    """Return emitter that generates the subtle smoke cloud left after a firework shell explodes"""
115    return Emitter(
116        center_xy=prev_emitter.get_pos(),
117        emit_controller=EmitBurst(4),
118        particle_factory=lambda emitter: FadeParticle(
119            filename_or_texture=PUFF_TEXTURE,
120            change_xy=Vec2(*rand_in_circle((0.0, 0.0), 0.4)) + Vec2(0.3, 0.0),
121            lifetime=4.0
122        )
123    )
124
125
126class AnimatedAlphaParticle(LifetimeParticle):
127    """A custom particle that animates between three different alpha levels"""
128
129    def __init__(
130            self,
131            filename_or_texture: Optional[PathOrTexture],
132            change_xy: Vec2,
133            start_alpha: int = 0,
134            duration1: float = 1.0,
135            mid_alpha: int = 255,
136            duration2: float = 1.0,
137            end_alpha: int = 0,
138            center_xy: Point = (0.0, 0.0),
139            angle: float = 0,
140            change_angle: float = 0,
141            scale: float = 1.0,
142            mutation_callback=None,
143    ):
144        super().__init__(filename_or_texture,
145                         change_xy,
146                         duration1 + duration2,
147                         center_xy,
148                         angle,
149                         change_angle,
150                         scale,
151                         start_alpha,
152                         mutation_callback)
153        self.start_alpha = start_alpha
154        self.in_duration = duration1
155        self.mid_alpha = mid_alpha
156        self.out_duration = duration2
157        self.end_alpha = end_alpha
158
159    def update(self):
160        super().update()
161        if self.lifetime_elapsed <= self.in_duration:
162            u = self.lifetime_elapsed / self.in_duration
163            self.alpha = clamp(lerp(self.start_alpha, self.mid_alpha, u), 0, 255)
164        else:
165            u = (self.lifetime_elapsed - self.in_duration) / self.out_duration
166            self.alpha = clamp(lerp(self.mid_alpha, self.end_alpha, u), 0, 255)
167
168
169class RocketEmitter(Emitter):
170    """Custom emitter class to add gravity to the emitter to represent gravity on the firework shell"""
171
172    def update(self):
173        super().update()
174        # gravity
175        self.change_y += -0.05
176
177
178class FireworksApp(arcade.Window):
179    def __init__(self):
180        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
181
182        self.background_color = arcade.color.BLACK
183        self.emitters = []
184
185        self.launch_firework(0)
186        arcade.schedule(self.launch_spinner, 4.0)
187
188        stars = Emitter(
189            center_xy=(0.0, 0.0),
190            emit_controller=EmitMaintainCount(20),
191            particle_factory=lambda emitter: AnimatedAlphaParticle(
192                filename_or_texture=random.choice(STAR_TEXTURES),
193                change_xy=(0.0, 0.0),
194                start_alpha=0,
195                duration1=random.uniform(2.0, 6.0),
196                mid_alpha=128,
197                duration2=random.uniform(2.0, 6.0),
198                end_alpha=0,
199                center_xy=rand_in_rect((0.0, 0.0), SCREEN_WIDTH, SCREEN_HEIGHT)
200            )
201        )
202        self.emitters.append(stars)
203
204        x, y = rand_in_circle(center=(0.0, 0.0), radius=0.04)
205        change_vec2 = Vec2(x, y) + Vec2(0.1, 0)
206        change_tuple = change_vec2.x, change_vec2.y
207        self.cloud = Emitter(
208            center_xy=(50, 500),
209            change_xy=(0.15, 0),
210            emit_controller=EmitMaintainCount(60),
211            particle_factory=lambda emitter: AnimatedAlphaParticle(
212                filename_or_texture=random.choice(CLOUD_TEXTURES),
213                change_xy=change_tuple,
214                start_alpha=0,
215                duration1=random.uniform(5.0, 10.0),
216                mid_alpha=255,
217                duration2=random.uniform(5.0, 10.0),
218                end_alpha=0,
219                center_xy=rand_in_circle((0.0, 0.0), 50)
220            )
221        )
222        self.emitters.append(self.cloud)
223
224    def launch_firework(self, delta_time):
225        launchers = (
226            self.launch_random_firework,
227            self.launch_ringed_firework,
228            self.launch_sparkle_firework,
229        )
230        random.choice(launchers)(delta_time)
231        pyglet.clock.schedule_once(self.launch_firework, random.uniform(LAUNCH_INTERVAL_MIN, LAUNCH_INTERVAL_MAX))
232
233    def launch_random_firework(self, _delta_time):
234        """Simple firework that explodes in a random color"""
235        rocket = make_rocket(self.explode_firework)
236        self.emitters.append(rocket)
237
238    def launch_ringed_firework(self, _delta_time):
239        """"Firework that has a basic explosion and a ring of sparks of a different color"""
240        rocket = make_rocket(self.explode_ringed_firework)
241        self.emitters.append(rocket)
242
243    def launch_sparkle_firework(self, _delta_time):
244        """Firework which has sparks that sparkle"""
245        rocket = make_rocket(self.explode_sparkle_firework)
246        self.emitters.append(rocket)
247
248    def launch_spinner(self, _delta_time):
249        """Start the spinner that throws sparks"""
250        spinner1 = make_spinner()
251        spinner2 = make_spinner()
252        spinner2.angle = 180
253        self.emitters.append(spinner1)
254        self.emitters.append(spinner2)
255
256    def explode_firework(self, prev_emitter):
257        """Actions that happen when a firework shell explodes, resulting in a typical firework"""
258        self.emitters.append(make_puff(prev_emitter))
259        self.emitters.append(make_flash(prev_emitter))
260
261        spark_texture = random.choice(SPARK_TEXTURES)
262        sparks = Emitter(
263            center_xy=prev_emitter.get_pos(),
264            emit_controller=EmitBurst(random.randint(30, 40)),
265            particle_factory=lambda emitter: FadeParticle(
266                filename_or_texture=spark_texture,
267                change_xy=rand_in_circle(center=(0.0, 0.0), radius=9.0),
268                lifetime=random.uniform(0.5, 1.2),
269                mutation_callback=firework_spark_mutator
270            )
271        )
272        self.emitters.append(sparks)
273
274    def explode_ringed_firework(self, prev_emitter):
275        """Actions that happen when a firework shell explodes, resulting in a ringed firework"""
276        self.emitters.append(make_puff(prev_emitter))
277        self.emitters.append(make_flash(prev_emitter))
278
279        spark_texture, ring_texture = random.choice(SPARK_PAIRS)
280        sparks = Emitter(
281            center_xy=prev_emitter.get_pos(),
282            emit_controller=EmitBurst(25),
283            particle_factory=lambda emitter: FadeParticle(
284                filename_or_texture=spark_texture,
285                change_xy=rand_in_circle((0.0, 0.0), 8.0),
286                lifetime=random.uniform(0.55, 0.8),
287                mutation_callback=firework_spark_mutator
288            )
289        )
290        self.emitters.append(sparks)
291
292        ring = Emitter(
293            center_xy=prev_emitter.get_pos(),
294            emit_controller=EmitBurst(20),
295            particle_factory=lambda emitter: FadeParticle(
296                filename_or_texture=ring_texture,
297                change_xy=rand_on_circle(center=(0.0, 0.0), radius=5.0),
298                lifetime=random.uniform(1.0, 1.6),
299                mutation_callback=firework_spark_mutator
300            )
301        )
302        self.emitters.append(ring)
303
304    def explode_sparkle_firework(self, prev_emitter):
305        """Actions that happen when a firework shell explodes, resulting in a sparkling firework"""
306        self.emitters.append(make_puff(prev_emitter))
307        self.emitters.append(make_flash(prev_emitter))
308
309        spark_texture = random.choice(SPARK_TEXTURES)
310        sparks = Emitter(
311            center_xy=prev_emitter.get_pos(),
312            emit_controller=EmitBurst(random.randint(30, 40)),
313            particle_factory=lambda emitter: AnimatedAlphaParticle(
314                filename_or_texture=spark_texture,
315                change_xy=rand_in_circle(center=(0.0, 0.0), radius=9.0),
316                start_alpha=255,
317                duration1=random.uniform(0.6, 1.0),
318                mid_alpha=0,
319                duration2=random.uniform(0.1, 0.2),
320                end_alpha=255,
321                mutation_callback=firework_spark_mutator
322            )
323        )
324        self.emitters.append(sparks)
325
326    def on_update(self, delta_time):
327        # prevent list from being mutated (often by callbacks) while iterating over it
328        emitters_to_update = self.emitters.copy()
329        # update cloud
330        if self.cloud.center_x > SCREEN_WIDTH:
331            self.cloud.center_x = 0
332        # update
333        for e in emitters_to_update:
334            e.update()
335        # remove emitters that can be reaped
336        to_del = [e for e in emitters_to_update if e.can_reap()]
337        for e in to_del:
338            self.emitters.remove(e)
339
340    def on_draw(self):
341        self.clear()
342        for e in self.emitters:
343            e.draw()
344        arcade.draw_lrbt_rectangle_filled(0, SCREEN_WIDTH, 0, 25, arcade.color.DARK_GREEN)
345        mid = SCREEN_WIDTH / 2
346        arcade.draw_lrbt_rectangle_filled(mid - 2, mid + 2, 10, SPINNER_HEIGHT, arcade.color.DARK_BROWN)
347
348    def on_key_press(self, key, modifiers):
349        if key == arcade.key.ESCAPE:
350            arcade.close_window()
351
352
353def firework_spark_mutator(particle: FadeParticle):
354    """mutation_callback shared by all fireworks sparks"""
355    # gravity
356    particle.change_y += -0.03
357    # drag
358    particle.change_x *= 0.92
359    particle.change_y *= 0.92
360
361
362def rocket_smoke_mutator(particle: LifetimeParticle):
363    particle.scale = lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original)
364
365
366def main():
367    app = FireworksApp()
368    app.run()
369
370
371if __name__ == "__main__":
372    main()