use crate::{config::AnimSettings, palettes::Palette, ESC};
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
const MAX_HEAT: u8 = 36;
const STEPS_PER_FRAME: u32 = 2;
const MAX_DURATION: Duration = Duration::from_millis(2200);
const SOURCE_COOL_START: f32 = 0.38;
const DIE_OUT_THRESHOLD: u8 = 2;
pub struct Rng(u64);
impl Default for Rng {
fn default() -> Self {
Self::new()
}
}
impl Rng {
pub fn new() -> Self {
let d = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let seed = (d.as_secs().wrapping_mul(6364136223846793005) ^ d.subsec_nanos() as u64) | 1;
Rng(seed)
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
pub fn range(&mut self, lo: i32, hi: i32) -> i32 {
let span = (hi - lo + 1) as u64;
lo + (self.next_u64() % span) as i32
}
}
pub fn terminal_size() -> (usize, usize) {
#[cfg(unix)]
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) == 0
&& ws.ws_col > 0
&& ws.ws_row > 0
{
return (ws.ws_col as usize, ws.ws_row as usize);
}
}
#[cfg(windows)]
if let Some((w, h)) = crate::win::terminal_size() {
return (w, h);
}
(80, 24)
}
#[derive(PartialEq, Clone, Copy)]
enum CellColor {
Default,
Rgb(u8, u8, u8),
}
fn render(
buf: &mut String,
grid: &[u8],
burned: &[bool],
cols: usize,
rows: usize,
palette: &Palette,
) {
use std::fmt::Write as _;
buf.clear();
let mut last_color: Option<CellColor> = None;
let mut need_move = true;
let mut write_col = 0usize;
let mut write_row = 0usize;
for y in 0..rows {
for x in 0..cols {
let idx = y * cols + x;
let heat = grid[idx];
if heat == 0 && !burned[idx] {
need_move = true;
continue;
}
if need_move || write_row != y || write_col != x {
let _ = write!(buf, "{ESC}[{};{}H", y + 1, x + 1);
last_color = None; need_move = false;
write_row = y;
write_col = x;
}
let color = if heat > 0 {
let (r, g, b) = palette[heat as usize];
CellColor::Rgb(r, g, b)
} else {
CellColor::Default
};
if last_color != Some(color) {
match color {
CellColor::Default => {
let _ = write!(buf, "{ESC}[49m");
}
CellColor::Rgb(r, g, b) => {
let _ = write!(buf, "{ESC}[48;2;{r};{g};{b}m");
}
}
last_color = Some(color);
}
buf.push(' ');
write_col += 1;
}
}
let _ = write!(buf, "{ESC}[0m");
}
fn clear_unburned(buf: &mut String, burned: &[bool], cols: usize, rows: usize) {
use std::fmt::Write as _;
buf.clear();
let _ = write!(buf, "{ESC}[49m");
let mut need_move = true;
let mut write_col = 0usize;
let mut write_row = 0usize;
for y in 0..rows {
for x in 0..cols {
let idx = y * cols + x;
if burned[idx] {
need_move = true;
continue;
}
if need_move || write_row != y || write_col != x {
let _ = write!(buf, "{ESC}[{};{}H", y + 1, x + 1);
need_move = false;
write_row = y;
write_col = x;
}
buf.push(' ');
write_col += 1;
}
}
let _ = write!(buf, "{ESC}[0m");
}
fn resize_grid(cols: usize, rows: usize, top_down: bool) -> Vec<u8> {
let mut grid = vec![0u8; cols * rows];
let source_row = if top_down { 0 } else { rows - 1 };
for x in 0..cols {
grid[source_row * cols + x] = MAX_HEAT;
}
grid
}
pub fn burn(palette: &Palette, settings: &AnimSettings, interrupted: Arc<AtomicBool>) {
let (mut cols, mut rows) = terminal_size();
let mut grid = resize_grid(cols, rows, settings.direction);
let mut burned = vec![false; cols * rows];
let mut rng = Rng::new();
let stdout = io::stdout();
let mut out = stdout.lock();
let _ = write!(out, "{ESC}[?25l");
let start = Instant::now();
let source_cool_at = MAX_DURATION.mul_f32(SOURCE_COOL_START);
let mut frame = String::with_capacity(cols * rows * 8);
let frame_delay = Duration::from_millis(1000 / settings.fps.max(1) as u64);
let top_down = settings.direction;
loop {
if interrupted.load(Ordering::Relaxed) {
break;
}
let elapsed = start.elapsed();
if elapsed > MAX_DURATION {
break;
}
let (new_cols, new_rows) = terminal_size();
if new_cols != cols || new_rows != rows {
cols = new_cols;
rows = new_rows;
grid = resize_grid(cols, rows, top_down);
burned = vec![false; cols * rows];
frame.reserve(cols * rows * 8);
}
let source_row = if top_down { 0 } else { rows - 1 };
if elapsed <= source_cool_at {
for x in 0..cols {
grid[source_row * cols + x] = MAX_HEAT;
}
}
for _ in 0..STEPS_PER_FRAME {
if top_down {
for x in 0..cols {
for y in 0..rows - 1 {
let above = grid[y * cols + x];
let decay = match settings.height {
0 => rng.range(1, 4), 1 => rng.range(0, 3), 2 => rng.range(0, 2), 3 => rng.range(0, 1), _ => rng.range(0, 3),
};
let drift = match settings.wind {
-2 => rng.range(-2, 0), -1 => rng.range(-1, 0), 0 => rng.range(-1, 1), 1 => rng.range(0, 1), 2 => rng.range(0, 2), _ => rng.range(-1, 1),
};
let nx = (x as i32 + drift).clamp(0, cols as i32 - 1) as usize;
let new_val = (above as i32 - decay).max(0) as u8;
grid[(y + 1) * cols + nx] = new_val;
}
}
if elapsed > source_cool_at {
for x in 0..cols {
let idx = x; let dec = rng.range(2, 6);
grid[idx] = (grid[idx] as i32 - dec).max(0) as u8;
}
}
} else {
for x in 0..cols {
for y in 1..rows {
let below = grid[y * cols + x];
let decay = match settings.height {
0 => rng.range(1, 4), 1 => rng.range(0, 3), 2 => rng.range(0, 2), 3 => rng.range(0, 1), _ => rng.range(0, 3),
};
let drift = match settings.wind {
-2 => rng.range(-2, 0), -1 => rng.range(-1, 0), 0 => rng.range(-1, 1), 1 => rng.range(0, 1), 2 => rng.range(0, 2), _ => rng.range(-1, 1),
};
let nx = (x as i32 + drift).clamp(0, cols as i32 - 1) as usize;
let new_val = (below as i32 - decay).max(0) as u8;
grid[(y - 1) * cols + nx] = new_val;
}
}
if elapsed > source_cool_at {
for x in 0..cols {
let idx = (rows - 1) * cols + x;
let dec = rng.range(2, 6);
grid[idx] = (grid[idx] as i32 - dec).max(0) as u8;
}
}
}
}
for (i, &h) in grid.iter().enumerate() {
if h > 0 {
burned[i] = true;
}
}
render(&mut frame, &grid, &burned, cols, rows, palette);
let _ = out.write_all(frame.as_bytes());
let _ = out.flush();
if elapsed > source_cool_at {
let peak = grid.iter().copied().max().unwrap_or(0);
if peak < DIE_OUT_THRESHOLD {
break;
}
}
std::thread::sleep(frame_delay);
}
clear_unburned(&mut frame, &burned, cols, rows);
let _ = out.write_all(frame.as_bytes());
let _ = out.flush();
let _ = write!(out, "{ESC}[?25h"); }