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