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