1use 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#[derive(Debug, Clone, Copy)]
18pub struct Char {
19 pub text: char,
20 pub color: style::Color,
21}
22
23#[allow(unused)]
24impl Char {
25 fn new(text: char, color: style::Color) -> Self {
27 Self { text, color }
28 }
29}
30
31pub 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 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 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 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 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 } else {
272 "/\\|()1{}[]?"
273 }
274 } else if density < 0.7 {
275 if cjk {
276 "时中自字木月日目火田左右点以"
277 } else {
279 "oahkbdpqwmZO0QLCJUYXzcvunxrjft*"
280 }
281 } else if cjk {
282 "龖龠龜"
283 } 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 } else {
305 "-_ +~<> i!lI;:,\"^"
306 }
307 } else if density < 0.85 {
308 if cjk {
309 "人中亿入上下火土"
310 } else {
312 "/\\| ()1{}[ ]?"
313 }
314 } else if cjk {
315 "繁荣昌盛国泰民安龍龖龠龜耋"
316 } 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 } else {
337 " /\\| ( ) 1{} [ ]?i !l I;: ,\"^ "
338 };
339 palette
340 .chars()
341 .choose(&mut thread_rng())
342 .expect("Fail to choose character.")
343}