1use std::io;
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10use terminal_graphics_interactive::{Colour, Display, Event, MouseKind, Terminal};
11
12const FRAME: Duration = Duration::from_millis(33);
14
15const TRAIL_LENGTH: u8 = 12;
17
18const 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#[derive(Debug, PartialEq, Eq)]
32enum Action {
33 Continue,
34 Quit,
35}
36
37struct Dot {
38 x: f32,
39 y: f32,
40 dx: f32,
43 dy: f32,
44 colour: Colour,
45}
46
47struct Screensaver {
48 width: isize,
49 height: isize,
50 dots: Vec<Dot>,
51 trail: Vec<(Colour, u8)>,
54 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 fn random(&mut self) -> f32 {
75 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 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 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 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 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 _ => Action::Continue,
140 }
141 }
142
143 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
161fn 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 *position = position.clamp(0.0, max);
172}
173
174fn main() -> io::Result<()> {
175 let mut terminal = Terminal::start()?;
176
177 let (width, height) = terminal.size();
179 let mut display = Display::new(width, height.saturating_sub(1));
180
181 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 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 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 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}