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()