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}