Skip to main content

sl/
render.rs

1use crate::config::Config;
2use crate::smoke::{add_smoke, get_smoke_particles, set_generation_gate, update_smoke};
3use crate::terminal::Terminal;
4use crate::train::ascii::*;
5use std::io;
6
7pub fn render_frame(
8    terminal: &Terminal,
9    x: i32,
10    pattern: usize,
11    config: &Config,
12) -> io::Result<()> {
13    let frame = build_frame(terminal, x, pattern, config);
14    terminal.render_frame(frame)?;
15    Ok(())
16}
17
18fn build_frame(terminal: &Terminal, x: i32, pattern: usize, config: &Config) -> String {
19    let mut frame = String::new();
20
21    // Clear screen (ANSI command)
22    frame.push_str("\x1B[2J\x1B[H");
23
24    // Set generation gate: only generate smoke every 4 frames
25    set_generation_gate(x % 4 == 0);
26
27    // Update smoke particles (applies movement and increments pattern)
28    update_smoke();
29
30    // Draw smoke BEFORE train so train renders on top (prevents smoke from overwriting train)
31    build_smoke(&mut frame, terminal);
32
33    if config.logo {
34        build_logo(&mut frame, terminal, x, pattern, config);
35    } else if config.c51 {
36        build_c51(&mut frame, terminal, x, pattern, config);
37    } else {
38        build_d51(&mut frame, terminal, x, pattern, config);
39    }
40
41    if config.accident {
42        build_man(&mut frame, terminal, x, config);
43    }
44
45    frame
46}
47
48fn build_d51(frame: &mut String, terminal: &Terminal, x: i32, pattern: usize, config: &Config) {
49    let train_height = 10i32;
50    let y_center = (terminal.height() as i32 - train_height) / 2;
51    let y_base = if config.flying {
52        y_center - (x / 4)
53    } else {
54        y_center
55    };
56
57    let pattern = pattern % D51_PATTERNS;
58
59    // Draw main body
60    for (i, line) in D51_STR.iter().enumerate() {
61        let y = y_base + i as i32;
62        if y >= 0 && y < terminal.height() as i32 {
63            add_line_to_frame(frame, x, y, line, terminal);
64        }
65    }
66
67    // Add smoke from funnel (generation gated in set_generation_gate)
68    let funnel_y = y_base - 1;
69    add_smoke(x + D51_FUNNEL as i32, funnel_y);
70
71    // Draw wheels
72    for (i, line) in D51_WHL[pattern].iter().enumerate() {
73        let y = y_base + (D51_STR.len() as i32) + i as i32;
74        if y >= 0 && y < terminal.height() as i32 {
75            add_line_to_frame(frame, x, y, line, terminal);
76        }
77    }
78
79    // Draw coal
80    for (i, line) in D51_COAL.iter().enumerate() {
81        let y = y_base + i as i32;
82        if y >= 0 && y < terminal.height() as i32 {
83            add_line_to_frame(frame, x + 53, y, line, terminal);
84        }
85    }
86}
87
88fn build_c51(frame: &mut String, terminal: &Terminal, x: i32, pattern: usize, config: &Config) {
89    let train_height = 10i32;
90    let y_center = (terminal.height() as i32 - train_height) / 2;
91    let y_base = if config.flying {
92        y_center - (x / 4)
93    } else {
94        y_center
95    };
96
97    let pattern = pattern % C51_PATTERNS;
98
99    // Draw main body
100    for (i, line) in C51_STR.iter().enumerate() {
101        let y = y_base + i as i32;
102        if y >= 0 && y < terminal.height() as i32 {
103            add_line_to_frame(frame, x, y, line, terminal);
104        }
105    }
106
107    // Add smoke from funnel (generation gated in set_generation_gate)
108    let funnel_y = y_base - 1;
109    add_smoke(x + C51_FUNNEL as i32, funnel_y);
110
111    // Draw wheels
112    for (i, line) in C51_WHL[pattern].iter().enumerate() {
113        let y = y_base + (C51_STR.len() as i32) + i as i32;
114        if y >= 0 && y < terminal.height() as i32 {
115            add_line_to_frame(frame, x, y, line, terminal);
116        }
117    }
118
119    // Draw coal
120    for (i, line) in C51_COAL.iter().enumerate() {
121        let y = y_base + i as i32;
122        if y >= 0 && y < terminal.height() as i32 {
123            add_line_to_frame(frame, x + 55, y, line, terminal);
124        }
125    }
126}
127
128fn build_logo(frame: &mut String, terminal: &Terminal, x: i32, pattern: usize, config: &Config) {
129    let train_height = 10i32;
130    let y_center = (terminal.height() as i32 - train_height) / 2;
131    let y_base = if config.flying {
132        y_center - (x / 4)
133    } else {
134        y_center
135    };
136
137    let pattern = pattern % 6;
138
139    // Draw SL logo
140    for (i, line) in LOGO_STR.iter().enumerate() {
141        let y = y_base + i as i32;
142        if y >= 0 && y < terminal.height() as i32 {
143            add_line_to_frame(frame, x, y, line, terminal);
144        }
145    }
146
147    // Add smoke from funnel (generation gated in set_generation_gate)
148    let funnel_y = y_base - 1;
149    add_smoke(x + LOGO_FUNNEL as i32, funnel_y);
150
151    // Draw wheels
152    for (i, line) in LOGO_WHL[pattern].iter().enumerate() {
153        let y = y_base + LOGO_STR.len() as i32 + i as i32;
154        if y >= 0 && y < terminal.height() as i32 {
155            add_line_to_frame(frame, x, y, line, terminal);
156        }
157    }
158
159    // Draw coal
160    for (i, line) in LOGO_COAL.iter().enumerate() {
161        let y = y_base + LOGO_STR.len() as i32 + 2 + i as i32;
162        if y >= 0 && y < terminal.height() as i32 {
163            add_line_to_frame(frame, x, y, line, terminal);
164        }
165    }
166
167    // Draw car
168    for (i, line) in LOGO_CAR.iter().enumerate() {
169        let y = y_base + LOGO_STR.len() as i32 + 2 + i as i32;
170        if y >= 0 && y < terminal.height() as i32 {
171            add_line_to_frame(frame, x, y, line, terminal);
172        }
173    }
174}
175
176fn build_man(frame: &mut String, terminal: &Terminal, x: i32, _config: &Config) {
177    let y = terminal.height() as i32 - 3;
178    for (i, line) in MAN.iter().enumerate() {
179        let y = y + i as i32;
180        if y >= 0 && y < terminal.height() as i32 {
181            add_line_to_frame(frame, x + 15, y, line, terminal);
182        }
183    }
184}
185
186fn build_smoke(frame: &mut String, terminal: &Terminal) {
187    let particles = get_smoke_particles();
188
189    for particle in particles {
190        // Particle coordinates have already been updated in build_frame() before this function
191        // Only draw if within screen bounds
192        if particle.x >= 0
193            && particle.x < terminal.width() as i32
194            && particle.y >= 0
195            && particle.y < terminal.height() as i32
196        {
197            let pattern_idx = particle.pattern.min(15) as usize;
198            let kind = particle.kind % 2;
199
200            if let Some(smoke_str) = SMOKE_PATTERN.get(kind) {
201                if let Some(ch_str) = smoke_str.get(pattern_idx) {
202                    // Draw all characters from the smoke string (e.g., "(   )")
203                    let mut x_offset = 0;
204                    for ch in ch_str.chars() {
205                        let draw_x = (particle.x as i32 + x_offset) as u16;
206                        if (particle.x as i32 + x_offset) >= 0
207                            && (particle.x as i32 + x_offset) < terminal.width() as i32
208                            && ch != ' '
209                        {
210                            add_char_to_frame(frame, draw_x, particle.y as u16, ch);
211                        }
212                        x_offset += 1;
213                    }
214                }
215            }
216        }
217    }
218}
219
220fn add_line_to_frame(frame: &mut String, start_x: i32, y: i32, line: &str, terminal: &Terminal) {
221    if y < 0 || y >= terminal.height() as i32 {
222        return;
223    }
224
225    for (i, ch) in line.chars().enumerate() {
226        let x = start_x + i as i32;
227        if x >= 0 && x < terminal.width() as i32 {
228            add_char_to_frame(frame, x as u16, y as u16, ch);
229        }
230    }
231}
232
233fn add_char_to_frame(frame: &mut String, x: u16, y: u16, ch: char) {
234    // ANSI escape sequence: CSI Py ; Px H (MoveTo)
235    frame.push_str(&format!("\x1B[{};{}H{}", y + 1, x + 1, ch));
236}