1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use ratatui::{buffer::Cell, layout::Flex, prelude::*};
use unicode_width::UnicodeWidthStr;

use crate::big_text::{BigTextBuilder, PixelSize};

/// delay the start of the animation so it doesn't start immediately
const DELAY: usize = 240;
/// higher means more pixels per frame are modified in the animation
const DRIP_SPEED: usize = 50;
/// delay the start of the text animation so it doesn't start immediately after the initial delay
const TEXT_DELAY: usize = 240;

/// Destroy mode activated by pressing `d`
pub fn destroy(frame: &mut Frame<'_>) {
    let frame_count = frame.count().saturating_sub(DELAY);
    if frame_count == 0 {
        return;
    }

    let area = frame.size();
    let buf = frame.buffer_mut();

    drip(frame_count, area, buf);
    text(frame_count, area, buf);
}

/// Move a bunch of random pixels down one row.
///
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
/// do this, but it works well enough for this demo.
#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)]
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
    // a seeded rng as we have to move the same random pixels each frame
    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
    let ramp_frames = 450;
    let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
    let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
    let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
    for _ in 0..pixel_count {
        let src_x = rng.gen_range(0..area.width);
        let src_y = rng.gen_range(1..area.height - 2);
        let src = buf.get_mut(src_x, src_y).clone();
        // 1% of the time, move a blank or pixel (10:1) to the top line of the screen
        if rng.gen_ratio(1, 100) {
            let dest_x = rng
                .gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
                .clamp(area.left(), area.right() - 1);
            let dest_y = area.top() + 1;

            let dest = buf.get_mut(dest_x, dest_y);
            // copy the cell to the new location about 1/10 of the time blank out the cell the rest
            // of the time. This has the effect of gradually removing the pixels from the screen.
            if rng.gen_ratio(1, 10) {
                *dest = src;
            } else {
                *dest = Cell::default();
            }
        } else {
            // move the pixel down one row
            let dest_x = src_x;
            let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
            // copy the cell to the new location
            let dest = buf.get_mut(dest_x, dest_y);
            *dest = src;
        }
    }
}

/// draw some text fading in and out from black to red and back
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
    let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
    if sub_frame == 0 {
        return;
    }

    let line = "RATATUI";
    let big_text = BigTextBuilder::default()
        .lines([line.into()])
        .pixel_size(PixelSize::Full)
        .style(Style::new().fg(Color::Rgb(255, 0, 0)))
        .build()
        .unwrap();

    // the font size is 8x8 for each character and we have 1 line
    let area = centered_rect(area, line.width() as u16 * 8, 8);

    let mask_buf = &mut Buffer::empty(area);
    big_text.render(area, mask_buf);

    let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);

    for row in area.rows() {
        for col in row.columns() {
            let cell = buf.get_mut(col.x, col.y);
            let mask_cell = mask_buf.get(col.x, col.y);
            cell.set_symbol(mask_cell.symbol());

            // blend the mask cell color with the cell color
            let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
            let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));

            let color = blend(mask_color, cell_color, percentage);
            cell.set_style(Style::new().fg(color));
        }
    }
}

fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
    let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
        return mask_color;
    };
    let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
        return mask_color;
    };

    let remain = 1.0 - percentage;

    let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
    let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
    let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);

    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    Color::Rgb(red as u8, green as u8, blue as u8)
}

/// a centered rect of the given size
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
    let horizontal = Layout::horizontal([width]).flex(Flex::Center);
    let vertical = Layout::vertical([height]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}