Skip to main content

screensaver/
screensaver.rs

1//! A comet-trail screensaver. Dots bounce around the window leaving fading
2//! trails. Click to add another dot; press any key to quit.
3//!
4//! It shows the shape of a typical animation: a loop that draws a frame, then
5//! spends the time until the next frame handling input, then advances the
6//! animation. Run it with `cargo run --example screensaver`.
7
8use std::io;
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10use terminal_graphics_interactive::{Colour, Display, Event, MouseKind, Terminal};
11
12/// The time between frames: about 30 frames a second.
13const FRAME: Duration = Duration::from_millis(33);
14
15/// How many frames a trail lasts before it has faded away.
16const TRAIL_LENGTH: u8 = 12;
17
18/// The most dots on screen, so clicking cannot slow the animation forever.
19const MAX_DOTS: usize = 64;
20
21const COLOURS: [Colour; 6] = [
22    Colour::BrightRed,
23    Colour::BrightYellow,
24    Colour::BrightGreen,
25    Colour::BrightCyan,
26    Colour::BrightBlue,
27    Colour::BrightMagenta,
28];
29
30/// What the screensaver should do after an event.
31#[derive(Debug, PartialEq, Eq)]
32enum Action {
33    Continue,
34    Quit,
35}
36
37struct Dot {
38    x: f32,
39    y: f32,
40    /// Cells moved per frame. Terminal cells are about twice as tall as they
41    /// are wide, so the vertical speed is smaller to look the same.
42    dx: f32,
43    dy: f32,
44    colour: Colour,
45}
46
47struct Screensaver {
48    width: isize,
49    height: isize,
50    dots: Vec<Dot>,
51    /// For every cell: the colour of the dot that last passed, and how many
52    /// frames of trail it has left (0 means nothing there).
53    trail: Vec<(Colour, u8)>,
54    /// State for a tiny random number generator, so no crate is needed.
55    seed: u32,
56}
57
58impl Screensaver {
59    fn new(width: u32, height: u32, seed: u32) -> Screensaver {
60        let mut saver = Screensaver {
61            width: width as isize,
62            height: height as isize,
63            dots: Vec::new(),
64            trail: vec![(Colour::Black, 0); width as usize * height as usize],
65            seed: seed | 1,
66        };
67        for _ in 0..4 {
68            saver.add_dot(width as f32 / 2.0, height as f32 / 2.0);
69        }
70        saver
71    }
72
73    /// A pseudo random number from 0.0 up to (not including) 1.0.
74    fn random(&mut self) -> f32 {
75        // xorshift32
76        self.seed ^= self.seed << 13;
77        self.seed ^= self.seed >> 17;
78        self.seed ^= self.seed << 5;
79        (self.seed >> 8) as f32 / (1u32 << 24) as f32
80    }
81
82    /// The largest x and y a dot can have: the last cell of the window.
83    fn limits(&self) -> (f32, f32) {
84        (
85            (self.width - 1).max(0) as f32,
86            (self.height - 1).max(0) as f32,
87        )
88    }
89
90    /// Adds a dot at `(x, y)` (moved inside the window if it is outside)
91    /// moving in a random direction. Does nothing once there are `MAX_DOTS`.
92    fn add_dot(&mut self, x: f32, y: f32) {
93        if self.dots.len() >= MAX_DOTS {
94            return;
95        }
96        let (max_x, max_y) = self.limits();
97        let sign = |random: f32| if random < 0.5 { -1.0 } else { 1.0 };
98        let dx = (0.2 + 0.5 * self.random()) * sign(self.random());
99        let dy = (0.1 + 0.25 * self.random()) * sign(self.random());
100        self.dots.push(Dot {
101            x: x.clamp(0.0, max_x),
102            y: y.clamp(0.0, max_y),
103            dx,
104            dy,
105            colour: COLOURS[self.dots.len() % COLOURS.len()],
106        });
107    }
108
109    /// Moves every dot one frame, bounces them off the walls and fades trails.
110    fn step(&mut self) {
111        for (_, frames_left) in &mut self.trail {
112            *frames_left = frames_left.saturating_sub(1);
113        }
114
115        let (max_x, max_y) = self.limits();
116        for dot in &mut self.dots {
117            dot.x += dot.dx;
118            dot.y += dot.dy;
119            bounce(&mut dot.x, &mut dot.dx, max_x);
120            bounce(&mut dot.y, &mut dot.dy, max_y);
121
122            if self.width > 0 && self.height > 0 {
123                let index = dot.y.round() as usize * self.width as usize + dot.x.round() as usize;
124                self.trail[index] = (dot.colour, TRAIL_LENGTH);
125            }
126        }
127    }
128
129    /// Reacts to input: any key quits, and a click adds a dot.
130    fn handle(&mut self, event: &Event) -> Action {
131        match event {
132            Event::Key(_) => Action::Quit,
133            Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::Down(_)) => {
134                self.add_dot(mouse.x as f32, mouse.y as f32);
135                Action::Continue
136            }
137            Event::Mouse(_) => Action::Continue,
138            // pasted text is not a key press, and more kinds of event may come
139            _ => Action::Continue,
140        }
141    }
142
143    /// Draws the trails into `display`, fresher parts as denser blocks.
144    fn render(&self, display: &mut Display) {
145        for y in 0..self.height {
146            for x in 0..self.width {
147                let (colour, frames_left) = self.trail[(y * self.width + x) as usize];
148                let block = match frames_left {
149                    0 => ' ',
150                    n if n == TRAIL_LENGTH => '█',
151                    n if n * 3 >= TRAIL_LENGTH * 2 => '▓',
152                    n if n * 3 >= TRAIL_LENGTH => '▒',
153                    _ => '░',
154                };
155                display.set_pixel(x, y, block, colour, Colour::Black);
156            }
157        }
158    }
159}
160
161/// Keeps `position` between 0 and `max`, turning `speed` around at a wall.
162fn bounce(position: &mut f32, speed: &mut f32, max: f32) {
163    if *position < 0.0 {
164        *position = -*position;
165        *speed = -*speed;
166    } else if *position > max {
167        *position = 2.0 * max - *position;
168        *speed = -*speed;
169    }
170    // a window narrower than a step would otherwise throw the dot out
171    *position = position.clamp(0.0, max);
172}
173
174fn main() -> io::Result<()> {
175    let mut terminal = Terminal::start()?;
176
177    // `draw` needs a display at most one row shorter than the window
178    let (width, height) = terminal.size();
179    let mut display = Display::new(width, height.saturating_sub(1));
180
181    // any number will do as a seed; the clock makes each run different
182    let seed = SystemTime::now()
183        .duration_since(UNIX_EPOCH)
184        .map_or(1, |since| since.subsec_nanos())
185        ^ std::process::id();
186    let mut saver = Screensaver::new(display.width(), display.height(), seed);
187
188    let mut next_frame = Instant::now() + FRAME;
189    loop {
190        saver.render(&mut display);
191        terminal.draw(&display)?;
192
193        // spend the time until the next frame handling input
194        loop {
195            let wait = next_frame.saturating_duration_since(Instant::now());
196            match terminal.poll(wait)? {
197                Some(event) => {
198                    if saver.handle(&event) == Action::Quit {
199                        return Ok(());
200                    }
201                }
202                None => break,
203            }
204        }
205
206        saver.step();
207        // if we fell behind, do not try to catch up with a burst of frames
208        next_frame = (next_frame + FRAME).max(Instant::now());
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use terminal_graphics_interactive::{Key, KeyCode, Modifiers, Mouse, MouseButton};
216
217    /// A screensaver with no dots, so tests control everything.
218    fn empty(width: u32, height: u32) -> Screensaver {
219        let mut saver = Screensaver::new(width, height, 1);
220        saver.dots.clear();
221        saver
222    }
223
224    fn dot(x: f32, y: f32, dx: f32, dy: f32) -> Dot {
225        Dot {
226            x,
227            y,
228            dx,
229            dy,
230            colour: Colour::BrightRed,
231        }
232    }
233
234    fn click(x: isize, y: isize) -> Event {
235        Event::Mouse(Mouse {
236            kind: MouseKind::Down(MouseButton::Left),
237            x,
238            y,
239            modifiers: Modifiers::NONE,
240        })
241    }
242
243    fn cell(saver: &Screensaver, x: isize, y: isize) -> (Colour, u8) {
244        saver.trail[(y * saver.width + x) as usize]
245    }
246
247    #[test]
248    fn a_dot_moves_by_its_speed_each_step() {
249        let mut saver = empty(40, 20);
250        saver.dots.push(dot(10.0, 5.0, 0.5, 0.25));
251        saver.step();
252        assert_eq!((saver.dots[0].x, saver.dots[0].y), (10.5, 5.25));
253    }
254
255    #[test]
256    fn a_dot_bounces_off_each_wall() {
257        let mut saver = empty(10, 6);
258        saver.dots.push(dot(8.8, 2.0, 0.5, 0.0));
259        saver.step();
260        assert!(saver.dots[0].dx < 0.0, "right wall");
261        assert!(saver.dots[0].x <= 9.0);
262
263        let mut saver = empty(10, 6);
264        saver.dots.push(dot(0.2, 2.0, -0.5, 0.0));
265        saver.step();
266        assert!(saver.dots[0].dx > 0.0, "left wall");
267        assert!(saver.dots[0].x >= 0.0);
268
269        let mut saver = empty(10, 6);
270        saver.dots.push(dot(3.0, 0.1, 0.0, -0.4));
271        saver.step();
272        assert!(saver.dots[0].dy > 0.0, "top wall");
273        assert!(saver.dots[0].y >= 0.0);
274
275        let mut saver = empty(10, 6);
276        saver.dots.push(dot(3.0, 4.9, 0.0, 0.4));
277        saver.step();
278        assert!(saver.dots[0].dy < 0.0, "bottom wall");
279        assert!(saver.dots[0].y <= 5.0);
280    }
281
282    #[test]
283    fn dots_never_leave_the_window_however_long_it_runs() {
284        let mut saver = Screensaver::new(30, 10, 12345);
285        for _ in 0..5000 {
286            saver.step();
287            for dot in &saver.dots {
288                assert!((0.0..=29.0).contains(&dot.x), "x = {}", dot.x);
289                assert!((0.0..=9.0).contains(&dot.y), "y = {}", dot.y);
290            }
291        }
292    }
293
294    #[test]
295    fn the_cell_under_a_dot_gets_a_fresh_trail_that_then_fades_away() {
296        let mut saver = empty(20, 10);
297        saver.dots.push(dot(5.0, 5.0, 0.0, 0.0));
298        saver.step();
299        assert_eq!(cell(&saver, 5, 5), (Colour::BrightRed, TRAIL_LENGTH));
300
301        saver.dots.clear();
302        for step in 1..=TRAIL_LENGTH {
303            saver.step();
304            assert_eq!(cell(&saver, 5, 5).1, TRAIL_LENGTH - step);
305        }
306        saver.step();
307        assert_eq!(cell(&saver, 5, 5).1, 0, "it stays gone");
308    }
309
310    #[test]
311    fn a_moving_dot_leaves_a_trail_that_is_fresher_near_the_dot() {
312        let mut saver = empty(40, 10);
313        saver.dots.push(dot(2.0, 5.0, 1.0, 0.0));
314        for _ in 0..5 {
315            saver.step();
316        }
317        assert!(cell(&saver, 7, 5).1 > cell(&saver, 4, 5).1);
318        assert!(cell(&saver, 4, 5).1 > cell(&saver, 3, 5).1);
319    }
320
321    #[test]
322    fn clicking_adds_a_dot_there_and_the_count_is_capped() {
323        let mut saver = empty(40, 20);
324        assert_eq!(saver.handle(&click(7, 3)), Action::Continue);
325        assert_eq!(saver.dots.len(), 1);
326        assert_eq!((saver.dots[0].x, saver.dots[0].y), (7.0, 3.0));
327
328        for _ in 0..(MAX_DOTS * 2) {
329            saver.handle(&click(7, 3));
330        }
331        assert_eq!(saver.dots.len(), MAX_DOTS);
332    }
333
334    #[test]
335    fn a_click_outside_the_window_is_clamped_inside_it() {
336        let mut saver = empty(10, 6);
337        saver.handle(&click(500, -500));
338        assert_eq!((saver.dots[0].x, saver.dots[0].y), (9.0, 0.0));
339    }
340
341    #[test]
342    fn new_dots_get_a_direction_and_are_not_stuck() {
343        let mut saver = Screensaver::new(40, 20, 99);
344        assert_eq!(saver.dots.len(), 4);
345        for dot in &saver.dots {
346            assert!(dot.dx.abs() > 0.05 || dot.dy.abs() > 0.05, "a stuck dot");
347            assert!(dot.dx.abs() <= 1.0 && dot.dy.abs() <= 0.5);
348        }
349        let colours: std::collections::HashSet<_> = saver.dots.iter().map(|d| d.colour).collect();
350        assert!(colours.len() > 1, "all the same colour");
351        saver.add_dot(1.0, 1.0);
352    }
353
354    #[test]
355    fn any_key_quits_but_mouse_and_movement_do_not() {
356        let mut saver = empty(10, 6);
357        for key in [
358            KeyCode::Char('x'),
359            KeyCode::Enter,
360            KeyCode::Esc,
361            KeyCode::Up,
362        ] {
363            assert_eq!(
364                saver.handle(&Event::Key(Key::new(key))),
365                Action::Quit,
366                "{key:?}"
367            );
368        }
369        assert_eq!(saver.handle(&click(1, 1)), Action::Continue);
370        let scroll = Event::Mouse(Mouse {
371            kind: MouseKind::ScrollUp,
372            x: 0,
373            y: 0,
374            modifiers: Modifiers::NONE,
375        });
376        assert_eq!(saver.handle(&scroll), Action::Continue);
377    }
378
379    #[test]
380    fn pasted_text_neither_quits_nor_adds_dots() {
381        let mut saver = empty(10, 6);
382        assert_eq!(
383            saver.handle(&Event::Paste("anything".into())),
384            Action::Continue
385        );
386        assert!(saver.dots.is_empty());
387    }
388
389    #[test]
390    fn render_draws_fading_blocks_in_the_trail_colour_and_black_elsewhere() {
391        let mut saver = empty(20, 5);
392        saver.trail[0] = (Colour::BrightCyan, TRAIL_LENGTH);
393        saver.trail[1] = (Colour::BrightCyan, 1);
394        let mut display = Display::new(20, 5);
395        saver.render(&mut display);
396
397        let fresh = display.pixel(0, 0).unwrap();
398        assert_eq!(fresh.character(), '█');
399        assert_eq!(fresh.colour(), Colour::BrightCyan);
400        let fading = display.pixel(1, 0).unwrap();
401        assert_ne!(fading.character(), '█');
402        assert_ne!(fading.character(), ' ');
403        let empty_cell = display.pixel(2, 0).unwrap();
404        assert_eq!(empty_cell.character(), ' ');
405        assert_eq!(empty_cell.background(), Colour::Black);
406    }
407
408    #[test]
409    fn a_window_of_any_size_works_including_nothing() {
410        for (w, h) in [(0, 0), (0, 5), (5, 0), (1, 1), (2, 1)] {
411            let mut saver = Screensaver::new(w, h, 7);
412            let mut display = Display::new(w, h);
413            for _ in 0..50 {
414                saver.step();
415                saver.render(&mut display);
416            }
417            saver.handle(&click(3, 3));
418        }
419    }
420
421    #[test]
422    fn the_same_seed_gives_the_same_animation() {
423        let mut a = Screensaver::new(30, 10, 5);
424        let mut b = Screensaver::new(30, 10, 5);
425        for _ in 0..100 {
426            a.step();
427            b.step();
428        }
429        let positions = |s: &Screensaver| s.dots.iter().map(|d| (d.x, d.y)).collect::<Vec<_>>();
430        assert_eq!(positions(&a), positions(&b));
431    }
432}