"""
Copyright 2023 Ethan Christensen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import math
import random
from typing import List
from bruhcolor import bruhcolored as bc
from ..bruhutil.bruhffer import Buffer
from ..bruhutil.bruhtypes import (
FireworkColorType,
FireworkType,
two_tone_colors,
valid_firework_color_types,
valid_firework_types,
)
from .base_effect import BaseEffect
from .settings import FireworkSettings
[docs]
class Particle:
"""
Class representing a particle in a firework with optional trail effects.
"""
[docs]
def __init__(
self,
x: int,
y: int,
dx: float,
dy: float,
width: int,
height: int,
symbol: chr = "*",
life: int = None,
trail_length: int = 0,
):
"""
Initialize a particle with the given parameters.
Args:
x (int): The x position of the particle.
y (int): The y position of the particle.
dx (float): The horizontal velocity of the particle.
dy (float): The vertical velocity of the particle.
width (int): The width of the canvas or screen.
height (int): The height of the canvas or screen.
symbol (chr, optional): The character that will be displayed to the screen. Defaults to "*".
life (int, optional): How long this particle can live for (frames). Defaults to None.
trail_length (int, optional): Length of the particle's trail. 0 means no trail. Defaults to 0.
"""
self.x = x
self.y = y
self.dx = dx
self.dy = dy
self.width = width
self.height = height
self.life = life if life else random.randint(8, 15)
self.symbol = symbol
# Trail-related attributes
self.trail_length = trail_length
self.trail = [] # Stores previous positions
self.trail_symbols = [
"·",
"⋅",
"∙",
"°",
] # Different symbols for trail fade effect
# Initialize previous position (maintain backward compatibility)
self.previous_x = x
self.previous_y = y
[docs]
def update(self):
"""
Function to update a particle and its parameters each frame.
"""
# Store current position for trail if enabled
if self.trail_length > 0:
self.trail.append((self.x, self.y, self.symbol))
# Keep trail at specified length
if len(self.trail) > self.trail_length:
self.trail.pop(0)
# Update previous position (for backward compatibility)
self.previous_x = self.x
self.previous_y = self.y
# Update current position
self.x += self.dx
self.y += self.dy
self.dy += 0.05
self.life -= 1
# Flicker effect
if random.random() > 0.5:
self.symbol = "*" if self.symbol == "." else "."
[docs]
def get_display_points(self):
"""
Returns all points that should be displayed, including trails if enabled.
Returns:
list: List of tuples (x, y, symbol) for all points to display
"""
points = [(self.x, self.y, self.symbol)]
if self.trail_length > 0:
# Add trail points with fading symbols
trail_len = len(self.trail)
for i, (trail_x, trail_y, _) in enumerate(reversed(self.trail)):
# Adjust the fade effect based on trail position
fade_factor = min(
i * len(self.trail_symbols) // trail_len,
len(self.trail_symbols) - 1,
)
symbol_index = len(self.trail_symbols) - fade_factor - 1
points.append((trail_x, trail_y, self.trail_symbols[symbol_index]))
return points
[docs]
def is_alive(self):
"""
Whether or not the particle is still alive. A Particle is alive if it has life left and is within bounds.
"""
return (
self.life > 0
and 0 <= int(self.x) < self.width
and 0 <= int(self.y) < self.height
)
[docs]
class Firework:
"""
The Firework class. Responsible for creating and updating fireworks.
"""
[docs]
def __init__(
self,
firework_type: FireworkType,
height: int,
width: int,
firework_color_type: str = None,
color_enabled: bool = False,
allowed_firework_types: List[str] = None,
):
"""
Initialize a Firework object.
Args:
firework_type (FireworkType): The type of firework this object should be. (eg. 'random', 'ring', 'snowflake').
height (int): The height of the display area (generally the terminal window).
width (int): The width of the display area (generally the terminal window).
firework_color_type (str, optional): The type of color to be applied to the Firework. Defaults to None.
color_enabled (bool, optional): Whether of not the Firework should have color. Defaults to False.
allowed_firework_types (List[str], optional): List of allowed firework types in the instance firework_type is 'random'. Defaults to None.
"""
self.width = width
self.height = height
self.x = random.randint(0, self.width - 1)
self.y = height - 1
self.previous_x = self.x
self.previous_y = self.y
self.peak = random.randint(5, self.height // 2)
self.exploded = False
self.particles = []
self.allowed_fire_work_types = allowed_firework_types or valid_firework_types
self.explosion_type = (
firework_type
if (firework_type != "random" and firework_type in valid_firework_types)
else random.choice(self.allowed_fire_work_types)
)
self.clear_particles = []
self.caught_last_trail = False
self.speed = 3
self.firework_color_type = firework_color_type
self.colors = self.get_colors()
self.color_enabled = color_enabled
self.trajectory_type = random.choice(["straight", "arc", "zigzag"])
self.angle = random.uniform(-0.5, 0.5) # Angle for non-straight trajectories
self.arc_direction = random.choice([-1, 1]) # Direction of arc curve
self.zigzag_phase = 0 # For zigzag pattern
self.zigzag_amplitude = random.uniform(0.2, 0.5) # Width of zigzag
[docs]
def update(self):
"""
Function to update the firework's state.
If the firework hasn't exploded yet, then it's trail needs to be advanced.
If the firework has exploded, then we need to update the particles that make
up the firework.
"""
if not self.exploded:
# Store previous position
self.previous_x = self.x
self.previous_y = self.y
# Update position based on trajectory type
if self.trajectory_type == "straight":
self.move_straight()
elif self.trajectory_type == "arc":
self.move_arc()
else: # zigzag
self.move_zigzag()
# Check if reached peak
if self.y <= self.peak:
self.exploded = True
self.create_particles()
else:
# Update particles
for particle in self.particles:
particle.update()
# Remove dead particles
for p in self.particles:
if not p.is_alive():
self.clear_particles.append(p)
self.particles = [p for p in self.particles if p.is_alive()]
[docs]
def move_straight(self):
"""
Function to move the firework trail straight up and down.
"""
# Move upward with slight angle
self.y -= self.speed
self.x += math.sin(self.angle) * self.speed * 0.5
[docs]
def move_arc(self):
"""
Function to move the firework trail in an arcing trajectory.
"""
# Create arcing trajectory
progress = (self.height - self.y) / (self.height - self.peak)
arc_offset = math.sin(progress * math.pi) * 2.0
self.y -= self.speed
self.x += self.arc_direction * arc_offset * 0.2
[docs]
def move_zigzag(self):
"""
Function to move the firework trail in a zigzag pattern.
"""
# Create zigzag pattern
self.zigzag_phase += 0.2
self.y -= self.speed
self.x += math.sin(self.zigzag_phase) * self.zigzag_amplitude
[docs]
def create_particles(self):
"""
Function to create the particles for the firework after it has
reached it's peak. It is determined by the firework_type parameter.
"""
if self.explosion_type == "circular":
self.circular_explosion()
elif self.explosion_type == "ring":
self.ring_explosion()
elif self.explosion_type == "starburst":
self.starburst_explosion()
elif self.explosion_type == "cone":
self.cone_explosion()
elif self.explosion_type == "spiral":
self.spiral_explosion()
elif self.explosion_type == "wave":
self.wave_explosion()
elif self.explosion_type == "burst":
self.burst_explosion()
elif self.explosion_type == "cross":
self.cross_explosion()
elif self.explosion_type == "flower":
self.flower_explosion()
elif self.explosion_type == "doublering":
self.double_ring_explosion()
elif self.explosion_type == "heart":
self.heart_explosion()
elif self.explosion_type == "star":
self.star_explosion()
elif self.explosion_type == "fireball":
self.fireball_explosion()
elif self.explosion_type == "diamond":
self.diamond_explosion()
elif self.explosion_type == "shockwave":
self.burst_with_shockwave_explosion()
elif self.explosion_type == "snowflake":
self.snowflake_explosion()
elif self.explosion_type == "cluster":
self.cluster_explosion()
elif self.explosion_type == "comet":
self.comet_tail_explosion()
elif self.explosion_type == "willow":
self.willow_explosion()
elif self.explosion_type == "dna":
self.dna_explosion()
elif self.explosion_type == "infinity":
self.infinity_explosion()
elif self.explosion_type == "galaxy":
self.galaxy_explosion()
elif self.explosion_type == "phoenix":
self.phoenix_explosion()
elif self.explosion_type == "fountain":
self.fountain_explosion()
elif self.explosion_type == "butterfly":
self.butterfly_explosion()
elif self.explosion_type == "dragon":
self.dragon_explosion()
elif self.explosion_type == "tornado":
self.tornado_explosion()
elif self.explosion_type == "matrix":
self.matrix_explosion()
elif self.explosion_type == "portal":
self.portal_explosion()
elif self.explosion_type == "fractal":
self.fractal_tree_explosion()
elif self.explosion_type == "tessellation":
self.tessellation_explosion()
elif self.explosion_type == "quantum":
self.quantum_explosion()
elif self.explosion_type == "mandelbrot":
self.mandelbrot_explosion()
elif self.explosion_type == "hypercube":
self.hypercube_explosion()
elif self.explosion_type == "chaos":
self.chaos_theory_explosion()
elif self.explosion_type == "timewarp":
self.time_warp_explosion()
elif self.explosion_type == "interdimensional":
self.interdimensional_portal_explosion()
elif self.explosion_type == "blackhole":
self.black_hole_singularity()
elif self.explosion_type == "mtheory":
self.m_theory_explosion()
elif self.explosion_type == "realitywarp":
self.reality_warping_tessellation()
elif self.explosion_type == "noneuclidean":
self.non_euclidean_explosion()
elif self.explosion_type == "cosmicstring":
self.cosmic_string_explosion()
elif self.explosion_type == "fancytrailburst":
self.fancy_trail_burst_explosion()
[docs]
def circular_explosion(self):
"""
Creates a circular explosion effect.
"""
for _ in range(30):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(0.5, 1.5)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def ring_explosion(self):
"""
Creates a ring explosion effect.
"""
for angle in range(0, 360, 12): # Ring pattern with evenly spaced particles
rad = math.radians(angle)
dx = math.cos(rad)
dy = math.sin(rad)
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def starburst_explosion(self):
"""
Creates a starburst explosion effect.
"""
for angle in range(
0, 360, 45
): # Starburst with particles in specific directions
rad = math.radians(angle)
speed = random.uniform(1, 1.5)
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def cone_explosion(self):
"""
Creates a cone explosion effect.
"""
for _ in range(20):
angle = random.uniform(
-math.pi / 6, math.pi / 6
) # Narrow range for cone shape
speed = random.uniform(0.5, 1.5)
dx = math.cos(angle) * speed
dy = -abs(math.sin(angle) * speed) # Force particles upward
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def spiral_explosion(self):
"""
Creates a spiral explosion effect.
"""
for i in range(20):
angle = i * 0.3 # Gradually increasing angle for spiral effect
speed = 0.1 * i # Particles spread out as the spiral grows
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def wave_explosion(self):
"""
Creates a wave explosion effect.
"""
for i in range(30):
angle = i * 0.2 # Slightly increase angle for wave effect
speed = random.uniform(0.5, 1.0)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed * 0.5 # Particles move slower upward
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def burst_explosion(self):
"""
Creates a burst explosion effect.
"""
for _ in range(20):
angle = random.uniform(0, 2 * math.pi) # Random angles for burst
speed = random.uniform(0.5, 1.5)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Particles gradually fall downward after a burst
for particle in self.particles:
particle.dy += 0.5 # Increase downward velocity
[docs]
def cross_explosion(self):
"""
Creates a cross explosion effect.
"""
for angle in [0, 90, 180, 270]: # Particles in cross directions
rad = math.radians(angle)
speed = random.uniform(0.5, 1.5)
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def flower_explosion(self):
"""
Creates a flower explosion effect.
"""
for angle in range(0, 360, 30):
rad = math.radians(angle)
speed = random.uniform(0.5, 1.0)
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Add smaller "petal" particles around each main particle
for petal_angle in [-0.1, 0.1]:
dx_petal = math.cos(rad + petal_angle) * (speed * 0.7)
dy_petal = math.sin(rad + petal_angle) * (speed * 0.7)
self.particles.append(
Particle(
self.x, self.y, dx_petal, dy_petal, self.width, self.height
)
)
[docs]
def double_ring_explosion(self):
"""
Creates a double ring explosion effect.
"""
for radius_multiplier in [0.8, 1.2]: # Two rings at slightly different radii
for angle in range(0, 360, 15):
rad = math.radians(angle)
speed = 1.0 * radius_multiplier
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def heart_explosion(self):
"""
Creates a heart explosion effect.
"""
for t in range(0, 360, 10): # Parametric heart shape
rad = math.radians(t)
dx = 16 * math.sin(rad) ** 3 * 0.1
dy = (
-(
13 * math.cos(rad)
- 5 * math.cos(2 * rad)
- 2 * math.cos(3 * rad)
- math.cos(4 * rad)
)
* 0.05
)
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def star_explosion(self):
"""
Creates a star explosion effect.
"""
for i in range(5): # 5-point star
angle = i * 2 * math.pi / 5
dx = math.cos(angle) * 1.5
dy = math.sin(angle) * 1.5
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Add particles in opposite direction for a sharper effect
self.particles.append(
Particle(self.x, self.y, -dx, -dy, self.width, self.height)
)
[docs]
def fireball_explosion(self):
"""
Creates a fireball explosion effect.
"""
for _ in range(50): # Dense number of particles
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(0.2, 1.5)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def diamond_explosion(self):
"""
Creates a diamond explosion effect.
"""
for angle in [45, 135, 225, 315]: # Four main directions for diamond points
rad = math.radians(angle)
dx = math.cos(rad) * 1.5
dy = math.sin(rad) * 1.5
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Add smaller particles near each main point for a thicker diamond shape
for offset in [-0.1, 0.1]:
dx_offset = math.cos(rad + offset) * 1.2
dy_offset = math.sin(rad + offset) * 1.2
self.particles.append(
Particle(
self.x, self.y, dx_offset, dy_offset, self.width, self.height
)
)
[docs]
def burst_with_shockwave_explosion(self):
"""
Creates a burst with shockwave explosion effect.
"""
# Main burst particles
for angle in range(0, 360, 20):
rad = math.radians(angle)
speed = random.uniform(0.8, 1.2)
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Shockwave particles in a ring around the burst
for angle in range(0, 360, 5):
rad = math.radians(angle)
dx = math.cos(rad) * 1.5
dy = math.sin(rad) * 1.5
self.particles.append(
Particle(
self.x, self.y, dx * 0.5, dy * 0.5, self.width, self.height, life=5
)
) # Short lifespan for shockwave
[docs]
def snowflake_explosion(self):
"""
Creates a snowflake explosion effect.
"""
for angle in range(0, 360, 60): # Six main directions
rad = math.radians(angle)
speed = random.uniform(0.8, 1.0)
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Small branches off each main point for the snowflake effect
for branch_angle in [-15, 15]: # Offset angles for branches
rad_branch = rad + math.radians(branch_angle)
dx_branch = math.cos(rad_branch) * (speed * 0.6)
dy_branch = math.sin(rad_branch) * (speed * 0.6)
self.particles.append(
Particle(
self.x, self.y, dx_branch, dy_branch, self.width, self.height
)
)
[docs]
def cluster_explosion(self):
"""
Creates a cluster explosion effect with particles moving in different directions
"""
for angle in range(0, 360, 30):
rad = math.radians(angle)
speed = 1.2
dx = math.cos(rad) * speed
dy = math.sin(rad) * speed
main_particle = Particle(self.x, self.y, dx, dy, self.width, self.height)
self.particles.append(main_particle)
# Surround each main particle with smaller "cluster" particles
for _ in range(6):
offset_dx = dx + random.uniform(-0.2, 0.2)
offset_dy = dy + random.uniform(-0.2, 0.2)
self.particles.append(
Particle(
self.x + offset_dx,
self.y + offset_dy,
offset_dx * 0.5,
offset_dy * 0.5,
self.width,
self.height,
)
)
[docs]
def comet_tail_explosion(self):
"""
Creates a comet tail explosion effect with particles following a comet-like path
"""
# Main comet direction
comet_angle = random.choice(
[45, 135, 225, 315]
) # Random diagonal angle for comet
rad = math.radians(comet_angle)
speed = 1.5
main_dx = math.cos(rad) * speed
main_dy = math.sin(rad) * speed
for i in range(8): # Comet particles along the main direction
trail_dx = main_dx * (1 - i * 0.1)
trail_dy = main_dy * (1 - i * 0.1)
self.particles.append(
Particle(self.x, self.y, trail_dx, trail_dy, self.width, self.height)
)
# Small trailing particles
for i in range(1, 5):
trail_dx = main_dx * 0.3
trail_dy = main_dy * 0.3
self.particles.append(
Particle(
self.x - trail_dx * i,
self.y - trail_dy * i,
trail_dx * 0.5,
trail_dy * 0.5,
self.width,
self.height,
)
)
[docs]
def willow_explosion(self):
"""
Creates a willow explosion effect with multiple branches extending from the center
"""
num_arms = 10 # Number of branches in the willow effect
angle_offset = 70 # Constrain angle range to mostly horizontal
for i in range(num_arms):
# Each arm has an angle mostly to the sides (slightly up or down)
angle = (
random.uniform(-angle_offset, angle_offset)
if i < num_arms / 2
else random.uniform(180 - angle_offset, 180 + angle_offset)
)
rad = math.radians(angle)
initial_speed = random.uniform(0.5, 1.0)
# Calculate initial movement in a mostly horizontal direction
dx = math.cos(rad) * initial_speed
dy = (
math.sin(rad) * initial_speed * 0.3
) # Smaller upward component for the drooping effect
# Main particle at the start of each "arm"
main_particle = Particle(
self.x, self.y, dx, dy, self.width, self.height, life=25
)
self.particles.append(main_particle)
# Trailing particles along each arm, curving downwards like branches
for j in range(1, 6):
# Reduce horizontal speed gradually, add downward pull for the arc
arc_dx = dx * (
1 - j * 0.1
) # Slightly reduce horizontal speed over time
arc_dy = dy + j * 0.1 # Increase downward speed to mimic gravity
trail_particle = Particle(
self.x,
self.y,
arc_dx,
arc_dy,
self.width,
self.height,
life=25 - j * 8,
)
self.particles.append(trail_particle)
[docs]
def dna_explosion(self):
"""
Creates a double helix pattern resembling DNA structure
"""
num_points = 30
radius = 1.0
vertical_stretch = 0.5
for i in range(num_points):
t = (i / num_points) * 4 * math.pi # Two complete rotations
# First strand
dx1 = math.cos(t) * radius
dy1 = -vertical_stretch * t # Negative for upward movement
self.particles.append(
Particle(self.x, self.y, dx1, dy1, self.width, self.height)
)
# Second strand (offset by pi)
dx2 = math.cos(t + math.pi) * radius
dy2 = -vertical_stretch * t
self.particles.append(
Particle(self.x, self.y, dx2, dy2, self.width, self.height)
)
# "Bridges" between strands (occasional connectors)
if i % 4 == 0:
dx_bridge = (dx1 + dx2) / 2
dy_bridge = dy1
self.particles.append(
Particle(
self.x,
self.y,
dx_bridge,
dy_bridge,
self.width,
self.height,
symbol="-",
)
)
[docs]
def infinity_explosion(self):
"""
Creates an infinity symbol (∞) pattern
"""
num_points = 40
size = 1.2
for i in range(num_points):
t = (i / num_points) * 2 * math.pi
# Parametric equations for infinity symbol
dx = size * math.cos(t) / (1 + math.sin(t) ** 2)
dy = size * math.sin(t) * math.cos(t) / (1 + math.sin(t) ** 2)
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def galaxy_explosion(self):
"""
Creates a spiral galaxy pattern with arms and central bulge
"""
# Central bulge
for _ in range(20):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(0.2, 0.5)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height, symbol="·")
)
# Spiral arms
arms = 2
for arm in range(arms):
start_angle = (2 * math.pi * arm) / arms
for i in range(30):
radius = 0.1 + (i * 0.05)
angle = start_angle + (i * 0.3)
dx = math.cos(angle) * radius
dy = math.sin(angle) * radius
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def phoenix_explosion(self):
"""
Creates a rising phoenix pattern with wings and tail
"""
# Central rising column
for i in range(10):
dy = -1.0 - (i * 0.1) # Upward movement
dx = random.uniform(-0.2, 0.2)
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Wings
wing_span = 15
for i in range(wing_span):
# Left wing
angle_left = math.radians(150 - (i * 4)) # Sweep from 150° to 90°
dx_left = math.cos(angle_left) * (i * 0.1)
dy_left = math.sin(angle_left) * (i * 0.1)
self.particles.append(
Particle(self.x, self.y, dx_left, dy_left, self.width, self.height)
)
# Right wing
angle_right = math.radians(30 + (i * 4)) # Sweep from 30° to 90°
dx_right = math.cos(angle_right) * (i * 0.1)
dy_right = math.sin(angle_right) * (i * 0.1)
self.particles.append(
Particle(self.x, self.y, dx_right, dy_right, self.width, self.height)
)
# Tail feathers
for i in range(5):
angle = math.radians(270 + random.uniform(-30, 30))
speed = 0.5 + (i * 0.1)
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height, symbol="~")
)
[docs]
def fountain_explosion(self):
"""
Creates an upward-shooting fountain pattern with cascading particles
"""
num_streams = 5
particles_per_stream = 8
for stream in range(num_streams):
base_angle = -90 + random.uniform(-15, 15) # Mostly upward
base_speed = random.uniform(1.0, 1.5)
for i in range(particles_per_stream):
angle = math.radians(base_angle)
speed = base_speed - (
i * 0.1
) # Particles get slower toward end of stream
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
# Add some slight random variation to each particle
dx += random.uniform(-0.1, 0.1)
dy += random.uniform(-0.1, 0.1)
particle = Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol=":" if i < particles_per_stream // 2 else ".",
life=20 - i, # Particles at end of stream die sooner
)
self.particles.append(particle)
[docs]
def butterfly_explosion(self):
"""
Creates a butterfly pattern with wings that flutter
"""
# Wing shape parameters
wing_points = 20
flutter_speed = 0.2
for i in range(wing_points):
t = (i / wing_points) * 2 * math.pi
# Parametric equations for wing shape (modified heart curve)
base_dx = math.sin(t) * (math.exp(math.cos(t)) - 2 * math.cos(4 * t))
base_dy = math.cos(t) * (math.exp(math.cos(t)) - 2 * math.cos(4 * t))
# Create left and right wings with flutter effect
for side in [-1, 1]: # Left and right wings
dx = base_dx * side * 0.3
dy = base_dy * 0.3
# Add flutter movement
dx += math.sin(t * flutter_speed) * 0.1
particle = Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol="·" if i % 2 == 0 else "*",
life=random.randint(15, 25),
)
self.particles.append(particle)
# Add body particles
if i < 5:
self.particles.append(
Particle(
self.x, self.y, 0, i * 0.1, self.width, self.height, symbol="█"
)
)
[docs]
def dragon_explosion(self):
"""
Creates a dragon shape with body, wings, and fire breath
"""
# Body
body_length = 15
for i in range(body_length):
angle = math.radians(random.uniform(-10, 10)) # Slight wiggle
dx = math.cos(angle) * 0.3
dy = -0.5 + (i * 0.05) # Curves upward
self.particles.append(
Particle(
self.x, self.y, dx, dy, self.width, self.height, symbol="▲", life=20
)
)
# Wings
wing_span = 12
for i in range(wing_span):
# Left and right wings
for side in [-1, 1]:
angle = math.radians(45 * side)
dx = math.cos(angle) * (i * 0.15) * side
dy = math.sin(angle) * (i * 0.1) - 0.5
self.particles.append(
Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol="*",
life=15,
)
)
# Fire breath
breath_particles = 20
for i in range(breath_particles):
angle = math.radians(random.uniform(-30, 30))
speed = random.uniform(1.0, 2.0)
dx = math.cos(angle) * speed
dy = -math.sin(angle) * speed # Upward fire breath
symbol = random.choice(["^", "*", "●"])
self.particles.append(
Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol=symbol,
life=10,
)
)
[docs]
def tornado_explosion(self):
"""
Creates a spinning tornado effect that grows wider at the top
"""
height_layers = 15
base_radius = 0.2
for layer in range(height_layers):
radius = base_radius + (layer * 0.1) # Gets wider as it goes up
particles_in_layer = int(6 + layer * 1.5) # More particles in higher layers
for p in range(particles_in_layer):
angle = (p / particles_in_layer) * 2 * math.pi
# Add spin effect
angle += layer * 0.5
dx = math.cos(angle) * radius
dy = -1 + (layer * 0.1) # Upward movement, slowing at top
# Vary the symbols based on position
symbol = "●" if layer < 3 else ("*" if layer < 10 else "·")
particle = Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol=symbol,
life=20 - layer,
)
self.particles.append(particle)
[docs]
def matrix_explosion(self):
"""
Creates a Matrix-style digital rain effect
"""
num_streams = 15
chars_per_stream = 8
for stream in range(num_streams):
# Calculate stream position
angle = random.uniform(0, 2 * math.pi)
distance = random.uniform(0.5, 2.0)
base_dx = math.cos(angle) * distance
base_dy = math.sin(angle) * distance
for i in range(chars_per_stream):
# Delay each character in stream
dx = base_dx
dy = base_dy + (i * 0.15)
# Use Matrix-like symbols
symbol = random.choice(["0", "1", "█", "▀", "▄", "■", "░", "▒", "▓"])
# Particles later in stream die sooner
life = 20 - i
self.particles.append(
Particle(
self.x,
self.y,
dx,
dy,
self.width,
self.height,
symbol=symbol,
life=life,
)
)
[docs]
def portal_explosion(self):
"""
Creates two connected portals with particles flowing between them
"""
# Portal parameters
portal_radius = 1.2
num_particles = 50
# Create two portal rings
for portal in range(2):
angle_offset = math.pi * portal # Second portal on opposite side
distance = 2.0 # Distance between portals
# Portal position
portal_x = math.cos(angle_offset) * distance
portal_y = math.sin(angle_offset) * distance
# Create portal ring
for i in range(12):
angle = (i / 12) * 2 * math.pi
dx = portal_x + math.cos(angle) * portal_radius
dy = portal_y + math.sin(angle) * portal_radius
self.particles.append(
Particle(
self.x,
self.y,
dx * 0.2,
dy * 0.2,
self.width,
self.height,
symbol="O",
life=25,
)
)
# Particles flowing between portals
for _ in range(num_particles):
t = random.uniform(0, 1) # Position along path
# Curved path between portals
dx = math.cos(t * math.pi * 2) * portal_radius
dy = math.sin(t * math.pi * 2) * portal_radius
# Add some randomness to path
dx += random.uniform(-0.2, 0.2)
dy += random.uniform(-0.2, 0.2)
symbol = random.choice(["*", "·", "•", "+"])
self.particles.append(
Particle(
self.x,
self.y,
dx * 0.3,
dy * 0.3,
self.width,
self.height,
symbol=symbol,
life=10,
)
)
[docs]
def fractal_tree_explosion(self):
"""
Creates a fractal tree pattern that branches out recursively
"""
def add_branch(x, y, angle, depth, speed):
if depth <= 0:
return
dx = math.cos(angle) * speed
dy = math.sin(angle) * speed
self.particles.append(Particle(x, y, dx, dy, self.width, self.height))
# Branch angles and reduced speed for sub-branches
new_speed = speed * 0.7
add_branch(x, y, angle - 0.5, depth - 1, new_speed) # Left branch
add_branch(x, y, angle + 0.5, depth - 1, new_speed) # Right branch
# Create initial branches
for angle in range(0, 360, 45):
add_branch(self.x, self.y, math.radians(angle), 4, 1.2)
[docs]
def tessellation_explosion(self):
"""
Creates an Islamic geometric pattern-inspired explosion
"""
num_layers = 3
points_per_layer = 8
for layer in range(num_layers):
radius = 0.8 + layer * 0.4
# Create regular polygon vertices
for i in range(points_per_layer):
base_angle = (2 * math.pi * i / points_per_layer) + (
layer * math.pi / points_per_layer
)
# Main point
dx = math.cos(base_angle) * radius
dy = math.sin(base_angle) * radius
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Connect to adjacent points with intermediate particles
base_angle + (math.pi / points_per_layer)
steps = 3
for step in range(steps):
t = step / steps
dx = (
math.cos(base_angle + t * (math.pi / points_per_layer)) * radius
)
dy = (
math.sin(base_angle + t * (math.pi / points_per_layer)) * radius
)
self.particles.append(
Particle(
self.x, self.y, dx * 0.8, dy * 0.8, self.width, self.height
)
)
[docs]
def quantum_explosion(self):
"""
Creates a quantum probability cloud-like pattern with orbital shells
"""
shells = 4
electrons_per_shell = 8
for shell in range(shells):
radius = 0.5 + shell * 0.3
for electron in range(electrons_per_shell):
# Base orbital motion
angle = 2 * math.pi * electron / electrons_per_shell
# Add quantum uncertainty
for uncertainty in range(3):
uncertain_radius = radius + random.uniform(-0.1, 0.1)
uncertain_angle = angle + random.uniform(-0.2, 0.2)
dx = math.cos(uncertain_angle) * uncertain_radius
dy = math.sin(uncertain_angle) * uncertain_radius
# Add some orbital velocity
orbital_dx = -dy * 0.3
orbital_dy = dx * 0.3
self.particles.append(
Particle(
self.x,
self.y,
dx + orbital_dx,
dy + orbital_dy,
self.width,
self.height,
)
)
[docs]
def mandelbrot_explosion(self):
"""
Creates an explosion pattern inspired by the Mandelbrot set
"""
points = 40
max_iterations = 3
for i in range(points):
angle = 2 * math.pi * i / points
# Generate points along cardioid and main bulb shapes
for iteration in range(max_iterations):
# Cardioid
t = angle + iteration * math.pi / 6
r = 0.5 * (1 - math.cos(t))
x = r * math.cos(t)
y = r * math.sin(t)
# Transform to velocity
speed = 1.0 - (iteration * 0.2)
dx = x * speed
dy = y * speed
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
# Main bulb
r2 = 0.25 * math.sqrt(abs(math.cos(2 * t)))
x2 = r2 * math.cos(t)
y2 = r2 * math.sin(t)
self.particles.append(
Particle(
self.x, self.y, x2 * speed, y2 * speed, self.width, self.height
)
)
[docs]
def hypercube_explosion(self):
"""
Creates a 4D hypercube projection explosion pattern
"""
# Generate 4D hypercube vertices
def rotate4d(point, angle):
x, y, z, w = point
# Rotate in xw plane
xr = x * math.cos(angle) - w * math.sin(angle)
wr = x * math.sin(angle) + w * math.cos(angle)
return (xr, y, z, wr)
vertices = []
for x in [-1, 1]:
for y in [-1, 1]:
for z in [-1, 1]:
for w in [-1, 1]:
vertices.append((x * 0.5, y * 0.5, z * 0.5, w * 0.5))
# Project and create particles
for angle in range(0, 360, 30):
rad = math.radians(angle)
for vertex in vertices:
# Rotate in 4D
rotated = rotate4d(vertex, rad)
# Project to 2D
factor = 1 / (2 - rotated[3]) # Perspective projection
x = rotated[0] * factor
y = rotated[1] * factor
speed = random.uniform(0.8, 1.2)
self.particles.append(
Particle(
self.x, self.y, x * speed, y * speed, self.width, self.height
)
)
[docs]
def chaos_theory_explosion(self):
"""
A completely chaotic explosion that uses strange attractors and randomness
"""
num_particles = 50
for _ in range(num_particles):
# Create chaotic initial conditions
angle = random.uniform(0, math.tau) # tau = 2π
speed = random.uniform(0.2, 2.0)
# Add some Lorenz attractor-inspired chaos
x_offset = math.sin(angle * 3) * math.cos(angle * 2)
y_offset = math.cos(angle * 4) * math.sin(angle * 5)
dx = (math.cos(angle) + x_offset) * speed
dy = (math.sin(angle) + y_offset) * speed
# Add multiple particles with slightly varying trajectories
for i in range(3):
chaos_dx = dx + random.uniform(-0.3, 0.3) * (i + 1)
chaos_dy = dy + random.uniform(-0.3, 0.3) * (i + 1)
self.particles.append(
Particle(
self.x, self.y, chaos_dx, chaos_dy, self.width, self.height
)
)
[docs]
def time_warp_explosion(self):
"""
Creates particles that appear to move through time differently
"""
num_timelines = 20
for timeline in range(num_timelines):
# Create a base particle trajectory
angle = random.uniform(0, math.tau)
speed = random.uniform(0.5, 1.5)
# Time dilation factor
time_factor = random.uniform(0.1, 2.0)
# Create particles moving at different "time speeds"
dx = math.cos(angle) * speed * time_factor
dy = math.sin(angle) * speed * time_factor
# Add temporal echo particles
num_echoes = 5
for echo in range(num_echoes):
echo_factor = math.sin(timeline + echo / num_echoes * math.pi)
echo_dx = dx * echo_factor
echo_dy = dy * echo_factor
# Add some spacetime distortion
distortion = math.sin(timeline * 0.1) * 0.5
echo_dx += distortion * random.uniform(-1, 1)
echo_dy += distortion * random.uniform(-1, 1)
self.particles.append(
Particle(self.x, self.y, echo_dx, echo_dy, self.width, self.height)
)
[docs]
def interdimensional_portal_explosion(self):
"""
Creates a swirling portal effect that seems to connect different dimensions
"""
num_dimensions = 5 # Number of "dimensional layers"
particles_per_dimension = 20
for dimension in range(num_dimensions):
dimension_offset = dimension * math.pi / num_dimensions
for i in range(particles_per_dimension):
# Create spiral pattern for each dimension
angle = (i / particles_per_dimension * math.tau) + dimension_offset
radius = 0.1 + dimension * 0.3
# Add interdimensional drift
drift_x = math.sin(angle * 3) * 0.5
drift_y = math.cos(angle * 2) * 0.5
# Calculate base velocities
dx = math.cos(angle) * radius + drift_x
dy = math.sin(angle) * radius + drift_y
# Add some dimensional instability
instability = random.uniform(-0.2, 0.2)
dx += instability * math.sin(dimension_offset)
dy += instability * math.cos(dimension_offset)
# Create particles with dimensional effects
for _ in range(3):
# Add quantum tunneling effect
tunnel_dx = dx + random.gauss(0, 0.1) * (dimension + 1)
tunnel_dy = dy + random.gauss(0, 0.1) * (dimension + 1)
self.particles.append(
Particle(
self.x,
self.y,
tunnel_dx,
tunnel_dy,
self.width,
self.height,
)
)
[docs]
def black_hole_singularity(self):
"""
Creates a black hole effect that warps space around it and emits Hawking radiation
"""
num_particles = 60
# Create infalling particles
for i in range(num_particles):
angle = random.uniform(0, math.tau)
distance = random.uniform(0.1, 2.0)
# Calculate gravitational effects
gravitational_strength = 1 / (distance + 0.1) # Prevent division by zero
# Spiral motion towards center
dx = math.cos(angle) * distance * gravitational_strength
dy = math.sin(angle) * distance * gravitational_strength
# Add relativistic frame dragging effect
frame_drag = math.atan2(dy, dx) * 0.3
dx += math.cos(frame_drag)
dy += math.sin(frame_drag)
# Hawking radiation (particles that escape)
if random.random() < 0.2: # 20% chance of radiation
radiation_speed = random.uniform(1.5, 2.0)
self.particles.append(
Particle(
self.x,
self.y,
dx * radiation_speed,
dy * radiation_speed,
self.width,
self.height,
)
)
# Infalling particles
self.particles.append(
Particle(self.x, self.y, -dx * 0.5, -dy * 0.5, self.width, self.height)
)
[docs]
def m_theory_explosion(self):
"""
Creates patterns inspired by 11-dimensional M-theory with membrane interactions
"""
dimensions = 11 # M-theory's 11 dimensions
particles_per_dim = 8
for d in range(dimensions):
phase = d * math.tau / dimensions
# Create brane-like structures in higher dimensions
for i in range(particles_per_dim):
# Complex dimensional mapping
angle1 = i * math.tau / particles_per_dim + phase
angle2 = angle1 * math.pi / 2
# Project from higher dimensions
for j in range(3): # Create multiple projections
# Use hyperbolic functions for exotic spatial effects
dx = math.sinh(angle1) * math.cosh(angle2) * (0.5 + j * 0.2)
dy = math.cosh(angle1) * math.sinh(angle2) * (0.5 + j * 0.2)
# Add quantum fluctuations
dx += random.gauss(0, 0.1) * math.sin(d)
dy += random.gauss(0, 0.1) * math.cos(d)
self.particles.append(
Particle(self.x, self.y, dx, dy, self.width, self.height)
)
[docs]
def reality_warping_tessellation(self):
"""
Creates a pattern that seems to fold and unfold reality itself
"""
def create_tessellation_point(angle, radius, iteration):
# Create golden ratio-based spiral
golden_ratio = 1.618033988749895
spiral_angle = angle * golden_ratio
# Calculate base position with reality-warping effects
x = math.cos(spiral_angle) * radius * math.sin(iteration * 0.1)
y = math.sin(spiral_angle) * radius * math.cos(iteration * 0.1)
# Add reality distortion
distortion = math.sin(iteration * 0.3) * 0.5
x += distortion * math.cos(spiral_angle * 2)
y += distortion * math.sin(spiral_angle * 3)
return x, y
layers = 5
points_per_layer = 12
for layer in range(layers):
radius = 0.3 + layer * 0.3
for i in range(points_per_layer):
angle = i * math.tau / points_per_layer
# Create multiple folded reality versions
for fold in range(3):
x1, y1 = create_tessellation_point(angle, radius, layer + fold)
x2, y2 = create_tessellation_point(
angle + 0.1, radius, layer + fold
)
# Create particles that follow the folds
speed = random.uniform(0.5, 1.5)
self.particles.append(
Particle(
self.x,
self.y,
x1 * speed,
y1 * speed,
self.width,
self.height,
)
)
self.particles.append(
Particle(
self.x,
self.y,
x2 * speed,
y2 * speed,
self.width,
self.height,
)
)
[docs]
def non_euclidean_explosion(self):
"""
Creates patterns that follow non-Euclidean geometry rules
"""
def hyperbolic_transform(x, y, curvature):
# Apply hyperbolic transformation
r = math.sqrt(x * x + y * y)
if r == 0:
return x, y
# Poincaré disk model transformation
factor = 2.0 / (1.0 + curvature * r * r)
return x * factor, y * factor
base_particles = 40
for i in range(base_particles):
angle = random.uniform(0, math.tau)
radius = random.uniform(0.1, 1.0)
# Create base movement
x = math.cos(angle) * radius
y = math.sin(angle) * radius
# Apply various non-Euclidean transformations
for curvature in [0.5, 1.0, 1.5]: # Different space curvatures
# Transform coordinates
dx, dy = hyperbolic_transform(x, y, curvature)
# Add Möbius transformation
complex_z = complex(dx, dy)
mobius_z = (complex_z + 0.2) / (1 - 0.3 * complex_z)
# Convert back to real coordinates with scaling
final_dx = mobius_z.real * 0.5
final_dy = mobius_z.imag * 0.5
# Add some quantum uncertainty to the geometry
for _ in range(2):
uncertainty_dx = final_dx + random.gauss(0, 0.1)
uncertainty_dy = final_dy + random.gauss(0, 0.1)
self.particles.append(
Particle(
self.x,
self.y,
uncertainty_dx,
uncertainty_dy,
self.width,
self.height,
)
)
[docs]
def cosmic_string_explosion(self):
"""
Creates patterns based on theoretical cosmic strings and topological defects
"""
num_strings = 8
points_per_string = 15
for string in range(num_strings):
# Create a base cosmic string
string_angle = string * math.tau / num_strings
string_tension = random.uniform(0.5, 1.5)
for point in range(points_per_string):
# Calculate string oscillation
t = point / points_per_string
wave = math.sin(t * math.pi * 2 + string_angle)
# Add string vibration modes
for mode in range(3):
mode_angle = string_angle + wave * 0.3 * (mode + 1)
mode_radius = 0.3 + mode * 0.2
# Calculate base velocities with string tension
dx = math.cos(mode_angle) * mode_radius * string_tension
dy = math.sin(mode_angle) * mode_radius * string_tension
# Add quantum fluctuations along the string
fluctuation = random.gauss(0, 0.1)
dx += fluctuation * math.sin(mode_angle)
dy += fluctuation * math.cos(mode_angle)
# Create particles with varying energies
for energy in range(2):
energy_factor = 1.0 + energy * 0.5
self.particles.append(
Particle(
self.x,
self.y,
dx * energy_factor,
dy * energy_factor,
self.width,
self.height,
)
)
[docs]
def fancy_trail_burst_explosion(self):
"""
Creates a more complex explosion with varying trail lengths and spiral motion
"""
num_particles = random.randint(8, 20)
for i in range(num_particles):
# Calculate angle for even distribution
angle = (i / num_particles) * math.tau
# Create two layers of particles
for layer in range(2):
# Base velocity with spiral component
speed = random.uniform(0.6, 1.0) * (layer + 1)
spiral_factor = 0.2 # Controls how much spiral motion
dx = math.cos(angle) * speed + math.sin(angle) * spiral_factor
dy = math.sin(angle) * speed - math.cos(angle) * spiral_factor
# Vary trail length based on position in the explosion
trail_length = int(
random.randint(2, 5) * (layer + 1) * (1 + math.sin(angle))
)
# Choose different symbols for each layer
symbol = "★" if layer == 0 else "✦"
particle = Particle(
x=self.x,
y=self.y,
dx=dx,
dy=dy,
width=self.width,
height=self.height,
symbol=symbol,
trail_length=trail_length,
life=50, # Longer life to see the spiral motion
)
self.particles.append(particle)
[docs]
def is_active(self):
"""
Returns True if the portal is active (emitting particles), False otherwise.
Returns:
bool: Whether or not the firework is still alive
"""
# Firework is active if it has not exploded or if particles are still alive
return not self.exploded or len(self.particles) > 0
[docs]
def get_colors(self):
"""
Get the colors for the firework based on its color type.
Returns:
list[int]: List of colors that should be used to color the firework.
"""
if self.firework_color_type == "solid":
return [random.randint(0, 255)]
elif self.firework_color_type == "twotone":
return random.choice(two_tone_colors)
elif self.firework_color_type == "random":
return [
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
]
elif self.firework_color_type == "rainbow":
return [196, 208, 190, 46, 27, 92]
[docs]
def render(self, buffer: Buffer):
"""
Render the firework to a Buffer object.
Args:
buffer (Buffer): Buffer used to house the image effect.
"""
# Draw firework trail or particles in the buffer
if not self.exploded:
if 0 <= self.x < self.width and 0 <= self.y < self.height:
buffer.put_char(int(self.x), int(self.y), val="|")
buffer.put_char(int(self.previous_x), int(self.previous_y), val=" ")
else:
buffer.put_char(int(self.previous_x), int(self.previous_y), val=" ")
else:
if not self.caught_last_trail:
self.caught_last_trail = True
buffer.put_char(int(self.previous_x), int(self.previous_y), val=" ")
for particle in self.particles:
for x, y, symbol in particle.get_display_points():
buffer.put_char(int(x), int(y), symbol)
px, py = int(particle.x), int(particle.y)
if 0 <= px < self.width and 0 <= py < self.height:
if self.color_enabled:
color = random.choice(self.colors)
buffer.put_char(
px, py, val=bc(particle.symbol, color=color).colored
)
else:
buffer.put_char(px, py, val=particle.symbol)
buffer.put_char(
int(particle.previous_x), int(particle.previous_y), val=" "
)
for particle in self.clear_particles:
buffer.put_char(
int(particle.previous_x), int(particle.previous_y), val=" "
)
[docs]
class FireworkEffect(BaseEffect):
"""
A spectacular fireworks display effect that can create various types of firework patterns.
This class provides a highly customizable fireworks display with support for different
explosion patterns, colors, and special effects. It can render traditional circular bursts,
as well as exotic patterns like fractals, quantum effects, and interdimensional portals.
Example Usage::
from bruhanimate import Buffer
from bruhanimate.bruheffect import FireworkEffect
# Create a basic fireworks display
buffer = Buffer(width=80, height=24)
fireworks = FireworkEffect(buffer=buffer, background=" ")
# Configure the fireworks
fireworks.set_firework_type("circular")
fireworks.set_firework_rate(0.1)
fireworks.set_firework_color_enabled(True)
fireworks.set_firework_color_type("rainbow")
# Render frames in your animation loop
for frame in range(100):
fireworks.render_frame(frame)
buffer.display()
Advanced Example::
# Create a mixed fireworks display with specific types
fireworks.set_firework_type("random")
fireworks.set_allowed_firework_types([
"circular", "starburst", "butterfly",
"quantum", "galaxy"
])
Warning:
- High firework rates (>0.3) may impact performance on slower systems
- Some exotic firework types may be CPU intensive
- Color support requires a terminal that supports ANSI color codes
Note:
The firework patterns are designed to work best in terminals with aspect
ratios close to 1:2 (height:width). Some patterns may appear distorted
in terminals with very different aspect ratios.
See Also:
- :class:`BaseEffect`: The parent class for all effects
- :class:`Buffer`: The buffer class used for rendering
"""
[docs]
def __init__(
self, buffer: Buffer, background: str, settings: FireworkSettings = None
):
"""
Initializes the FireworkEffect.
Args:
buffer (Buffer): Effect buffer to push updates to.
background (str): Character or string to use as the background.
settings (FireworkSettings, optional): Configuration for the firework effect. Defaults to None.
"""
super(FireworkEffect, self).__init__(buffer, background)
s = settings or FireworkSettings()
self.firework_type: FireworkType = (
s.firework_type
if s.firework_type in valid_firework_types or s.firework_type == "random"
else "circular"
)
self.firework_color_type: FireworkColorType = (
s.color_type if s.color_type in valid_firework_color_types else "solid"
)
self.color_enabled: bool = s.color_enabled
self.fireworks: list[Firework] = []
self.firework_rate: float = s.rate
self.allowed_firework_types = valid_firework_types
self.second_effect = None
[docs]
def set_firework_type(self, firework_type: FireworkType):
"""
Set the type of firework explosions to be created.
Args:
firework_type (FireworkType): The type of firework to create. Can be one of:
- "circular": Traditional circular burst
- "starburst": Radiating star pattern
- "butterfly": Elegant butterfly pattern
- "quantum": Quantum probability cloud
... and many more
Example::
fireworks.set_firework_type("butterfly")
# Or use random for variety
fireworks.set_firework_type("random")
Warning:
Some exotic firework types (like "quantum", "hypercube", etc.) may be
more CPU intensive than traditional patterns.
Note:
When using "random" type, the actual patterns used will be limited to
those specified in allowed_firework_types.
"""
if firework_type in valid_firework_types or firework_type == "random":
self.firework_type = firework_type
[docs]
def set_firework_color_enabled(self, color_enabled: bool):
"""
Set how frequently new fireworks should be launched.
Args:
firework_rate (float): Probability (0.0 to 1.0) of launching a new firework
each frame. Higher values mean more frequent launches.
Example::
# Sparse fireworks
fireworks.set_firework_rate(0.05) # 5% chance per frame
# Dense show
fireworks.set_firework_rate(0.3) # 30% chance per frame
Warning:
- Values >0.3 may cause performance issues on slower systems
- Values >0.5 may create very dense/cluttered displays
- Values ≤0.0 will prevent any fireworks from launching
Note:
The actual visual density will depend on your frame rate. Adjust this
value based on how frequently you call render_frame().
"""
self.color_enabled = color_enabled
[docs]
def set_firework_color_type(self, firework_color_type: FireworkColorType):
"""
Enable or disable colored fireworks.
Args:
color_enabled (bool): Whether fireworks should be rendered in color.
Example::
# Enable colorful display
fireworks.set_firework_color_enabled(True)
fireworks.set_firework_color_type("rainbow")
Warning:
- Colors require a terminal that supports ANSI color codes
- Some terminals may display colors differently
- Windows command prompt has limited color support
Note:
Even when enabled, colors will only be visible if a color type
is set via set_firework_color_type().
"""
if firework_color_type in valid_firework_color_types:
self.firework_color_type = firework_color_type
[docs]
def set_firework_rate(self, firework_rate: float):
"""
Function to set the rate at which fireworks should be launched
"""
if firework_rate > 0.0 and firework_rate <= 1.0:
self.firework_rate = firework_rate
[docs]
def set_allowed_firework_types(self, allowed_firework_types):
"""
Specify which firework types can be used when type is set to "random".
Args:
allowed_firework_types (List[str]): List of firework type names to allow.
Must be valid types from valid_firework_types.
Example::
# Create a show with only geometric patterns
fireworks.set_allowed_firework_types([
"circular", "ring", "diamond", "star"
])
# Create a show with exotic effects
fireworks.set_allowed_firework_types([
"quantum", "hypercube", "galaxy",
"interdimensional", "cosmic_string"
])
Warning:
- An empty list or list with no valid types will revert to all types
- Invalid type names will be silently ignored
- Some combinations of types may create visually jarring transitions
Note:
This setting only affects fireworks when type is set to "random".
For consistent displays, choose types with similar visual characteristics.
"""
allowed_firework_types = [
ft for ft in allowed_firework_types if ft in valid_firework_types
]
if len(allowed_firework_types) > 0:
self.allowed_firework_types = allowed_firework_types
[docs]
def set_second_effect(self, second_effect: any):
if isinstance(second_effect, BaseEffect):
self.second_effect = second_effect
[docs]
def render_frame(self, frame_number):
"""
Renders the background to the screen
"""
if random.random() < self.firework_rate:
self.fireworks.append(
Firework(
firework_type=self.firework_type,
height=self.buffer.height(),
width=self.buffer.width(),
firework_color_type=self.firework_color_type,
color_enabled=self.color_enabled,
allowed_firework_types=self.allowed_firework_types,
)
)
if self.second_effect is not None:
try:
self.second_effect.render_frame(frame_number=frame_number)
self.buffer.sync_with(self.second_effect.buffer)
except Exception:
pass
for firework in self.fireworks:
firework.update()
firework.render(self.buffer)
self.fireworks = [
firework for firework in self.fireworks if firework.is_active()
]