365 lines
14 KiB
Python
365 lines
14 KiB
Python
"""
|
|
Particle Fireworks
|
|
|
|
Use a fireworks display to demonstrate "real-world" uses of Emitters and Particles
|
|
|
|
If Python and Arcade are installed, this example can be run from the command line with:
|
|
python -m arcade.examples.sprite_list_particle_fireworks
|
|
"""
|
|
import arcade
|
|
from arcade import Point, Vector
|
|
from arcade.utils import _Vec2 # bring in "private" class
|
|
from arcade.examples.frametime_plotter import FrametimePlotter
|
|
import os
|
|
import random
|
|
import pyglet
|
|
|
|
SCREEN_WIDTH = 800
|
|
SCREEN_HEIGHT = 600
|
|
SCREEN_TITLE = "Particle based fireworks"
|
|
LAUNCH_INTERVAL_MIN = 1.5
|
|
LAUNCH_INTERVAL_MAX = 2.5
|
|
TEXTURE = "images/pool_cue_ball.png"
|
|
RAINBOW_COLORS = (
|
|
arcade.color.ELECTRIC_CRIMSON,
|
|
arcade.color.FLUORESCENT_ORANGE,
|
|
arcade.color.ELECTRIC_YELLOW,
|
|
arcade.color.ELECTRIC_GREEN,
|
|
arcade.color.ELECTRIC_CYAN,
|
|
arcade.color.MEDIUM_ELECTRIC_BLUE,
|
|
arcade.color.ELECTRIC_INDIGO,
|
|
arcade.color.ELECTRIC_PURPLE,
|
|
)
|
|
SPARK_TEXTURES = [arcade.make_circle_texture(15, clr) for clr in RAINBOW_COLORS]
|
|
SPARK_PAIRS = [
|
|
[SPARK_TEXTURES[0], SPARK_TEXTURES[3]],
|
|
[SPARK_TEXTURES[1], SPARK_TEXTURES[5]],
|
|
[SPARK_TEXTURES[7], SPARK_TEXTURES[2]],
|
|
]
|
|
ROCKET_SMOKE_TEXTURE = arcade.make_soft_circle_texture(15, arcade.color.GRAY)
|
|
PUFF_TEXTURE = arcade.make_soft_circle_texture(80, (40, 40, 40))
|
|
FLASH_TEXTURE = arcade.make_soft_circle_texture(70, (128, 128, 90))
|
|
CLOUD_TEXTURES = [
|
|
arcade.make_soft_circle_texture(50, arcade.color.WHITE),
|
|
arcade.make_soft_circle_texture(50, arcade.color.LIGHT_GRAY),
|
|
arcade.make_soft_circle_texture(50, arcade.color.LIGHT_BLUE),
|
|
]
|
|
STAR_TEXTURES = [
|
|
arcade.make_soft_circle_texture(6, arcade.color.WHITE),
|
|
arcade.make_soft_circle_texture(6, arcade.color.PASTEL_YELLOW),
|
|
]
|
|
SPINNER_HEIGHT = 75
|
|
|
|
|
|
def make_spinner():
|
|
spinner = arcade.Emitter(
|
|
center_xy=(SCREEN_WIDTH / 2, SPINNER_HEIGHT - 5),
|
|
emit_controller=arcade.EmitterIntervalWithTime(0.025, 2.0),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=random.choice(STAR_TEXTURES),
|
|
change_xy=(0, 6.0),
|
|
lifetime=0.2
|
|
)
|
|
)
|
|
spinner.change_angle = 16.28
|
|
return spinner
|
|
|
|
|
|
def make_rocket(emit_done_cb):
|
|
"""Emitter that displays the smoke trail as the firework shell climbs into the sky"""
|
|
rocket = RocketEmitter(
|
|
center_xy=(random.uniform(100, SCREEN_WIDTH - 100), 25),
|
|
emit_controller=arcade.EmitterIntervalWithTime(0.04, 2.0),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=ROCKET_SMOKE_TEXTURE,
|
|
change_xy=arcade.rand_in_circle((0.0, 0.0), 0.08),
|
|
scale=0.5,
|
|
lifetime=random.uniform(1.0, 1.5),
|
|
start_alpha=100,
|
|
end_alpha=0,
|
|
mutation_callback=rocket_smoke_mutator
|
|
),
|
|
emit_done_cb=emit_done_cb
|
|
)
|
|
rocket.change_x = random.uniform(-1.0, 1.0)
|
|
rocket.change_y = random.uniform(5.0, 7.25)
|
|
return rocket
|
|
|
|
|
|
def make_flash(prev_emitter):
|
|
"""Return emitter that displays the brief flash when a firework shell explodes"""
|
|
return arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(3),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=FLASH_TEXTURE,
|
|
change_xy=arcade.rand_in_circle((0.0, 0.0), 3.5),
|
|
lifetime=0.15
|
|
)
|
|
)
|
|
|
|
|
|
def make_puff(prev_emitter):
|
|
"""Return emitter that generates the subtle smoke cloud left after a firework shell explodes"""
|
|
return arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(4),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=PUFF_TEXTURE,
|
|
change_xy=(_Vec2(arcade.rand_in_circle((0.0, 0.0), 0.4)) + _Vec2(0.3, 0.0)).as_tuple(),
|
|
lifetime=4.0
|
|
)
|
|
)
|
|
|
|
|
|
class AnimatedAlphaParticle(arcade.LifetimeParticle):
|
|
"""A custom particle that animates between three different alpha levels"""
|
|
|
|
def __init__(
|
|
self,
|
|
filename_or_texture: arcade.FilenameOrTexture,
|
|
change_xy: Vector,
|
|
start_alpha: int = 0,
|
|
duration1: float = 1.0,
|
|
mid_alpha: int = 255,
|
|
duration2: float = 1.0,
|
|
end_alpha: int = 0,
|
|
center_xy: Point = (0.0, 0.0),
|
|
angle: float = 0,
|
|
change_angle: float = 0,
|
|
scale: float = 1.0,
|
|
mutation_callback=None,
|
|
):
|
|
super().__init__(filename_or_texture, change_xy, duration1 + duration2, center_xy, angle, change_angle, scale,
|
|
start_alpha, mutation_callback)
|
|
self.start_alpha = start_alpha
|
|
self.in_duration = duration1
|
|
self.mid_alpha = mid_alpha
|
|
self.out_duration = duration2
|
|
self.end_alpha = end_alpha
|
|
|
|
def update(self):
|
|
super().update()
|
|
if self.lifetime_elapsed <= self.in_duration:
|
|
u = self.lifetime_elapsed / self.in_duration
|
|
self.alpha = arcade.lerp(self.start_alpha, self.mid_alpha, u)
|
|
else:
|
|
u = (self.lifetime_elapsed - self.in_duration) / self.out_duration
|
|
self.alpha = arcade.lerp(self.mid_alpha, self.end_alpha, u)
|
|
|
|
|
|
class RocketEmitter(arcade.Emitter):
|
|
"""Custom emitter class to add gravity to the emitter to represent gravity on the firework shell"""
|
|
|
|
def update(self):
|
|
super().update()
|
|
# gravity
|
|
self.change_y += -0.05
|
|
|
|
|
|
class FireworksApp(arcade.Window):
|
|
def __init__(self):
|
|
super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
|
|
|
|
# Set the working directory (where we expect to find files) to the same
|
|
# directory this .py file is in. You can leave this out of your own
|
|
# code, but it is needed to easily run the examples using "python -m"
|
|
# as mentioned at the top of this program.
|
|
file_path = os.path.dirname(os.path.abspath(__file__))
|
|
os.chdir(file_path)
|
|
|
|
arcade.set_background_color(arcade.color.BLACK)
|
|
self.emitters = []
|
|
self.frametime_plotter = FrametimePlotter()
|
|
|
|
self.launch_firework(0)
|
|
arcade.schedule(self.launch_spinner, 4.0)
|
|
|
|
stars = arcade.Emitter(
|
|
center_xy=(0.0, 0.0),
|
|
emit_controller=arcade.EmitMaintainCount(20),
|
|
particle_factory=lambda emitter: AnimatedAlphaParticle(
|
|
filename_or_texture=random.choice(STAR_TEXTURES),
|
|
change_xy=(0.0, 0.0),
|
|
start_alpha=0,
|
|
duration1=random.uniform(2.0, 6.0),
|
|
mid_alpha=128,
|
|
duration2=random.uniform(2.0, 6.0),
|
|
end_alpha=0,
|
|
center_xy=arcade.rand_in_rect((0.0, 0.0), SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
)
|
|
)
|
|
self.emitters.append(stars)
|
|
|
|
self.cloud = arcade.Emitter(
|
|
center_xy=(50, 500),
|
|
change_xy=(0.15, 0),
|
|
emit_controller=arcade.EmitMaintainCount(60),
|
|
particle_factory=lambda emitter: AnimatedAlphaParticle(
|
|
filename_or_texture=random.choice(CLOUD_TEXTURES),
|
|
change_xy=(_Vec2(arcade.rand_in_circle((0.0, 0.0), 0.04)) + _Vec2(0.1, 0)).as_tuple(),
|
|
start_alpha=0,
|
|
duration1=random.uniform(5.0, 10.0),
|
|
mid_alpha=255,
|
|
duration2=random.uniform(5.0, 10.0),
|
|
end_alpha=0,
|
|
center_xy=arcade.rand_in_circle((0.0, 0.0), 50)
|
|
)
|
|
)
|
|
self.emitters.append(self.cloud)
|
|
|
|
def launch_firework(self, delta_time):
|
|
self.frametime_plotter.add_event("launch")
|
|
launchers = (
|
|
self.launch_random_firework,
|
|
self.launch_ringed_firework,
|
|
self.launch_sparkle_firework,
|
|
)
|
|
random.choice(launchers)(delta_time)
|
|
pyglet.clock.schedule_once(self.launch_firework, random.uniform(LAUNCH_INTERVAL_MIN, LAUNCH_INTERVAL_MAX))
|
|
|
|
def launch_random_firework(self, delta_time):
|
|
"""Simple firework that explodes in a random color"""
|
|
rocket = make_rocket(self.explode_firework)
|
|
self.emitters.append(rocket)
|
|
|
|
def launch_ringed_firework(self, delta_time):
|
|
""""Firework that has a basic explosion and a ring of sparks of a different color"""
|
|
rocket = make_rocket(self.explode_ringed_firework)
|
|
self.emitters.append(rocket)
|
|
|
|
def launch_sparkle_firework(self, delta_time):
|
|
"""Firework which has sparks that sparkle"""
|
|
rocket = make_rocket(self.explode_sparkle_firework)
|
|
self.emitters.append(rocket)
|
|
|
|
def launch_spinner(self, delta_time):
|
|
"""Start the spinner that throws sparks"""
|
|
spinner1 = make_spinner()
|
|
spinner2 = make_spinner()
|
|
spinner2.angle = 180
|
|
self.emitters.append(spinner1)
|
|
self.emitters.append(spinner2)
|
|
|
|
def explode_firework(self, prev_emitter):
|
|
"""Actions that happen when a firework shell explodes, resulting in a typical firework"""
|
|
self.emitters.append(make_puff(prev_emitter))
|
|
self.emitters.append(make_flash(prev_emitter))
|
|
|
|
spark_texture = random.choice(SPARK_TEXTURES)
|
|
sparks = arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(random.randint(30, 40)),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=spark_texture,
|
|
change_xy=arcade.rand_in_circle((0.0, 0.0), 9.0),
|
|
lifetime=random.uniform(0.5, 1.2),
|
|
mutation_callback=firework_spark_mutator
|
|
)
|
|
)
|
|
self.emitters.append(sparks)
|
|
|
|
def explode_ringed_firework(self, prev_emitter):
|
|
"""Actions that happen when a firework shell explodes, resulting in a ringed firework"""
|
|
self.emitters.append(make_puff(prev_emitter))
|
|
self.emitters.append(make_flash(prev_emitter))
|
|
|
|
spark_texture, ring_texture = random.choice(SPARK_PAIRS)
|
|
sparks = arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(25),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=spark_texture,
|
|
change_xy=arcade.rand_in_circle((0.0, 0.0), 8.0),
|
|
lifetime=random.uniform(0.55, 0.8),
|
|
mutation_callback=firework_spark_mutator
|
|
)
|
|
)
|
|
self.emitters.append(sparks)
|
|
|
|
ring = arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(20),
|
|
particle_factory=lambda emitter: arcade.FadeParticle(
|
|
filename_or_texture=ring_texture,
|
|
change_xy=arcade.rand_on_circle((0.0, 0.0), 5.0) + arcade.rand_in_circle((0.0, 0.0), 0.25),
|
|
lifetime=random.uniform(1.0, 1.6),
|
|
mutation_callback=firework_spark_mutator
|
|
)
|
|
)
|
|
self.emitters.append(ring)
|
|
|
|
def explode_sparkle_firework(self, prev_emitter):
|
|
"""Actions that happen when a firework shell explodes, resulting in a sparkling firework"""
|
|
self.emitters.append(make_puff(prev_emitter))
|
|
self.emitters.append(make_flash(prev_emitter))
|
|
|
|
spark_texture = random.choice(SPARK_TEXTURES)
|
|
sparks = arcade.Emitter(
|
|
center_xy=prev_emitter.get_pos(),
|
|
emit_controller=arcade.EmitBurst(random.randint(30, 40)),
|
|
particle_factory=lambda emitter: AnimatedAlphaParticle(
|
|
filename_or_texture=spark_texture,
|
|
change_xy=arcade.rand_in_circle((0.0, 0.0), 9.0),
|
|
start_alpha=255,
|
|
duration1=random.uniform(0.6, 1.0),
|
|
mid_alpha=0,
|
|
duration2=random.uniform(0.1, 0.2),
|
|
end_alpha=255,
|
|
mutation_callback=firework_spark_mutator
|
|
)
|
|
)
|
|
self.emitters.append(sparks)
|
|
|
|
def update(self, delta_time):
|
|
# prevent list from being mutated (often by callbacks) while iterating over it
|
|
emitters_to_update = self.emitters.copy()
|
|
# update cloud
|
|
if self.cloud.center_x > SCREEN_WIDTH:
|
|
self.cloud.center_x = 0
|
|
# update
|
|
for e in emitters_to_update:
|
|
e.update()
|
|
# remove emitters that can be reaped
|
|
to_del = [e for e in emitters_to_update if e.can_reap()]
|
|
for e in to_del:
|
|
self.emitters.remove(e)
|
|
self.frametime_plotter.end_frame(delta_time)
|
|
|
|
def on_draw(self):
|
|
arcade.start_render()
|
|
for e in self.emitters:
|
|
e.draw()
|
|
arcade.draw_lrtb_rectangle_filled(0, SCREEN_WIDTH, 25, 0, arcade.color.DARK_GREEN)
|
|
mid = SCREEN_WIDTH / 2
|
|
arcade.draw_lrtb_rectangle_filled(mid - 2, mid + 2, SPINNER_HEIGHT, 10, arcade.color.DARK_BROWN)
|
|
|
|
def on_key_press(self, key, modifiers):
|
|
if key == arcade.key.ESCAPE:
|
|
arcade.close_window()
|
|
|
|
|
|
def firework_spark_mutator(particle: arcade.FadeParticle):
|
|
"""mutation_callback shared by all fireworks sparks"""
|
|
# gravity
|
|
particle.change_y += -0.03
|
|
# drag
|
|
particle.change_x *= 0.92
|
|
particle.change_y *= 0.92
|
|
|
|
|
|
def rocket_smoke_mutator(particle: arcade.LifetimeParticle):
|
|
particle.scale = arcade.lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original)
|
|
# A Sprite's scale doesn't affect generated textures
|
|
# (ex: make_soft_circle_texture) or scale being animated over time.
|
|
# The fix below is copied from Sprite.update_animation().
|
|
# Bug may have been recorded here: https://github.com/pvcraven/arcade/issues/331
|
|
particle.width = particle._texture.width * particle.scale
|
|
particle.height = particle._texture.height * particle.scale
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = FireworksApp()
|
|
arcade.run()
|
|
app.frametime_plotter.show()
|