use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::Paragraph,
Frame, Terminal,
};
use std::{
env,
io,
time::{Duration, Instant},
};
use tokio::time;
mod array;
mod confetti;
mod fireworks;
mod simulation;
use simulation::System;
enum Mode {
Confetti,
Fireworks,
}
struct App {
system: System,
mode: Mode,
last_frame: Instant,
}
impl App {
fn new(mode: Mode) -> Self {
Self {
system: System::new(),
mode,
last_frame: Instant::now(),
}
}
fn update(&mut self, terminal_size: Rect) {
if self.system.frame.width != terminal_size.width as i32
|| self.system.frame.height != terminal_size.height as i32 {
self.system.frame.width = terminal_size.width as i32;
self.system.frame.height = terminal_size.height as i32;
if self.system.particles.is_empty() {
match self.mode {
Mode::Confetti => {
let new_particles = confetti::spawn(self.system.frame.width, self.system.frame.height);
self.system.particles.extend(new_particles);
}
Mode::Fireworks => {
let new_particle = fireworks::spawn_shoot(self.system.frame.width, self.system.frame.height);
self.system.particles.push(new_particle);
}
}
}
}
let now = Instant::now();
if now.duration_since(self.last_frame) >= Duration::from_millis(33) { self.system.update();
self.last_frame = now;
}
}
fn spawn_new_particles(&mut self) {
match self.mode {
Mode::Confetti => {
let new_particles = confetti::spawn(self.system.frame.width, self.system.frame.height);
self.system.particles.extend(new_particles);
}
Mode::Fireworks => {
let new_particle = fireworks::spawn_shoot(self.system.frame.width, self.system.frame.height);
self.system.particles.push(new_particle);
}
}
}
fn render(&self, frame: &mut Frame) {
let plane = self.system.render();
let mut lines = Vec::new();
for row in plane {
let mut spans = Vec::new();
for (x, cell) in row.iter().enumerate() {
if cell.is_empty() {
spans.push(Span::raw(" "));
} else {
let mut color = Color::White;
for particle in &self.system.particles {
if self.system.visible(particle) {
let pos = particle.physics.position();
if pos.x as usize == x && pos.y as usize == lines.len() {
color = particle.color;
break;
}
}
}
spans.push(Span::styled(cell.clone(), Style::default().fg(color)));
}
}
lines.push(Line::from(spans));
}
let paragraph = Paragraph::new(lines);
frame.render_widget(paragraph, frame.area());
}
}
async fn run_app() -> Result<()> {
let args: Vec<String> = env::args().collect();
let mode = if args.len() > 1 && args[1] == "fireworks" {
Mode::Fireworks
} else {
Mode::Confetti
};
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(mode);
let mut last_tick = Instant::now();
loop {
if event::poll(Duration::from_millis(16))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => break,
KeyCode::Esc => break,
_ => app.spawn_new_particles(),
}
}
}
let now = Instant::now();
if now.duration_since(last_tick) >= Duration::from_millis(33) {
terminal.draw(|f| {
app.update(f.area());
app.render(f);
})?;
last_tick = now;
}
time::sleep(Duration::from_millis(1)).await;
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
run_app().await
}