matrix/
matrix.rs

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/// One continuous text string.
38///
39/// Droplets run down the screen in a stream.
40#[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; // Just out-of-bounds, and some.
54
55        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        // Make one very fast at random. Doing it in `recycle()` instead
75        // of `new()` keeps the initial "curtain fall" homogeneous.
76        if rng.frand() > 0.99 {
77            self.speed = (self.speed * 2.0).max(1.3);
78        }
79    }
80
81    /// `self.y` as drawable integer.
82    ///
83    /// We keep the original `y` as fractional, it makes it easier to
84    /// modulate falling speed.
85    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            // No green chars visible.
106            return;
107        }
108
109        // `-3` to exclude tip and pre-tip.
110        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        // Start at `y=0`, NOT `droplet.y`. The droplet is already
129        // rendered, including spacing, etc.
130        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            // `merge_text()` ignores spaces.
134            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        // Use `self.x` and `self.length` to deterministically randomize
147        // bucket size and bright spots distribution.
148        let s = f64::from(self.x).sin().abs(); // [0; 1]
149        let d = f64::from(self.length).sin() - 0.3; // [-1.3; 0.7] (slightly skewed towards dim).
150
151        if f64::from(i_tip - i).sin() * s <= d {
152            // `sin(x) * S >= D`
153            // Deterministic way (`i_tip - i`) to modulate shade.
154            // `S` influences the size of the (base) buckets.
155            // `D` (`[-1; 1]`) affects the distribution. Higher means
156            //     bias towards bright, lower means bias towards dim,
157            //     `0.0` being neutral.
158            Shade::BrightGreen.into()
159        } else {
160            Shade::DimGreen.into()
161        }
162    }
163}
164
165impl fmt::Display for Droplet {
166    /// Render droplet as part of a stream, with leading and trailing whitespace.
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        // Not yet visible (above screen).
169        if self.iy() + self.length <= 0 {
170            return write!(f, "{}", " ".repeat(STREAM_LENGTH as usize));
171        }
172        // No longer visible (below screen).
173        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            // Equivalent to `&self.chars[window_start..=window_end]`, but with `chars()`.
184            &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    // Wake up, Neo...
237    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    // The Matrix has you...
251    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    // Follow the white rabbit.
265    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    // Knock, knock, Neo.
275    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    // Don't tear down.
282    // game_loop.tear_down();
283
284    sleep(4000);
285}