1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_possible_wrap,
4 clippy::cast_sign_loss
5)]
6
7use std::fmt;
8use std::time;
9
10use textcanvas::random::Rng;
11use textcanvas::utils::GameLoop;
12use textcanvas::{Color, TextCanvas};
13
14const NB_STREAMS: i32 = 80;
15const STREAM_LENGTH: i32 = 24;
16const CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
17const GLITCHES: &str = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
18
19enum Shade {
20 BrightGreen,
21 DimGreen,
22 PreTip,
23 Tip,
24}
25
26impl From<Shade> for Color {
27 fn from(shade: Shade) -> Self {
28 match shade {
29 Shade::BrightGreen => Self::new().bright_green().fix(),
30 Shade::DimGreen => Self::new().green().fix(),
31 Shade::PreTip => Self::new().white().fix(),
32 Shade::Tip => Self::new().bright_white().fix(),
33 }
34 }
35}
36
37#[derive(Debug)]
41pub struct Droplet {
42 x: i32,
43 y: f64,
44 length: i32,
45 chars: String,
46 speed: f64,
47}
48
49impl Droplet {
50 pub fn new(rng: &mut Rng) -> Self {
51 let x = rng.irand_between(0, NB_STREAMS - 1);
52 let length = rng.irand_between(STREAM_LENGTH / 2, STREAM_LENGTH * 3 / 2);
53 let y = f64::from(-length) * 1.5; let mut chars: Vec<char> = CHARS.chars().collect();
56 chars = rng.sample(&chars, STREAM_LENGTH as usize);
57 let chars: String = chars.into_iter().collect();
58
59 let speed = rng.frand_between(0.3, 0.8);
60
61 Self {
62 x,
63 y,
64 length,
65 chars,
66 speed,
67 }
68 }
69
70 pub fn recycle(&mut self, rng: &mut Rng) {
71 let droplet = Self::new(rng);
72 *self = droplet;
73
74 if rng.frand() > 0.99 {
77 self.speed = (self.speed * 2.0).max(1.3);
78 }
79 }
80
81 fn iy(&self) -> i32 {
86 self.y.trunc() as i32
87 }
88
89 pub fn fall(&mut self) {
90 self.y += self.speed;
91 }
92
93 #[must_use]
94 pub fn has_fallen_out_of_screen(&self) -> bool {
95 self.iy() >= STREAM_LENGTH
96 }
97
98 pub fn maybe_glitch(&mut self, rng: &mut Rng) {
99 if rng.frand() <= 0.999 {
100 return;
101 }
102
103 let tip = self.iy() + self.length;
104 if tip - 2 < 0 {
105 return;
107 }
108
109 let pos = rng.irand_between(0, tip - 3) as usize;
111
112 let mut chars: Vec<char> = self.chars.chars().collect();
113 for (i, char) in chars.iter_mut().enumerate() {
114 if i == pos {
115 let glitch = rng.sample(&GLITCHES.chars().collect::<Vec<char>>(), 1)[0];
116 *char = glitch;
117 }
118 }
119 self.chars = chars.into_iter().collect();
120 }
121
122 pub fn draw_onto(&mut self, canvas: &mut TextCanvas) {
123 let chars = self.to_string();
124 debug_assert!(chars.chars().count() == STREAM_LENGTH as usize);
125
126 let i_tip = self.iy() + self.length - 1;
127
128 for (i, char_) in chars.chars().enumerate() {
131 let i = i as i32;
132 canvas.set_color(&self.determine_char_color(i, i_tip));
133 canvas.merge_text(&char_.to_string(), self.x, i);
135 }
136 }
137
138 fn determine_char_color(&self, i: i32, i_tip: i32) -> Color {
139 if i == i_tip {
140 return Shade::Tip.into();
141 }
142 if i == i_tip - 1 {
143 return Shade::PreTip.into();
144 }
145
146 let s = f64::from(self.x).sin().abs(); let d = f64::from(self.length).sin() - 0.3; if f64::from(i_tip - i).sin() * s <= d {
152 Shade::BrightGreen.into()
159 } else {
160 Shade::DimGreen.into()
161 }
162 }
163}
164
165impl fmt::Display for Droplet {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 if self.iy() + self.length <= 0 {
170 return write!(f, "{}", " ".repeat(STREAM_LENGTH as usize));
171 }
172 if self.iy() >= STREAM_LENGTH {
174 return write!(f, "{}", " ".repeat(STREAM_LENGTH as usize));
175 }
176 let window_start = self.iy().clamp(0, STREAM_LENGTH - 1) as usize;
177 let window_end = (self.iy() + self.length - 1).clamp(0, STREAM_LENGTH - 1) as usize;
178
179 write!(
180 f,
181 "{}{}{}",
182 " ".repeat(window_start),
183 &self
185 .chars
186 .chars()
187 .skip(window_start)
188 .take(window_end - window_start + 1)
189 .collect::<String>(),
190 " ".repeat(STREAM_LENGTH as usize - window_end - 1)
191 )
192 }
193}
194
195fn main() {
196 debug_assert!(CHARS.chars().count() > STREAM_LENGTH as usize);
197
198 if std::env::args().any(|arg| arg == "-i" || arg == "--with-intro") {
199 do_intro();
200 }
201
202 let mut canvas = TextCanvas::new(NB_STREAMS, STREAM_LENGTH);
203 let mut rng = Rng::new();
204
205 let mut droplets: Vec<Droplet> = (0..(NB_STREAMS * 11 / 10))
206 .map(|_| Droplet::new(&mut rng))
207 .collect();
208
209 GameLoop::loop_fixed(time::Duration::from_millis(30), &mut || {
210 canvas.clear();
211
212 for droplet in &mut droplets {
213 droplet.fall();
214 if droplet.has_fallen_out_of_screen() {
215 droplet.recycle(&mut rng);
216 }
217 droplet.maybe_glitch(&mut rng);
218 droplet.draw_onto(&mut canvas);
219 }
220
221 Some(canvas.to_string())
222 });
223}
224
225fn do_intro() {
226 let sleep = |duration| std::thread::sleep(time::Duration::from_millis(duration));
227
228 let mut game_loop = GameLoop::new();
229 game_loop.set_up();
230
231 let mut canvas = TextCanvas::new(NB_STREAMS, STREAM_LENGTH);
232 let mut rng = Rng::new_from_seed(42);
233
234 canvas.set_color(Color::new().bright_green());
235
236 for (x, c) in "Wake up, Neo...".chars().enumerate() {
238 canvas.draw_text(&c.to_string(), x as i32 + 3, 1);
239 game_loop.update(&canvas.to_string());
240 sleep(if c == ',' {
241 300
242 } else if c == ' ' {
243 100
244 } else {
245 50
246 });
247 }
248 sleep(2000);
249
250 canvas.clear();
252 for (x, c) in "The Matrix has you...".chars().enumerate() {
253 canvas.draw_text(&c.to_string(), x as i32 + 3, 1);
254 game_loop.update(&canvas.to_string());
255
256 sleep(if x < 3 {
257 400
258 } else {
259 u64::from(rng.urand_between(150, 300))
260 });
261 }
262 sleep(2000);
263
264 canvas.clear();
266 for (x, c) in "Follow the white rabbit.".chars().enumerate() {
267 canvas.draw_text(&c.to_string(), x as i32 + 3, 1);
268 game_loop.update(&canvas.to_string());
269
270 sleep(if x < 4 { 100 } else { 50 });
271 }
272 sleep(3000);
273
274 canvas.clear();
276 game_loop.update(&canvas.to_string());
277 sleep(70);
278 canvas.draw_text("Knock, knock, Neo.", 3, 1);
279 game_loop.update(&canvas.to_string());
280
281 sleep(4000);
285}