mrs_matrix/
raindrop.rs

1//! Raindrop structure + implementation
2
3use rand::{self, Rng, rngs, seq::SliceRandom};
4use crossterm::style::{self, Stylize};
5
6use self::color_algorithms::ColorAlgorithm;
7
8pub mod charsets;
9pub mod color_algorithms;
10
11// shortest length a follower will be
12const FOLLOWER_MIN_LENGTH: u16 = 4;
13
14// the longest follower is the terminal height minus this offset
15const FOLLOWER_MAX_LENGTH_OFFSET: u16 = 4;
16
17// rows will start with a position offset from 0 by a value 
18// that is (pseudo)randomly selected from this range
19const START_OFFSET_RANGE: std::ops::RangeInclusive<i32> = -64..=-1;
20
21/// A `Raindrop` describes a single 'falling stream' of randomized characters
22/// 
23/// Raindrops consist of a 'leader' and a 'follower'.
24/// The leader is a continuously (per frame) randomized single character at the bottom of the raindrop.
25/// The follower is a string of characters that follow the leader. They have randomized length and content,
26/// but unlike leaders, are randomized only once (at instantiation) rather than continuously (per frame)
27pub struct Raindrop<'a, T>
28where T: ColorAlgorithm
29{
30    // follower_content is ordered such that index 0 represents
31    // the first char above the leader, index 1 represents the second, and so on
32    // note that Vec<char> is used instead of String; this is because we care about
33    // char-by-char indexing more than we care about the potential waste of 3 bytes per char
34    follower_content: Vec<char>,
35
36    // row index representing the terminal row that the leader is on
37    // the follower will be on indecies below this value
38    // note that this value may be negative or greater than the terminal height;
39    // this is why an i32 must be used instead of u16
40    row_index: i32,
41
42    // reference to a set of characters that will be selected from
43    // when generating pseudorandom characters
44    charset: &'a Vec<char>,
45
46    // probability of advancing position on any given frame,
47    // defaults to 1.0, but can be any value `n` where `0.0 < n <= 1.0`
48    advance_chance: f64,
49
50    // ColorAlgorithm implementor that is used to color follower chars
51    color_algorithm: T,
52
53    // locally cached random number generator
54    local_rng: rngs::ThreadRng
55}
56
57impl<'a, T> Raindrop<'a, T>
58where T: ColorAlgorithm
59{
60
61    /// Returns a (pseudo)randomly generated character from the internal charset
62    pub fn gen_char(&mut self) -> char 
63    {
64        *(self.charset.choose(&mut self.local_rng).unwrap())
65    }
66
67    /// Returns a new `Raindrop` instance
68    /// 
69    /// `charset` should be a reference to Vector of chars.
70    /// 
71    /// `color_algorithm` should implement 
72    /// [ColorAlgorithm](crate::raindrop::color_algorithms::ColorAlgorithm). It defines
73    /// how follower characters will be colored.
74    /// 
75    /// `advance_chance` is the chance that, on any given frame, this `Raindrop` will 
76    /// advance its animation. This can be any real number within the range `[0.0, 1.0)`.
77    /// If the `advance_chance` is 1.0, this `Raindrop` will always advance its animation.
78    /// 
79    /// `terminal_height` should be the current height of the terminal, in rows.
80    /// 
81    ///# Panics
82    /// 
83    /// This function panics if `advance_chance` is outside the range `[0.0, 1.0)`
84    /// 
85    ///# Examples
86    /// ```
87    /// use mrs_matrix::raindrop::{Raindrop, color_algorithms};
88    /// use crossterm::terminal;
89    /// 
90    /// let charset = vec!['a','b', 'c'];
91    /// 
92    /// let color_algorithm = color_algorithms::LightnessDescending{
93    ///     hue: 118.0,
94    ///     saturation: 0.82
95    /// };
96    /// 
97    /// let advance_chance = 0.75;
98    /// 
99    /// let term_height = terminal::size().unwrap().1;
100    /// 
101    /// let new_raindrop_instance = Raindrop::new(&charset, color_algorithm, advance_chance, term_height);
102    /// // do something with instance
103    /// ```
104    pub fn new(charset: &'a Vec<char>, color_algorithm: T, advance_chance: f64, terminal_height: u16) -> Self
105    {
106        
107        assert!(advance_chance > 0.0, "Attempted to set advance chance at 0 or below");
108        assert!(advance_chance <= 1.0, "Attempted to set advance chance greater than 1");
109
110        // create a new `Raindrop` instance
111        // use an empty vector for follower content and a zero for row index;
112        // these will be overwritten by the call to reinit_state; in fact they could safely be null
113        // if rust had a null type
114        let mut new_instance  = Self {
115            charset,
116            color_algorithm,
117            local_rng: rand::thread_rng(),
118            follower_content: Vec::new(),
119            row_index: 0,
120            advance_chance
121        };
122
123        // do the work of initializing the state of the raindrop;
124        // setting its follower_content and row_index pseudorandomly
125        new_instance.reinit_state(terminal_height);
126
127        // return the newly created and initialized instance
128        new_instance
129    }
130
131    /// Re-initializes the state of the `Raindrop` instance 
132    /// 
133    /// Uses an internally cached random number generator to generate
134    /// pseudorandom follower chars and sets the row index to a pseudorandom value
135    /// less than (visually 'above') row 0.
136    /// 
137    /// `terminal_height` should be the current height of the terminal, in rows
138    /// 
139    /// # Notes
140    /// 
141    /// The [Raindrop::new](crate::raindrop::Raindrop::new) function uses this function internally
142    /// to set the initial state. Calling this function manually is similar to creating
143    /// a new `Raindrop` instance outright, but avoids the need to create a new [Rng].
144    pub fn reinit_state(&mut self, terminal_height: u16)
145    {
146        // determine max follower length by subtracting offset from current terminal height
147        let max_follower_length = terminal_height.saturating_sub(FOLLOWER_MAX_LENGTH_OFFSET)
148        // ensure max follower length is at least FOLLOWER_MIN_LENGTH + 1
149        .max(FOLLOWER_MIN_LENGTH + 1);
150 
151        // use rng to generate follower_content and row_index
152        // first determine follower length
153        let follower_length = self.local_rng.gen_range(FOLLOWER_MIN_LENGTH..=max_follower_length);
154 
155        // create empty vector with capacity great enough to hold all follower chars
156        let mut new_follower_content = Vec::with_capacity(follower_length.into());
157         
158        // generate follower_length chars and place them in new_follower_content vec
159        for _ in 0..follower_length{
160            new_follower_content.push(self.gen_char());
161        }
162
163        // store new follower content
164        // this needs to be done seperately from using self.gen_char 
165        // to satisfy the borrow checker (as both self.follower_length.push 
166        // and self.gen_char mutably borrow self)
167        self.follower_content = new_follower_content;
168 
169        // generate and store new row index value
170        // this can be done in a single step
171        self.row_index = self.local_rng.gen_range(START_OFFSET_RANGE); 
172 
173        // don't return anything
174    }
175
176    /// Returns the character that should be printed for a given row
177    /// 
178    /// # Notes
179    /// 
180    /// This function returns an [Option](Option). When requesting a char for a
181    /// row that this instance has no char for (for example, because this raindrop 
182    /// is above the provided row), `None` will be returned.
183    /// If this instance does have a char for the provided row, `Some(char)` is returned.
184    pub fn get_char_at_row(&mut self, row_index: u16) -> Option<char>
185    {
186        
187        // cast provided row index to i32 and bind to a more clear name
188        // we only want to accept valid u16 values, but need the value to be an i32 for
189        // comparisons and math with self.row_index
190        let provided_row_index: i32 = row_index.into();
191
192        // return None immediately if provided row is beyond this Raindrop's row
193        if self.row_index < provided_row_index{
194            return None;
195        }
196        
197        // return a randomly selected char if provided row index points to the leader of this Raindrop
198        // (i.e. if the provided row index and current row index match exactly)
199        if self.row_index == provided_row_index {
200            return Some(self.gen_char());
201        }
202
203        // we already checked if provided row index was greater than row index
204        // and if provided row index was equal to row index,
205        // so if we reach this point, provided row index must be less than row index
206
207        // find the index within follower_content that provided_row_index should point to,
208        // keeping min mind that follower starts 1 row above (less than) row_index
209        match TryInto::<usize>::try_into((self.row_index - 1) - provided_row_index) 
210        {
211            Err(_) => {
212                //if follower_index can't be represented as a usize for whatever reason,
213                //print a warning to stderr and return None
214                eprintln!("Failed to represent follower_index ({}) as a usize; skipping char", 
215                    (self.row_index - 1) - provided_row_index);
216                return None
217            },
218            Ok(follower_index) => {
219                //return either the char at the follower index, or None if there isn't one
220                match self.follower_content.get(follower_index) {
221                    Some(&val) => Some(val),
222                    None => None
223                }
224            }
225        }
226        
227    }
228
229    /// Returns the character that should be printed for a given row with appropriate styling
230    /// 
231    /// Internally, uses `get_styled_char` to retrieve the actual character. Then applies a color
232    /// according to this `Raindrop`'s `color_algorithm`
233    /// 
234    /// The leader of the raindrop will always be styled white (and bolded).
235    pub fn get_styled_char_at_row(&mut self, row_index: u16) -> Option<style::StyledContent<char>>
236    {
237        match self.get_char_at_row(row_index){
238            //if get_char_at_row returns None, return None immediately
239            None => None,
240            Some(unstyled_char) => {
241                
242                
243                if self.row_index == row_index.into() {
244                    //if char is the leader, style as white (and bold)
245                    Some(unstyled_char.with(style::Color::White)
246                    .attribute(style::Attribute::Bold))
247                } else {
248                    //calculate follower proportion from position_in_follower and follower_length
249                    let position_in_follower = ((self.row_index - 1) - (row_index as i32)) as f32;
250                    let follower_length: f32 = self.follower_content.len() as f32;
251
252                    let follower_proportion = (position_in_follower/follower_length).min(1.0).max(0.0);
253                    
254                    let char_color = 
255                        self.color_algorithm.gen_color(follower_proportion);
256                    
257                    Some(unstyled_char.with(char_color.into()))
258                }
259            }
260        } 
261    }
262
263    /// Moves the `Raindrop` down one row.
264    /// 
265    /// To reset to the top, use [reinit_state](crate::raindrop::Raindrop::reinit_state).
266    pub fn move_drop(&mut self)
267    {
268        self.row_index += 1;
269    }
270
271    /// Returns `true` if Raindrop displays any chars on a terminal of height `terminal_height`; `false` otherwise
272    pub fn is_visible(&self, terminal_height: u16) -> bool
273    {
274
275        // if row_index is less than zero, return false immediately
276        if self.row_index < 0 {
277            return false;
278        }
279
280        self.row_index < (terminal_height as i32) + (self.follower_content.len() as i32)
281
282    }
283
284    /// Advance the `Raindrop` by one 'frame'
285    /// 
286    /// `terminal_height` should be the current height of the terminal, in rows.
287    /// 
288    /// This is similar to [move_drop](crate::raindrop::Raindrop::move_drop), with two key differences:
289    /// - If the `Raindrop` is not visible because it has fallen down below the bottom of the terminal,
290    /// [reinit_state](crate::raindrop::Raindrop::reinit_state) is called to re-randomize the `Raindrop` and
291    /// move it slightly above the top of the terminal.
292    /// 
293    /// - If the `Raindrop` has had its `advance_chance` set to some value that is not 1.0, this function
294    /// will only have a chance of advancing this raindrop's position. If you want to move the `Raindrop` 
295    /// for certain, use the [move_drop](crate::raindrop::Raindrop::move_drop) method
296    pub fn advance_animation(&mut self, terminal_height: u16)
297    {
298        // only perform visibility check if current row is not less than 0
299        // if we didn't make this check conditional, advance_animation would continuously call reinit_state
300        // as raindrops always start above row 0 but are never visible until they reach row 0
301        if !(self.row_index < 0) {
302            if !self.is_visible(terminal_height){
303                self.reinit_state(terminal_height);
304                return;
305            }
306        }
307        
308        if self.advance_chance == 1.0 {
309            // unconditionally move if advance_chance is 1.0, skipping an uneeded rng call
310            self.move_drop();
311        }
312        else if self.local_rng.gen_bool(self.advance_chance) {
313            // if advance_chance is not 1.0, perform rng call to decide whether to move
314            self.move_drop();
315        }
316       
317    }
318
319}