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}