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