tui_rain/lib.rs
1#![doc = include_str!("../README.md")]
2
3use std::{cmp::Ordering, time::Duration};
4
5use rand::{RngCore, SeedableRng};
6use rand_pcg::Pcg64Mcg;
7use ratatui::{
8 buffer::Buffer,
9 layout::Rect,
10 style::{Color, Style, Stylize},
11 widgets::Widget,
12};
13
14/// A configuration for the density of the rain effect.
15#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
16pub enum RainDensity {
17 /// An absolute target number of drops to have in the frame.
18 Absolute { num_drops: usize },
19
20 /// Compute the number of drops based on the frame size. Lower value is denser.
21 ///
22 /// Is converted to an absolute value, with 1 drop per `sparseness` pixels.
23 Relative { sparseness: usize },
24
25 /// A dense rain. Equivalent to `Relative { sparseness: 20 }`.
26 Dense,
27
28 /// A normal rain. Equivalent to `Relative { sparseness: 50 }`.
29 Normal,
30
31 /// A sparse rain. Equivalent to `Relative { sparseness: 100 }`.
32 Sparse,
33}
34
35impl RainDensity {
36 /// Get the absolute number of drops given an area.
37 fn num_drops(&self, area: Rect) -> usize {
38 match self {
39 RainDensity::Absolute { num_drops } => *num_drops,
40 RainDensity::Relative { sparseness } if *sparseness == 0 => 0,
41 RainDensity::Relative { sparseness } => {
42 (area.width * area.height) as usize / *sparseness
43 }
44 RainDensity::Dense => RainDensity::Relative { sparseness: 20 }.num_drops(area),
45 RainDensity::Normal => RainDensity::Relative { sparseness: 50 }.num_drops(area),
46 RainDensity::Sparse => RainDensity::Relative { sparseness: 100 }.num_drops(area),
47 }
48 }
49}
50
51/// The speed of the rain.
52#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
53pub enum RainSpeed {
54 /// An absolute target speed in pixels / second.
55 Absolute { speed: f64 },
56
57 /// A fast rain. Equivalent to `Absolute { speed: 20.0 }`.
58 Fast,
59
60 /// A normal rain. Equivalent to `Absolute { speed: 10.0 }`.
61 Normal,
62
63 /// A slow rain. Equivalent to `Absolute { speed: 5.0 }`.
64 Slow,
65}
66
67impl RainSpeed {
68 /// Get the absolute speed.
69 fn speed(&self) -> f64 {
70 match self {
71 RainSpeed::Absolute { speed } => *speed,
72 RainSpeed::Fast => 20.0,
73 RainSpeed::Normal => 10.0,
74 RainSpeed::Slow => 5.0,
75 }
76 }
77}
78
79/// A character set for the rain.
80#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
81pub enum CharacterSet {
82 /// An explicit enumeration of character options. This is the least performant.
83 Explicit { options: Vec<char> },
84
85 /// A range of unicode values.
86 UnicodeRange { start: u32, len: u32 },
87
88 /// Half-width Japanese Kana characters. This is the closest to the original.
89 ///
90 /// Equivalent to `CharacterSet::UnicodeRange { start: 0xFF66, len: 56 }`.
91 HalfKana,
92
93 /// The lowercase English alphabet.
94 ///
95 /// Equivalent to `CharacterSet::UnicodeRange { start: 0x61, len: 26 }`.
96 Lowercase,
97}
98
99impl CharacterSet {
100 fn get(&self, seed: u32) -> char {
101 match self {
102 CharacterSet::Explicit { options } => options[seed as usize % options.len()],
103 CharacterSet::UnicodeRange { start, len } => {
104 char::from_u32((seed % len) + start).unwrap()
105 }
106 CharacterSet::HalfKana => CharacterSet::UnicodeRange {
107 start: 0xFF66,
108 len: 56,
109 }
110 .get(seed),
111 CharacterSet::Lowercase => CharacterSet::UnicodeRange {
112 start: 0x61,
113 len: 26,
114 }
115 .get(seed),
116 }
117 }
118
119 fn size(&self) -> usize {
120 match self {
121 CharacterSet::Explicit { options } => options.len(),
122 CharacterSet::UnicodeRange { start: _, len } => *len as usize,
123 CharacterSet::HalfKana => 56,
124 CharacterSet::Lowercase => 26,
125 }
126 }
127}
128
129#[derive(Clone, PartialEq, Debug)]
130pub struct Rain {
131 elapsed: Duration,
132 seed: u64,
133 rain_density: RainDensity,
134 rain_speed: RainSpeed,
135 rain_speed_variance: f64,
136 tail_lifespan: Duration,
137 color: Color,
138 head_color: Color,
139 bold_dim_effect: bool,
140 noise_interval: Duration,
141 character_set: CharacterSet,
142}
143
144impl Rain {
145 /// Construct a new rain widget with defaults for matrix rain.
146 pub fn new_matrix(elapsed: Duration) -> Rain {
147 Rain {
148 elapsed,
149 seed: 1234,
150 rain_density: RainDensity::Normal,
151 rain_speed: RainSpeed::Slow,
152 rain_speed_variance: 0.5,
153 tail_lifespan: Duration::from_secs(2),
154 color: Color::LightGreen,
155 head_color: Color::White,
156 bold_dim_effect: true,
157 noise_interval: Duration::from_secs(5),
158 character_set: CharacterSet::HalfKana,
159 }
160 }
161
162 /// Construct a new rain widget with defaults for standard rain.
163 pub fn new_rain(elapsed: Duration) -> Rain {
164 Rain {
165 elapsed,
166 seed: 1234,
167 rain_density: RainDensity::Dense,
168 rain_speed: RainSpeed::Fast,
169 rain_speed_variance: 0.5,
170 tail_lifespan: Duration::from_millis(250),
171 color: Color::LightBlue,
172 head_color: Color::White,
173 bold_dim_effect: true,
174 noise_interval: Duration::from_secs(1),
175 character_set: CharacterSet::UnicodeRange {
176 start: 0x7c,
177 len: 1,
178 },
179 }
180 }
181
182 /// Construct a new rain widget with defaults for snow.
183 pub fn new_snow(elapsed: Duration) -> Rain {
184 Rain {
185 elapsed,
186 seed: 1234,
187 rain_density: RainDensity::Dense,
188 rain_speed: RainSpeed::Absolute { speed: 2.0 },
189 rain_speed_variance: 0.1,
190 tail_lifespan: Duration::from_millis(500),
191 color: Color::White,
192 head_color: Color::White,
193 bold_dim_effect: true,
194 noise_interval: Duration::from_secs(1),
195 character_set: CharacterSet::UnicodeRange {
196 start: 0x2a,
197 len: 1,
198 },
199 }
200 }
201
202 /// Construct a new rain widget with defaults for emoji soup.
203 ///
204 /// Terminals that render emojis as two characters wide will not enjoy this.
205 pub fn new_emoji_soup(elapsed: Duration) -> Rain {
206 Rain {
207 elapsed,
208 seed: 1234,
209 rain_density: RainDensity::Dense,
210 rain_speed: RainSpeed::Normal,
211 rain_speed_variance: 0.1,
212 tail_lifespan: Duration::from_millis(500),
213 color: Color::White,
214 head_color: Color::White,
215 bold_dim_effect: true,
216 noise_interval: Duration::from_secs(1),
217 character_set: CharacterSet::UnicodeRange {
218 start: 0x1f600,
219 len: 80,
220 },
221 }
222 }
223
224 /// Set the random seed for the generation.
225 ///
226 /// The random seed can be configured. Given a constant screen size, results should
227 /// be reproducible across executions, operating systems, and architectures.
228 ///
229 /// ```
230 /// use std::time::Duration;
231 /// use tui_rain::Rain;
232 ///
233 /// let elapsed = Duration::from_secs(5);
234 ///
235 /// Rain::new_matrix(elapsed)
236 /// .with_seed(1234);
237 /// ```
238 pub fn with_seed(mut self, seed: u64) -> Rain {
239 self.seed = seed;
240 self
241 }
242
243 /// Set the target density for the rain.
244 ///
245 /// This can be configured as an absolute number of drops:
246 ///
247 /// ```
248 /// use std::time::Duration;
249 /// use tui_rain::{Rain, RainDensity};
250 ///
251 /// Rain::new_matrix(Duration::from_secs(0))
252 /// .with_rain_density(RainDensity::Absolute {
253 /// num_drops: 100,
254 /// });
255 /// ```
256 /// Or a ratio of screen pixels to drops (lower is more dense):
257 ///
258 /// ```
259 /// use std::time::Duration;
260 /// use tui_rain::{Rain, RainDensity};
261 ///
262 /// Rain::new_matrix(Duration::from_secs(0))
263 /// .with_rain_density(RainDensity::Relative {
264 /// sparseness: 50,
265 /// });
266 /// ```
267 ///
268 /// The actual number of drops on the screen at any time is randomly distributed
269 /// between 0 and twice the target.
270 ///
271 /// Preset relative options include:
272 ///
273 /// - `RainDensity::Sparse`
274 /// - `RainDensity::Normal`
275 /// - `RainDensity::Dense`
276 pub fn with_rain_density(mut self, rain_density: RainDensity) -> Rain {
277 self.rain_density = rain_density;
278 self
279 }
280
281 /// Set the target speed for the rain.
282 ///
283 /// Speed can be configured as an absolute value of pixels per second, or as a
284 /// preset.
285 ///
286 /// For an absolute speed in pixels per second:
287 ///
288 /// ```
289 /// use std::time::Duration;
290 /// use tui_rain::{Rain, RainSpeed};
291 ///
292 /// let elapsed = Duration::from_secs(5);
293 ///
294 /// Rain::new_matrix(elapsed)
295 /// .with_rain_speed(RainSpeed::Absolute {
296 /// speed: 10.0,
297 /// });
298 /// ```
299 ///
300 /// Preset options include:
301 ///
302 /// - `RainSpeed::Slow`
303 /// - `RainSpeed::Normal`
304 /// - `RainSpeed::Fast`
305 pub fn with_rain_speed(mut self, rain_speed: RainSpeed) -> Rain {
306 self.rain_speed = rain_speed;
307 self
308 }
309
310 /// Set the rain speed variance.
311 ///
312 /// To avoid perfectly consistent patterns, you can configure some variance in the
313 /// speed of each drop. This can also give an impression of parallax (depth).
314 ///
315 /// For example, a value of `0.1` will cause each drop's speed to be uniformly
316 /// distrbuted within ±10% of the target speed:
317 ///
318 /// ```
319 /// use std::time::Duration;
320 /// use tui_rain::Rain;
321 ///
322 /// let elapsed = Duration::from_secs(5);
323 ///
324 /// Rain::new_matrix(elapsed)
325 /// .with_rain_speed_variance(0.1);
326 /// ```
327 ///
328 /// The speed of an individual drop will never go below 0.001 pixels / second, but
329 /// can vary arbitrarily high.
330 pub fn with_rain_speed_variance(mut self, rain_speed_variance: f64) -> Rain {
331 self.rain_speed_variance = rain_speed_variance;
332 self
333 }
334
335 /// Set the tail lifespan for the rain.
336 ///
337 /// You can make the rain drop tails appear shorter / longer by configuring how long
338 /// the tail effect lasts:
339 ///
340 /// ```
341 /// use std::time::Duration;
342 /// use tui_rain::Rain;
343 ///
344 /// let elapsed = Duration::from_secs(5);
345 ///
346 /// Rain::new_matrix(elapsed)
347 /// .with_tail_lifespan(Duration::from_secs(5));
348 /// ```
349 ///
350 /// The drop length is capped at the screen height to avoid strange wraparound
351 /// effects.
352 pub fn with_tail_lifespan(mut self, tail_lifespan: Duration) -> Rain {
353 self.tail_lifespan = tail_lifespan;
354 self
355 }
356
357 /// Set the color for the rain.
358 ///
359 /// You can change the tail color for each drop:
360 ///
361 /// ```
362 /// use std::time::Duration;
363 /// use tui_rain::Rain;
364 ///
365 /// let elapsed = Duration::from_secs(5);
366 ///
367 /// Rain::new_matrix(elapsed)
368 /// .with_color(ratatui::style::Color::LightGreen);
369 /// ```
370 ///
371 /// The color of the head is [independently configured](Rain::with_head_color). The
372 /// bold / dim effects that automatically get applied over a drop's length may tweak
373 /// the color inadvertently, but [this can be disabled](Rain::with_bold_dim_effect).
374 pub fn with_color(mut self, color: Color) -> Rain {
375 self.color = color;
376 self
377 }
378
379 /// Set the head color for the rain.
380 ///
381 /// You can change the head color for each drop:
382 ///
383 /// ```
384 /// use std::time::Duration;
385 /// use tui_rain::Rain;
386 ///
387 /// let elapsed = Duration::from_secs(5);
388 ///
389 /// Rain::new_matrix(elapsed)
390 /// .with_head_color(ratatui::style::Color::Green);
391 /// ```
392 ///
393 /// The color of the tail is [independently configured](Rain::with_color). The
394 /// bold / dim effects that automatically get applied over a drop's length may tweak
395 /// the color inadvertently, but [this can be disabled](Rain::with_bold_dim_effect).
396 pub fn with_head_color(mut self, head_color: Color) -> Rain {
397 self.head_color = head_color;
398 self
399 }
400
401 /// Set whether to apply the bold / dim effect.
402 ///
403 /// By default, the lower third of each drop has the bold effect applied, and the
404 /// upper third has the dim effect applied. This produces an impression of the drop
405 /// fading instead of abruptly ending.
406 ///
407 /// This may tweak the color of glyphs away from the base color on some terminals,
408 /// so it can be disabled if desired:
409 ///
410 /// ```
411 /// use std::time::Duration;
412 /// use tui_rain::Rain;
413 ///
414 /// let elapsed = Duration::from_secs(5);
415 ///
416 /// Rain::new_matrix(elapsed)
417 /// .with_bold_dim_effect(false);
418 ///```
419 pub fn with_bold_dim_effect(mut self, bold_dim_effect: bool) -> Rain {
420 self.bold_dim_effect = bold_dim_effect;
421 self
422 }
423
424 /// Set the interval between random character changes.
425 ///
426 /// A more subtle effect is that glyphs already rendered in a drop occasionally
427 /// switch characters before dissapearing. The time interval between each character
428 /// switch is per-glyph, and can be adjusted:
429 ///
430 /// ```
431 /// use std::time::Duration;
432 /// use tui_rain::Rain;
433 ///
434 /// let elapsed = Duration::from_secs(5);
435 ///
436 /// Rain::new_matrix(elapsed)
437 /// .with_noise_interval(Duration::from_secs(10));
438 /// ```
439 pub fn with_noise_interval(mut self, noise_interval: Duration) -> Rain {
440 self.noise_interval = noise_interval;
441 self
442 }
443
444 /// Set the character set for the drops.
445 ///
446 /// The simplest option is to provide an explicit set of characters to choose from:
447 ///
448 /// ```
449 /// use std::time::Duration;
450 /// use tui_rain::{CharacterSet, Rain};
451 ///
452 /// let elapsed = Duration::from_secs(5);
453 ///
454 /// Rain::new_matrix(elapsed)
455 /// .with_character_set(CharacterSet::Explicit {
456 /// options: vec!['a', 'b', 'c'],
457 /// });
458 /// ```
459 ///
460 /// More performant is to provide a unicode range:
461 ///
462 /// ```
463 /// use std::time::Duration;
464 /// use tui_rain::{CharacterSet, Rain};
465 ///
466 /// let elapsed = Duration::from_secs(5);
467 ///
468 /// Rain::new_matrix(elapsed)
469 /// .with_character_set(CharacterSet::UnicodeRange {
470 /// start: 0x61,
471 /// len: 26,
472 /// });
473 /// ```
474 ///
475 /// Preset unicode ranges include:
476 ///
477 /// - `CharacterSet::HalfKana` is the half-width Japanese kana character set (used
478 /// in the classic matrix rain)
479 /// - `CharacterSet::Lowercase` is the lowercase English character set
480 pub fn with_character_set(mut self, character_set: CharacterSet) -> Rain {
481 self.character_set = character_set;
482 self
483 }
484
485 /// Build the rng. Uses a fast but portable and reproducible rng.
486 fn build_rng(&self) -> impl RngCore {
487 Pcg64Mcg::seed_from_u64(self.seed)
488 }
489
490 /// Build a drop from the given consistent initial entropy state.
491 ///
492 /// The entropy vector's length becomes the drop's track length, so ensure it's at
493 /// least the window height.
494 fn build_drop(&self, entropy: Vec<u64>, width: u16, height: u16) -> Vec<Glyph> {
495 let elapsed = self.elapsed.as_secs_f64();
496 let rain_speed = self.rain_speed.speed();
497 let tail_lifespan = self.tail_lifespan.as_secs_f64();
498 let noise_interval = self.noise_interval.as_secs_f64();
499
500 // A single drop can expect to be called with the exact same entropy vec on each
501 // frame. This means we can sample the entropy vec to reproducibly generate
502 // features every frame (e.g. speed).
503
504 // Later code assumes at least 1 entry in the entropy vec, so break early if not.
505 if entropy.is_empty() {
506 return vec![];
507 }
508
509 // The length of the entropy vec becomes the length of the drop's track.
510 // This track is usually longer than the screen height by a random amount.
511 let track_len = entropy.len() as u16;
512
513 // Use some entropy to compute the drop's actual speed.
514 // n.b. since the entropy vec is stable, the drop's speed will not vary over time.
515 let rain_speed = uniform(
516 entropy[0],
517 rain_speed * (1.0 - self.rain_speed_variance),
518 rain_speed * (1.0 + self.rain_speed_variance),
519 )
520 .max(1e-3); // Prevent speed from hitting 0 (if user specifies high variance)
521
522 // Compute how long our drop will take to make 1 cycle given our track len and speed
523 let cycle_time_secs = entropy.len() as f64 / rain_speed;
524
525 // Use some entropy to compute a stable random time offset for this drop.
526 // If this value were 0, every drop would start falling with an identical y value.
527 let initial_cycle_offset_secs = uniform(entropy[0], 0.0, cycle_time_secs);
528
529 // Compute how far we are into the current cycle and current drop head height.
530 let current_cycle_offset_secs = (elapsed + initial_cycle_offset_secs) % cycle_time_secs;
531 let head_y = (current_cycle_offset_secs * rain_speed) as u16;
532
533 // Compute drop length given speed and tail lifespan.
534 // Cap at screen height to avoid weird wraparound when tail length is long.
535 let drop_len = ((rain_speed * tail_lifespan) as u16).min(height);
536
537 // Render each glyph in the drop.
538 (0..drop_len)
539 .filter_map(|y_offset| {
540 // Compute how long ago this glyph would have first appeared
541 let age = y_offset as f64 / rain_speed;
542
543 // If it would have first appeared before the rendering began, don't render.
544 if age > elapsed {
545 return None;
546 }
547
548 // Compute which cycle this particular glyph is a member of
549 let cycle_num =
550 ((elapsed + initial_cycle_offset_secs - age) / cycle_time_secs) as usize;
551
552 // Don't render glyphs from cycle 0
553 // (prevents drops from appearing to spawn in the middle of the screen)
554 if cycle_num == 0 {
555 return None;
556 }
557
558 // Get stable entropy to decide what column cycle X is rendered in.
559 // This must be per-glyph to prevent drops from jumping side-to-side when they wrap around.
560 let x_entropy = entropy[cycle_num % entropy.len()];
561 let x = (x_entropy % width as u64) as u16;
562
563 // Compute the y value for this glyph, and don't render if off the screen.
564 let y = (head_y + track_len - y_offset) % track_len;
565 if y >= height {
566 return None;
567 }
568
569 // The 'noise' of glyphs randomly changing is actually modeled as every glyph in the track
570 // just cycling through possible values veeeery slowly. We need a random offset for this
571 // cycling so every glyph doesn't change at the same time.
572 let time_offset = uniform(
573 entropy[y as usize],
574 0.0,
575 noise_interval * self.character_set.size() as f64,
576 );
577
578 // Decide what character is rendered based on noise.
579 let content = self
580 .character_set
581 .get(((time_offset + elapsed) / noise_interval) as u32);
582
583 // Compute the styling for the glyph
584 let mut style = Style::default();
585
586 // Color appropriately depending on whether this glyph is the head.
587 if age > 0.0 {
588 style = style.fg(self.color)
589 } else {
590 style = style.fg(self.head_color)
591 }
592
593 // The lowest third of glyphs is bold, the highest third is dim
594 if self.bold_dim_effect {
595 if y_offset < drop_len / 3 {
596 style = style.bold().not_dim()
597 } else if y_offset > drop_len * 2 / 3 {
598 style = style.dim().not_bold()
599 } else {
600 style = style.not_bold().not_dim()
601 }
602 }
603
604 Some(Glyph {
605 x,
606 y,
607 age,
608 content,
609 style,
610 })
611 })
612 .collect()
613 }
614}
615
616impl Widget for Rain {
617 fn render(self, area: Rect, buf: &mut Buffer) {
618 let mut rng = self.build_rng();
619
620 // We don't actually have n drops with tracks equal to the screen height.
621 // We actually have 2n drops with tracks ranging from 1.5 to 2.5 the screen height.
622 // This introduces more randomness to the apparent n and reduces cyclic appearance.
623 let num_drops = self.rain_density.num_drops(area) * 2;
624 let drop_track_lens: Vec<usize> = (0..num_drops)
625 .map(|_| (area.height as u64 * 3 / 2 + rng.next_u64() % area.height as u64) as usize)
626 .collect();
627
628 // We construct entropy consistently every frame to mimic statefulness.
629 // This is not a performance bottleneck, so caching wouldn't deliver much benefit.
630 let entropy: Vec<Vec<u64>> = drop_track_lens
631 .iter()
632 .map(|track_len| (0..*track_len).map(|_| rng.next_u64()).collect())
633 .collect();
634
635 // For every entropy vec, construct a single drop (vertical line of glyphs).
636 let mut glyphs: Vec<Glyph> = entropy
637 .into_iter()
638 .flat_map(|drop_entropy| self.build_drop(drop_entropy, area.width, area.height))
639 .collect();
640
641 // Sort all the glyphs by age so drop heads always render on top.
642 // This is a moderate bottleneck when the screen is large / there's a lot of glyphs.
643 glyphs.sort_by(|a, b| a.age.partial_cmp(&b.age).unwrap_or(Ordering::Equal));
644
645 // Actually render to the buffer.
646 for glyph in glyphs {
647 buf[(glyph.x, glyph.y)].set_char(glyph.content);
648 buf[(glyph.x, glyph.y)].set_style(glyph.style);
649 }
650 }
651}
652
653/// A Glyph to be rendered on the screen.
654struct Glyph {
655 x: u16,
656 y: u16,
657 age: f64,
658 content: char,
659 style: Style,
660}
661
662/// Map a uniform random u64 to a uniform random f64 in the range [lower, upper).
663fn uniform(seed: u64, lower: f64, upper: f64) -> f64 {
664 (seed as f64 / u64::MAX as f64) * (upper - lower) + lower
665}