Skip to main content

sl/
render.rs

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