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}