mrs_matrix/
animation.rs

1//! Functions relating directly to drawing animations on the screen
2//! 
3use std::io::{stdout, Write};
4use std::time::{Instant, Duration};
5use crossterm::{
6    self,
7    event::{self, Event},
8    QueueableCommand, 
9    style::{Print, PrintStyledContent},
10    terminal,
11    cursor
12};
13use crate::raindrop::{Raindrop, color_algorithms::ColorAlgorithm};
14
15/// Returns a `Vec<Raindrop>` with one `Raindrop` for each terminal column
16/// 
17/// `charset` should be a reference to a Vector of chars. This will be the set of 
18/// characters that the raindrops will be generated from.
19/// 
20/// `advance_chance` is the chance that a `Raindrop` will advance on any given frame.
21/// 
22/// `terminal_width` should be the width of the terminal in columns
23/// 
24/// `terminal_height` should be the height of the terminal in rows
25/// 
26/// Note that this function is intentionally private because it's unlikely to be generally useful
27fn create_raindrops<T>(charset: &Vec<char>, color_algorithm: T, 
28    advance_chance:f64, terminal_width: u16, terminal_height: u16) 
29-> Vec<Raindrop<T>>
30where T: ColorAlgorithm
31{
32    let mut raindrop_vec: Vec<Raindrop<T>> = Vec::with_capacity(terminal_width.into());
33
34    for _ in 0..terminal_width {
35        let new_raindrop = Raindrop::new(
36            charset, color_algorithm, advance_chance, terminal_height);
37        raindrop_vec.push(new_raindrop);
38    }
39
40    raindrop_vec
41}
42
43/// The main loop that renders the screen
44/// 
45/// Returns after receiving any keypress
46/// 
47/// `charset` should be a `Vec<char>`. This will be the set of characters that will be
48/// displayed within the animation.
49/// 
50/// `color_algorithm` should be an instance of a type implementing [ColorAlgorithm], such as
51/// [LightnessDescending](crate::raindrop::color_algorithms::LightnessDescending).
52/// 
53/// `advance_chance` should be the chance (from 0.0 to 1.0) that any one `Raindrop` will advance
54/// its movement on any given frame. This value must be within the range `[0.0, 1.0)`.
55/// 
56/// `target_framerate` should be the number of frames per second to target.
57/// 
58/// # Panics
59/// 
60/// This function panics if `charset` is empty (i.e. has a length of zero).
61/// 
62/// This function panics if `target_framerate` is zero.
63/// 
64/// This function panics if `advance_chance` is outside the range `[0.0, 1.0)`
65/// 
66/// # Examples
67/// ```
68/// use mrs_matrix::animation::anim_loop;
69/// use mrs_matrix::raindrop::charsets::{Charset, PrintableAscii};
70/// use mrs_matrix::raindrop::color_algorithms::LightnessDescending;
71/// 
72/// pub fn main() -> crossterm::Result<()>
73/// {
74///     let charset = PrintableAscii().get_charset();
75///     let color_algorithm = LightnessDescending{
76///         hue: 118.0,
77///         saturation: 0.82
78///     };
79///     let advance_chance = 0.75;
80///     let target_framerate = 25;
81///     anim_loop(charset, color_algorithm, advance_chance, target_framerate)
82/// }
83/// ```
84pub fn anim_loop<T: ColorAlgorithm>(charset: Vec<char>, color_algorithm: T,
85     advance_chance:f64, target_framerate: usize) -> crossterm::Result<()>
86{
87    assert!(charset.len() > 0, "cannot run anim_loop with empty character set");
88    assert!(target_framerate > 0, 
89        "cannot run anim_loop at target framerate of zero");
90
91    let mut out = stdout();
92
93    let (mut term_cols, mut term_rows) = terminal::size()?;
94
95    //enable raw mode to process keypress by keypress
96    terminal::enable_raw_mode()?;
97
98    //enter alternate screen, and hide the cursor
99    out.queue(terminal::EnterAlternateScreen)?
100    .queue(cursor::Hide)?;
101
102    //calculate target frame duration by dividing one second by the number of frames that should be in one second
103    let target_frame_duration = Duration::from_secs_f64(1.0/(target_framerate as f64));
104
105    let mut raindrop_vector = 
106        create_raindrops(&charset, color_algorithm, advance_chance, 
107            term_cols, term_rows);
108
109    let mut start_instant: Instant;
110    loop {
111        start_instant = Instant::now();
112
113        //reset cursor position
114        out.queue(cursor::MoveTo(0,0))?;
115
116        //iterate through all rows
117        for row_index in 0..term_rows {
118
119            //strangely, these commands seem to be 1 based, unlike MoveTo
120            out.queue(cursor::MoveToRow(row_index + 1))?
121            .queue(cursor::MoveToColumn(1))?;
122
123            //iterate through all columns by iterating through raindrop_vector, printing styled chars where applicable
124            //note that spaces are printed for columns on this row without a printable char
125            for raindrop in raindrop_vector.iter_mut() {
126                match raindrop.get_styled_char_at_row(row_index) {
127                    None => out.queue(Print(" "))?,
128                    Some(styled_char) => out.queue(PrintStyledContent(styled_char))?
129                };
130            }
131        }
132
133        //flush buffer to 'draw'
134        out.flush()?;
135
136        //call advance_animation on all the raindrops
137        for raindrop in raindrop_vector.iter_mut() {
138            raindrop.advance_animation(term_rows);
139        }
140    
141        //wait for enough time to hit target_frame_duration, or no time if frame duration exceeds target
142        if event::poll(target_frame_duration.saturating_sub(Instant::now() - start_instant))? {
143            match event::read()? {
144                //upon recieving a resize event set new column amount
145                Event::Resize(new_cols, new_rows) => {
146                    term_cols = new_cols;
147                    term_rows = new_rows;
148
149                    raindrop_vector = 
150                        create_raindrops(&charset, color_algorithm,
151                            advance_chance, term_cols, term_rows);
152                },
153                //stop loop upon recieving a mouse or key event
154                _ => break
155            }
156        }
157    }
158
159    //disable raw mode
160    terminal::disable_raw_mode()?;
161
162    //be sure to leave the alternate screen and show the cursor again
163    out.queue(terminal::LeaveAlternateScreen)?
164    .queue(cursor::Show)?;
165    out.flush()?;
166
167    Ok(())
168}