[go: up one dir, main page]

demo2/
destroy.rs

1use rand::Rng;
2use rand_chacha::rand_core::SeedableRng;
3use ratatui::{
4    buffer::Buffer,
5    layout::{Flex, Layout, Rect},
6    style::{Color, Style},
7    text::Text,
8    widgets::Widget,
9    Frame,
10};
11
12/// delay the start of the animation so it doesn't start immediately
13const DELAY: usize = 120;
14/// higher means more pixels per frame are modified in the animation
15const DRIP_SPEED: usize = 500;
16/// delay the start of the text animation so it doesn't start immediately after the initial delay
17const TEXT_DELAY: usize = 180;
18
19/// Destroy mode activated by pressing `d`
20pub fn destroy(frame: &mut Frame<'_>) {
21    let frame_count = frame.count().saturating_sub(DELAY);
22    if frame_count == 0 {
23        return;
24    }
25
26    let area = frame.area();
27    let buf = frame.buffer_mut();
28
29    drip(frame_count, area, buf);
30    text(frame_count, area, buf);
31}
32
33/// Move a bunch of random pixels down one row.
34///
35/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
36/// do this, but it works well enough for this demo.
37#[allow(
38    clippy::cast_possible_truncation,
39    clippy::cast_precision_loss,
40    clippy::cast_sign_loss
41)]
42fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
43    // a seeded rng as we have to move the same random pixels each frame
44    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
45    let ramp_frames = 450;
46    let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
47    let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
48    let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
49    for _ in 0..pixel_count {
50        let src_x = rng.gen_range(0..area.width);
51        let src_y = rng.gen_range(1..area.height - 2);
52        let src = buf[(src_x, src_y)].clone();
53        // 1% of the time, move a blank or pixel (10:1) to the top line of the screen
54        if rng.gen_ratio(1, 100) {
55            let dest_x = rng
56                .gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
57                .clamp(area.left(), area.right() - 1);
58            let dest_y = area.top() + 1;
59
60            let dest = &mut buf[(dest_x, dest_y)];
61            // copy the cell to the new location about 1/10 of the time blank out the cell the rest
62            // of the time. This has the effect of gradually removing the pixels from the screen.
63            if rng.gen_ratio(1, 10) {
64                *dest = src;
65            } else {
66                dest.reset();
67            }
68        } else {
69            // move the pixel down one row
70            let dest_x = src_x;
71            let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
72            // copy the cell to the new location
73            buf[(dest_x, dest_y)] = src;
74        }
75    }
76}
77
78/// draw some text fading in and out from black to red and back
79#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
80fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
81    let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
82    if sub_frame == 0 {
83        return;
84    }
85
86    let logo = indoc::indoc! {"
87        ██████      ████    ██████    ████    ██████  ██    ██  ██
88        ██    ██  ██    ██    ██    ██    ██    ██    ██    ██  ██
89        ██████    ████████    ██    ████████    ██    ██    ██  ██
90        ██  ██    ██    ██    ██    ██    ██    ██    ██    ██  ██
91        ██    ██  ██    ██    ██    ██    ██    ██      ████    ██
92    "};
93    let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
94    let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);
95
96    let mask_buf = &mut Buffer::empty(area);
97    logo_text.render(area, mask_buf);
98
99    let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
100
101    for row in area.rows() {
102        for col in row.columns() {
103            let cell = &mut buf[(col.x, col.y)];
104            let mask_cell = &mut mask_buf[(col.x, col.y)];
105            cell.set_symbol(mask_cell.symbol());
106
107            // blend the mask cell color with the cell color
108            let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
109            let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
110
111            let color = blend(mask_color, cell_color, percentage);
112            cell.set_style(Style::new().fg(color));
113        }
114    }
115}
116
117fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
118    let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
119        return mask_color;
120    };
121    let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
122        return mask_color;
123    };
124
125    let remain = 1.0 - percentage;
126
127    let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
128    let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
129    let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
130
131    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
132    Color::Rgb(red as u8, green as u8, blue as u8)
133}
134
135/// a centered rect of the given size
136fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
137    let horizontal = Layout::horizontal([width]).flex(Flex::Center);
138    let vertical = Layout::vertical([height]).flex(Flex::Center);
139    let [area] = vertical.areas(area);
140    let [area] = horizontal.areas(area);
141    area
142}