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