Skip to main content

fire_cli_rs/
renderer.rs

1use std::{
2    io::{self, Write, BufWriter},
3    time::{Duration, Instant},
4    sync::atomic::Ordering,
5};
6
7use crate::theme::{Theme, ColorMode};
8use crate::simulation::Rng;
9use crate::input::check_input;
10use crate::terminal::{get_size, EXIT_REQUESTED};
11
12#[cfg(unix)]
13use crate::terminal::RESIZE_REQUESTED;
14
15pub fn precompile_color_codes(theme: &Theme) -> [Vec<u8>; 4] {
16    [
17        format!("\x1b[{}m", theme.colors[0]).into_bytes(),
18        format!("\x1b[{}m", theme.colors[1]).into_bytes(),
19        format!("\x1b[{}m", theme.colors[2]).into_bytes(),
20        format!("\x1b[{}m", theme.colors[3]).into_bytes(),
21    ]
22}
23
24pub const fn precompile_chars() -> [[u8; 4]; 10] {
25    let mut result = [[0u8; 4]; 10];
26    let mut i = 0;
27    while i < 10 {
28        let ch = Theme::CHARS[i] as u32;
29        assert!(ch <= 127, "CHARS must contain only ASCII characters");
30        result[i][0] = ch as u8;
31        i += 1;
32    }
33    result
34}
35
36#[inline(always)]
37pub fn push_truecolor(buf: &mut Vec<u8>, r: u8, g: u8, b: u8) {
38    #[inline(always)]
39    fn push_u8(buf: &mut Vec<u8>, mut n: u8) {
40        if n >= 100 {
41            buf.push(b'0' + n / 100);
42            n %= 100;
43            buf.push(b'0' + n / 10);
44            buf.push(b'0' + n % 10);
45        } else if n >= 10 {
46            buf.push(b'0' + n / 10);
47            buf.push(b'0' + n % 10);
48        } else {
49            buf.push(b'0' + n);
50        }
51    }
52    buf.extend_from_slice(b"\x1b[38;2;");
53    push_u8(buf, r);
54    buf.push(b';');
55    push_u8(buf, g);
56    buf.push(b';');
57    push_u8(buf, b);
58    buf.push(b'm');
59}
60
61pub fn run_main_loop(
62    theme: &Theme,
63    color_mode: ColorMode,
64    fps: u32,
65    use_color: bool,
66) -> io::Result<()> {
67    use crate::simulation::simulate_step;
68    use crate::theme::hue_to_color_bytes;
69
70    let stdout = io::stdout();
71    let mut stdout = BufWriter::with_capacity(128 * 1024, stdout);
72
73    stdout.write_all(b"\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H")?;
74    stdout.flush()?;
75
76    let mut current_size = get_size();
77    let (mut w, mut h) = current_size;
78    let mut size = w * h;
79    let mut buf = vec![0u8; size + w + 1];
80
81    let render_interval = Duration::from_secs_f64(1.0 / fps as f64);
82    let physics_step = 1.0 / fps as f64;
83
84    let mut screen = Vec::with_capacity((w + 1) * h * 20);
85    let mut rng = Rng::new();
86    let mut last_instant = Instant::now();
87    let mut accumulator = 0.0f64;
88
89    let color_codes = precompile_color_codes(theme);
90    let char_bytes = precompile_chars();
91    let mut rainbow_offset: f32 = 0.0;
92
93    loop {
94        let frame_start = Instant::now();
95
96        #[cfg(unix)]
97        if RESIZE_REQUESTED.load(Ordering::Relaxed) {
98            RESIZE_REQUESTED.store(false, Ordering::Relaxed);
99            let new_size = get_size();
100            if new_size != current_size {
101                current_size = new_size;
102                w = current_size.0;
103                h = current_size.1;
104                size = w * h;
105                buf = vec![0u8; size + w + 1];
106                screen = Vec::with_capacity((w + 1) * h * 20);
107                stdout.write_all(b"\x1b[2J")?;
108                stdout.flush()?;
109            }
110        }
111        #[cfg(windows)]
112        {
113            let new_size = get_size();
114            if new_size != current_size {
115                current_size = new_size;
116                w = current_size.0;
117                h = current_size.1;
118                size = w * h;
119                buf = vec![0u8; size + w + 1];
120                screen = Vec::with_capacity((w + 1) * h * 20);
121                stdout.write_all(b"\x1b[2J")?;
122                stdout.flush()?;
123            }
124        }
125
126        let now = Instant::now();
127        let dt = now.duration_since(last_instant);
128        last_instant = now;
129        accumulator += dt.as_secs_f64();
130
131        if accumulator > 0.25 {
132            accumulator = 0.25;
133        }
134
135        let mut steps = 0;
136        while accumulator >= physics_step && steps < 5 {
137            simulate_step(&mut buf, w, h, &mut rng);
138            accumulator -= physics_step;
139            steps += 1;
140        }
141        if color_mode == ColorMode::Rainbow {
142            rainbow_offset = (rainbow_offset + 0.5).rem_euclid(360.0);
143        }
144        screen.clear();
145        screen.extend_from_slice(b"\x1b[1;1H");
146
147        let mut last_color_idx: Option<usize> = None;
148        let mut last_rgb: Option<(u8, u8, u8)> = None;
149
150        for y in 0..h {
151            let row_start = y * w;
152            for i in row_start..row_start + w {
153                let heat = buf[i] as usize;
154
155                if use_color {
156                    match color_mode {
157                        ColorMode::Theme => {
158                            let color_idx = match heat {
159                                0..=4 => 0,
160                                5..=9 => 1,
161                                10..=15 => 2,
162                                _ => 3,
163                            };
164                            if last_color_idx != Some(color_idx) {
165                                screen.extend_from_slice(&color_codes[color_idx]);
166                                last_color_idx = Some(color_idx);
167                                last_rgb = None;
168                            }
169                        }
170                        ColorMode::Rainbow => {
171                            if heat > 0 {
172                                let x = i % w;
173                                let hue = (rainbow_offset + x as f32 * 360.0 / w as f32)
174                                    .rem_euclid(360.0);
175                                let [r, g, b] = hue_to_color_bytes(hue, heat);
176                                if last_rgb != Some((r, g, b)) {
177                                    push_truecolor(&mut screen, r, g, b);
178                                    last_rgb = Some((r, g, b));
179                                    last_color_idx = None;
180                                }
181                            } else {
182                                last_rgb = None;
183                            }
184                        }
185                    }
186                }
187
188                let ch_idx = heat.min(9);
189                screen.push(char_bytes[ch_idx][0]);
190            }
191
192            if use_color {
193                screen.extend_from_slice(b"\x1b[0m");
194                last_color_idx = None;
195                last_rgb = None;
196            }
197
198            screen.extend_from_slice(b"\x1b[0K");
199            if y < h - 1 {
200                screen.extend_from_slice(b"\r\n");
201            }
202        }
203
204        stdout.write_all(&screen)?;
205        stdout.flush()?;
206
207        if check_input() || EXIT_REQUESTED.load(Ordering::Relaxed) {
208            break;
209        }
210
211        let frame_elapsed = frame_start.elapsed();
212        if frame_elapsed < render_interval {
213            std::thread::sleep(render_interval - frame_elapsed);
214        }
215    }
216
217    Ok(())
218}