firework_rs/
term.rs

1//! `term` module provides functions of rendering in terminal
2
3use std::io::{Stdout, Write};
4
5use crossterm::{cursor::MoveTo, queue, style, terminal};
6use glam::Vec2;
7use rand::{seq::IteratorRandom, thread_rng};
8
9use crate::{
10    config::Config,
11    fireworks::{FireworkManager, FireworkState},
12    particle::LifeState,
13    utils::distance_squared,
14};
15
16/// Wrap a character with color
17#[derive(Debug, Clone, Copy)]
18pub struct Char {
19    pub text: char,
20    pub color: style::Color,
21}
22
23#[allow(unused)]
24impl Char {
25    /// Create a new `Char`
26    fn new(text: char, color: style::Color) -> Self {
27        Self { text, color }
28    }
29}
30
31/// Struct that represents a terminal
32pub struct Terminal {
33    pub size: (u16, u16),
34    pub screen: Vec<Vec<Char>>,
35}
36
37impl Default for Terminal {
38    fn default() -> Self {
39        let size = terminal::size().expect("Fail to get terminal size.");
40        let screen = vec![
41            vec![
42                Char {
43                    text: ' ',
44                    color: style::Color::White
45                };
46                size.0 as usize
47            ];
48            size.1 as usize
49        ];
50        Self { size, screen }
51    }
52}
53
54impl Terminal {
55    pub fn new(cfg: &Config) -> Self {
56        let mut size = terminal::size().expect("Fail to get terminal size.");
57        if cfg.enable_cjk {
58            size.0 = (size.0 - 1) / 2;
59        }
60        let screen = vec![
61            vec![
62                Char {
63                    text: ' ',
64                    color: style::Color::White
65                };
66                size.0 as usize
67            ];
68            size.1 as usize
69        ];
70        Self { size, screen }
71    }
72
73    /// Reload terminal to adapt new window size
74    pub fn reinit(&mut self, cfg: &Config) {
75        let mut size = terminal::size().expect("Fail to get terminal size.");
76        if cfg.enable_cjk {
77            size.0 = (size.0 - 1) / 2;
78        }
79        self.size = size;
80        self.screen = vec![
81            vec![
82                Char {
83                    text: ' ',
84                    color: style::Color::White
85                };
86                size.0 as usize
87            ];
88            size.1 as usize
89        ];
90    }
91
92    /// Clear the terminal screen by setting all the characters in terminal to space
93    pub fn clear_screen(&mut self) {
94        let size = terminal::size().expect("Fail to get terminal size.");
95        self.screen = vec![
96            vec![
97                Char {
98                    text: ' ',
99                    color: style::Color::White
100                };
101                size.0 as usize
102            ];
103            size.1 as usize
104        ];
105    }
106
107    /// Print the data out to terminal
108    pub fn print(&self, w: &mut Stdout, cfg: &Config) {
109        self.screen.iter().enumerate().for_each(|(y, line)| {
110            line.iter().enumerate().for_each(|(x, c)| {
111                queue!(
112                    w,
113                    MoveTo(
114                        if cfg.enable_cjk {
115                            (x * 2) as u16
116                        } else {
117                            x as u16
118                        },
119                        y as u16
120                    ),
121                    style::SetForegroundColor(c.color),
122                    style::Print(c.text)
123                )
124                .expect("Std io error.")
125            });
126        });
127        w.flush().expect("Std io error.");
128    }
129
130    /// Write the rendering data of all `Fireworks` and `Particles` to `Terminal`
131    pub fn render(&mut self, fm: &FireworkManager, cfg: &Config) {
132        self.clear_screen();
133        for firework in fm.fireworks.iter().rev() {
134            if firework.state == FireworkState::Alive {
135                for particle in firework.current_particles.iter().rev() {
136                    let grad = if firework.config.enable_gradient {
137                        Some((firework.config.gradient_scale)(
138                            particle.time_elapsed.as_secs_f32()
139                                / particle.config.life_time.as_secs_f32(),
140                        ))
141                    } else {
142                        None
143                    };
144                    particle
145                        .trail
146                        .iter()
147                        .map(|p| {
148                            if cfg.enable_cjk {
149                                *p
150                            } else {
151                                Vec2::new(p.x * 2., p.y)
152                            }
153                        })
154                        .rev()
155                        .collect::<Vec<_>>()
156                        .windows(2)
157                        .enumerate()
158                        .for_each(|(idx, v)| {
159                            let density = (particle.config.trail_length - idx - 1) as f32
160                                / particle.config.trail_length as f32;
161                            construct_line(v[0], v[1]).iter().for_each(|p| {
162                                if self.inside(*p)
163                                    && self.screen[p.1 as usize][p.0 as usize].text == ' '
164                                {
165                                    if let Some(c) = match particle.life_state {
166                                        LifeState::Alive => {
167                                            Some(get_char_alive(density, cfg.enable_cjk))
168                                        }
169                                        LifeState::Declining => {
170                                            Some(get_char_declining(density, cfg.enable_cjk))
171                                        }
172                                        LifeState::Dying => {
173                                            Some(get_char_dying(density, cfg.enable_cjk))
174                                        }
175                                        LifeState::Dead => None,
176                                    } {
177                                        self.screen[p.1 as usize][p.0 as usize] = Char {
178                                            text: c,
179                                            color: {
180                                                let color_u8 = if let Some(g) = grad {
181                                                    shift_gradient(particle.config.color, g)
182                                                } else {
183                                                    particle.config.color
184                                                };
185                                                style::Color::Rgb {
186                                                    r: color_u8.0,
187                                                    g: color_u8.1,
188                                                    b: color_u8.2,
189                                                }
190                                            },
191                                        }
192                                    }
193                                }
194                            });
195                        });
196                }
197            }
198        }
199    }
200
201    fn inside(&self, (x, y): (isize, isize)) -> bool {
202        x < self.size.0 as isize && y < self.size.1 as isize && x >= 0 && y >= 0
203    }
204}
205
206fn construct_line(a: Vec2, b: Vec2) -> Vec<(isize, isize)> {
207    const STEP: f32 = 0.2;
208    let (x0, y0) = (a.x, a.y);
209    let (x1, y1) = (b.x, b.y);
210    let mut path = Vec::new();
211    let mut x = x0;
212    let mut y = y0;
213    let slope = (y1 - y0) / (x1 - x0);
214    let dx = if x0 == x1 {
215        0.
216    } else if x1 > x0 {
217        1.
218    } else {
219        -1.
220    };
221    let dy = if y0 == y1 {
222        0.
223    } else if y1 > y0 {
224        1.
225    } else {
226        -1.
227    };
228    let mut ds = distance_squared(a, b) + f32::EPSILON;
229    path.push((x0.round() as isize, y0.round() as isize));
230    if (x1 - x0).abs() >= (y1 - y0).abs() {
231        while distance_squared(Vec2::new(x, y), b) <= ds {
232            if *path.last().unwrap() != (x.round() as isize, y.round() as isize) {
233                path.push((x.round() as isize, y.round() as isize));
234                ds = distance_squared(Vec2::new(x, y), b);
235            }
236            x += dx * STEP;
237            y += dy * (STEP * slope).abs();
238        }
239    } else {
240        while distance_squared(Vec2::new(x, y), b) <= ds {
241            if *path.last().unwrap() != (x.round() as isize, y.round() as isize) {
242                path.push((x.round() as isize, y.round() as isize));
243                ds = distance_squared(Vec2::new(x, y), b);
244            }
245            y += dy * STEP;
246            x += dx * (STEP / slope).abs();
247        }
248    }
249    path
250}
251
252fn shift_gradient(color: (u8, u8, u8), scale: f32) -> (u8, u8, u8) {
253    (
254        (color.0 as f32 * scale) as u8,
255        (color.1 as f32 * scale) as u8,
256        (color.2 as f32 * scale) as u8,
257    )
258}
259
260fn get_char_alive(density: f32, cjk: bool) -> char {
261    let palette = if density < 0.3 {
262        if cjk {
263            "。,”“』 『¥"
264        } else {
265            "`'. "
266        }
267    } else if density < 0.5 {
268        if cjk {
269            "一二三二三五十十已于上下义天"
270            // "いうよへくひとフーク "
271        } else {
272            "/\\|()1{}[]?"
273        }
274    } else if density < 0.7 {
275        if cjk {
276            "时中自字木月日目火田左右点以"
277            // "探しているのが誰かなのかどこかなのかそれともただ単に就職先なのか自分でもよくわからない"
278        } else {
279            "oahkbdpqwmZO0QLCJUYXzcvunxrjft*"
280        }
281    } else if cjk {
282        "龖龠龜"
283        // "東京福岡横浜縄"
284    } else {
285        "$@B%8&WM#"
286    };
287    palette
288        .chars()
289        .choose(&mut thread_rng())
290        .expect("Fail to choose character.")
291}
292
293fn get_char_declining(density: f32, cjk: bool) -> char {
294    let palette = if density < 0.2 {
295        if cjk {
296            "?。, 『』 ||"
297        } else {
298            "` '. "
299        }
300    } else if density < 0.6 {
301        if cjk {
302            "()【】*¥|十一二三六"
303            // "()【】*¥|ソファー"
304        } else {
305            "-_ +~<> i!lI;:,\"^"
306        }
307    } else if density < 0.85 {
308        if cjk {
309            "人中亿入上下火土"
310            // "人ならざるものに出会うかもしれない"
311        } else {
312            "/\\| ()1{}[ ]?"
313        }
314    } else if cjk {
315        "繁荣昌盛国泰民安龍龖龠龜耋"
316        // "時間言葉目覚"
317    } else {
318        "xrjft*"
319    };
320    palette
321        .chars()
322        .choose(&mut thread_rng())
323        .expect("Fail to choose character.")
324}
325
326fn get_char_dying(density: f32, cjk: bool) -> char {
327    let palette = if density < 0.6 {
328        if cjk {
329            "。 『 』 、: |。,— ……"
330        } else {
331            ".  ,`.    ^,' . "
332        }
333    } else if cjk {
334        "|¥人 上十入乙小 下"
335        // "イントマトナイフ"
336    } else {
337        " /\\| ( )  1{} [  ]?i !l I;: ,\"^ "
338    };
339    palette
340        .chars()
341        .choose(&mut thread_rng())
342        .expect("Fail to choose character.")
343}