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