#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_wrap)]
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{self, Event, KeyCode, KeyModifiers},
execute,
style::Print,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
};
use dotmax::animation::FrameTimer;
use dotmax::color::schemes::{get_scheme, rainbow};
use dotmax::{BrailleGrid, Color, ColorScheme, TerminalRenderer};
use std::f64::consts::PI;
use std::io::{stdout, Write};
use std::time::Duration;
const WIDTH: usize = 80;
const HEIGHT: usize = 24;
const GRAVITY: f64 = 0.15; const DRAG: f64 = 0.98; const SPAWN_INTERVAL: u64 = 60; const PARTICLES_PER_EXPLOSION: usize = 30;
const TARGET_FPS: u32 = 60;
struct Particle {
x: f64,
y: f64,
vx: f64,
vy: f64,
lifetime: u32, max_lifetime: u32,
color: Color,
}
impl Particle {
fn update(&mut self) {
self.vy += GRAVITY;
self.vx *= DRAG;
self.vy *= DRAG;
self.x += self.vx;
self.y += self.vy;
if self.lifetime > 0 {
self.lifetime -= 1;
}
}
const fn is_alive(&self) -> bool {
self.lifetime > 0
}
fn faded_color(&self) -> Color {
let life_ratio = f64::from(self.lifetime) / f64::from(self.max_lifetime);
let fade = life_ratio.clamp(0.0, 1.0);
Color::rgb(
(f64::from(self.color.r) * fade) as u8,
(f64::from(self.color.g) * fade) as u8,
(f64::from(self.color.b) * fade) as u8,
)
}
fn is_visible(&self, dot_width: usize, dot_height: usize) -> bool {
self.x >= 0.0
&& self.x < dot_width as f64
&& self.y >= 0.0
&& self.y < dot_height as f64
}
}
struct SimpleRng {
state: u64,
}
impl SimpleRng {
const fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn range(&mut self, min: f64, max: f64) -> f64 {
(max - min).mul_add(self.next_f64(), min)
}
}
fn create_explosion(
x: f64,
y: f64,
rng: &mut SimpleRng,
scheme: &ColorScheme,
) -> Vec<Particle> {
let mut particles = Vec::with_capacity(PARTICLES_PER_EXPLOSION);
for _ in 0..PARTICLES_PER_EXPLOSION {
let angle = rng.range(0.0, 2.0 * PI);
let speed = rng.range(1.5, 4.0);
let lifetime = rng.range(30.0, 80.0) as u32;
let color_intensity = (angle / (2.0 * PI)) as f32;
let color = scheme.sample(color_intensity);
particles.push(Particle {
x,
y,
vx: angle.cos() * speed,
vy: angle.sin() * speed,
lifetime,
max_lifetime: lifetime,
color,
});
}
particles
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, Clear(ClearType::All), Hide, MoveTo(0, 0))?;
let mut renderer = TerminalRenderer::new()?;
let mut timer = FrameTimer::new(TARGET_FPS);
let mut particles: Vec<Particle> = Vec::new();
let mut rng = SimpleRng::new(42);
let schemes = [
rainbow(),
get_scheme("heat_map").unwrap(),
get_scheme("cyan_magenta").unwrap(),
get_scheme("blue_purple").unwrap(),
];
let mut current_scheme = 0;
let mut frame: u64 = 0;
let dot_width = WIDTH * 2;
let dot_height = HEIGHT * 4;
loop {
if event::poll(Duration::from_millis(0))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q')
|| key.code == KeyCode::Esc
|| (key.code == KeyCode::Char('c')
&& key.modifiers.contains(KeyModifiers::CONTROL))
{
break;
}
}
}
if frame % SPAWN_INTERVAL == 0 {
let spawn_x = rng.range(20.0, (dot_width - 20) as f64);
let spawn_y = rng.range(10.0, (dot_height / 2) as f64);
let mut new_particles = create_explosion(
spawn_x,
spawn_y,
&mut rng,
&schemes[current_scheme],
);
particles.append(&mut new_particles);
current_scheme = (current_scheme + 1) % schemes.len();
}
for particle in &mut particles {
particle.update();
}
particles.retain(|p| p.is_alive() && p.is_visible(dot_width, dot_height));
let mut grid = BrailleGrid::new(WIDTH, HEIGHT)?;
for particle in &particles {
let x = particle.x as usize;
let y = particle.y as usize;
if x < dot_width && y < dot_height {
let color = particle.faded_color();
grid.set_dot(x, y)?;
let cell_x = x / 2;
let cell_y = y / 4;
grid.set_cell_color(cell_x, cell_y, color)?;
}
}
renderer.render(&grid)?;
execute!(
stdout,
MoveTo(0, HEIGHT as u16 + 1),
Print(format!(
"Fireworks | Particles: {:4} | Frame: {:5} | FPS: {:5.1} | [q]uit ",
particles.len(),
frame,
timer.actual_fps()
))
)?;
stdout.flush()?;
frame += 1;
timer.wait_for_next_frame();
}
execute!(stdout, Show, Clear(ClearType::All), MoveTo(0, 0))?;
renderer.cleanup()?;
disable_raw_mode()?;
println!("Fireworks Demo Complete!");
println!("Rendered {frame} frames of particle effects.");
println!("\nFeatures demonstrated:");
println!("- Particle system (position, velocity, lifetime)");
println!("- Gravity and drag physics");
println!("- Color fading over lifetime");
println!("- Multiple color schemes cycling");
println!("- Random burst patterns");
Ok(())
}