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