粒子系统-烟花#

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