confetty-tui 0.1.0

Confetti and fireworks in your terminal
Documentation
use ratatui::style::Color;

#[derive(Debug, Clone)]
pub struct Point {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

#[derive(Debug, Clone)]
pub struct Vector {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

#[derive(Debug, Clone)]
pub struct Physics {
    pub position: Point,
    pub velocity: Vector,
    pub gravity: f64,
    pub fps: f64,
}

impl Physics {
    pub fn new(position: Point, velocity: Vector, fps: f64) -> Self {
        Self {
            position,
            velocity,
            gravity: 50.0, // Terminal gravity equivalent
            fps,
        }
    }

    pub fn update(&mut self) {
        let dt = 1.0 / self.fps;
        
        // Update velocity with gravity
        self.velocity.y += self.gravity * dt;
        
        // Update position with velocity
        self.position.x += self.velocity.x * dt;
        self.position.y += self.velocity.y * dt;
        self.position.z += self.velocity.z * dt;
    }

    pub fn position(&self) -> Point {
        self.position.clone()
    }

    pub fn velocity(&self) -> Vector {
        self.velocity.clone()
    }
}

pub struct Particle {
    pub char: String,
    pub color: Color,
    pub tail_char: String,
    pub physics: Physics,
    pub hidden: bool,
    pub shooting: bool,
    pub explosion_call: Option<fn(color: Color, x: f64, y: f64, width: i32, height: i32) -> Vec<Particle>>,
}

impl Particle {
    pub fn new(char: String, color: Color, position: Point, velocity: Vector, fps: f64) -> Self {
        Self {
            char,
            color,
            tail_char: String::new(),
            physics: Physics::new(position, velocity, fps),
            hidden: false,
            shooting: false,
            explosion_call: None,
        }
    }
}

pub struct Frame {
    pub width: i32,
    pub height: i32,
}

impl Default for Frame {
    fn default() -> Self {
        Self { width: 0, height: 0 }
    }
}

pub struct System {
    pub frame: Frame,
    pub particles: Vec<Particle>,
}

impl System {
    pub fn new() -> Self {
        Self {
            frame: Frame::default(),
            particles: Vec::new(),
        }
    }

    pub fn update(&mut self) {
        let mut new_particles = Vec::new();
        let mut i = 0;
        
        while i < self.particles.len() {
            let pos = self.particles[i].physics.position();

            // If the shooting particle is slow enough then hide it and call the explosion function
            if !self.particles[i].hidden && self.particles[i].shooting && self.particles[i].physics.velocity().y > -3.0 {
                self.particles[i].hidden = true;
                if let Some(explosion_fn) = self.particles[i].explosion_call {
                    let color = self.particles[i].color;
                    let explosion_particles = explosion_fn(color, pos.x, pos.y, self.frame.width, self.frame.height);
                    new_particles.extend(explosion_particles);
                }
            }

            // Remove particles that are hidden or out of bounds
            if self.particles[i].hidden 
                || pos.x > self.frame.width as f64 
                || pos.x < 0.0 
                || pos.y > self.frame.height as f64 {
                self.particles.remove(i);
            } else {
                self.particles[i].physics.update();
                i += 1;
            }
        }
        
        // Add new explosion particles
        self.particles.extend(new_particles);
    }

    pub fn visible(&self, particle: &Particle) -> bool {
        let pos = particle.physics.position();
        let x = pos.x as i32;
        let y = pos.y as i32;
        !particle.hidden 
            && y >= 0 
            && y < self.frame.height - 1 
            && x >= 0 
            && x < self.frame.width - 1
    }

    pub fn render(&self) -> Vec<Vec<String>> {
        let mut plane = vec![vec![String::new(); self.frame.width as usize]; self.frame.height as usize];
        
        for particle in &self.particles {
            if self.visible(particle) {
                let pos = particle.physics.position();
                let y = pos.y as usize;
                let x = pos.x as usize;
                
                if y < plane.len() && x < plane[y].len() {
                    plane[y][x] = particle.char.clone();
                    
                    if particle.shooting {
                        let tail_length = (-particle.physics.velocity().y) as i32;
                        for i in 1..tail_length {
                            let tail_y = (pos.y as i32 + i) as usize;
                            if tail_y > 0 && tail_y < (self.frame.height - 1) as usize && tail_y < plane.len() {
                                plane[tail_y][x] = particle.tail_char.clone();
                            }
                        }
                    }
                }
            }
        }
        
        plane
    }
}