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
12const DELAY: usize = 120;
14const DRIP_SPEED: usize = 500;
16const TEXT_DELAY: usize = 180;
18
19pub 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#[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 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 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 if rng.gen_ratio(1, 10) {
64 *dest = src;
65 } else {
66 dest.reset();
67 }
68 } else {
69 let dest_x = src_x;
71 let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
72 buf[(dest_x, dest_y)] = src;
74 }
75 }
76}
77
78#[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 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
135fn 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}