use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::Widget,
};
use crate::tui::theme::solarized;
const KATAKANA_HALF: &[char] = &[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ',
'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ',
'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン',
];
const KATAKANA_FULL: &[char] = &[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ',
'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ',
'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン',
];
const HIRAGANA: &[char] = &[
'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た',
'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ',
];
const ASCII_HACKER: &[char] = &[
'0', '1', '.', ':', '_', '-', '>', '<', '/', '\\', '|', '+', '*', '{', '}', '[', ']', '(', ')',
'=', '#', '@', '$', '%', '^', '&', '~', '`', ';', '"', '\'', '!', '?',
];
const NIKA_MASCOTS: &[&str] = &[
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦋",
"🦀", "⚡", "✨", "🔮", "💻", "🖥️", "⌨️", "🌌", "🪐", "💎", "🌟", "☄️", "🌙", "🔥", "🐔", "🦖", "🦕", "🪼", "🦙", "🐒", "🦄", "🐯", "🦁", "🦚", "🦎", "🦈", "🦞", "🦑", "🐙", "🦩", "🦜", "🏝️", "🗿", "🐦🔥", "🎭", "🎪", "👾", "🤖", ];
const RAIN_COLORS: &[Color] = &[
solarized::CYAN,
solarized::CYAN,
solarized::GREEN,
solarized::GREEN,
solarized::BLUE,
solarized::VIOLET,
solarized::MAGENTA,
solarized::YELLOW,
solarized::ORANGE,
solarized::BASE01,
solarized::BASE00,
];
const NIKA_PATTERN: &[&str] = &[
"X X . . X X X X X X . . X X . X X X X .", "X X . . X X X X X X . . X X . X X X X .", "X X X . X X X X X X . X X . X X . . X X", "X X X . X X X X X X X X . . X X . . X X", "X X X X X X X X X X X X . . X X X X X X", "X X . X X X X X X X X X . . X X X X X X", "X X . . X X X X X X . X X . X X . . X X", "X X . . X X X X X X . . X X X X . . X X", "X X . . X X X X X X . . X X X X . . X X", ];
const NIKA_PATTERN_WIDTH: usize = 44;
const NIKA_PATTERN_HEIGHT: usize = 9;
const DECO_EMOJIS: &[&str] = &["🦋", "✨", "🌌", "💫", "⭐", "🌟", "🪐", "🌙"];
#[derive(Clone)]
enum RainGlyph {
Char(char),
Emoji(&'static str),
}
impl RainGlyph {
fn write_to_buf(&self, buf: &mut Buffer, x: u16, y: u16, style: Style) {
match self {
RainGlyph::Char(c) => {
let mut char_buf = [0u8; 4];
let s = c.encode_utf8(&mut char_buf);
buf.set_string(x, y, s, style);
}
RainGlyph::Emoji(e) => {
buf.set_string(x, y, *e, style);
}
}
}
fn width(&self) -> u16 {
match self {
RainGlyph::Char(_) => 1,
RainGlyph::Emoji(_) => 2,
}
}
}
#[derive(Clone)]
struct RainDrop {
y: i16,
speed: u8,
glyph: RainGlyph,
color: Color,
trail: u8,
}
pub struct MatrixRain {
frame: u8,
density: f32,
opacity: f32,
with_mascots: bool,
seed: u64,
nika_pattern: bool,
explosion_frame: u8,
}
impl Default for MatrixRain {
fn default() -> Self {
Self {
frame: 0,
density: 0.15, opacity: 1.0,
with_mascots: true,
seed: 42,
nika_pattern: false,
explosion_frame: 0,
}
}
}
impl MatrixRain {
pub fn new() -> Self {
Self::default()
}
pub fn frame(mut self, frame: u8) -> Self {
self.frame = frame;
self
}
pub fn density(mut self, density: f32) -> Self {
self.density = density.clamp(0.0, 1.0);
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity.clamp(0.0, 1.0);
self
}
pub fn with_mascots(mut self, enable: bool) -> Self {
self.with_mascots = enable;
self
}
pub fn with_emojis(self, enable: bool) -> Self {
self.with_mascots(enable)
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_nika_pattern(mut self, enable: bool) -> Self {
self.nika_pattern = enable;
self
}
pub fn explosion_frame(mut self, frame: u8) -> Self {
self.explosion_frame = frame;
self
}
fn random_glyph(&self, rng: &mut SmallRng) -> RainGlyph {
let roll: f32 = rng.gen();
if self.with_mascots && roll < 0.06 {
let idx = rng.gen_range(0..NIKA_MASCOTS.len());
RainGlyph::Emoji(NIKA_MASCOTS[idx])
} else if roll < 0.40 {
let idx = rng.gen_range(0..KATAKANA_HALF.len());
RainGlyph::Char(KATAKANA_HALF[idx])
} else if roll < 0.60 {
let idx = rng.gen_range(0..KATAKANA_FULL.len());
RainGlyph::Char(KATAKANA_FULL[idx])
} else if roll < 0.75 {
let idx = rng.gen_range(0..HIRAGANA.len());
RainGlyph::Char(HIRAGANA[idx])
} else {
let idx = rng.gen_range(0..ASCII_HACKER.len());
RainGlyph::Char(ASCII_HACKER[idx])
}
}
fn generate_drops(&self, col: u16, height: u16, rng: &mut SmallRng) -> Vec<RainDrop> {
let mut drops = Vec::new();
let num_drops = ((height as f32 * self.density * 0.3) as usize).max(1);
for i in 0..num_drops {
let base_y =
(col as i16 * 7 + i as i16 * 17 + self.frame as i16 * 2) % (height as i16 * 2);
let y = base_y - height as i16;
let speed = rng.gen_range(1..=2); let color_idx = rng.gen_range(0..RAIN_COLORS.len());
let trail = rng.gen_range(1..=3);
drops.push(RainDrop {
y,
speed,
glyph: self.random_glyph(rng),
color: RAIN_COLORS[color_idx],
trail,
});
}
drops
}
fn apply_opacity(&self, color: Color, brightness: f32) -> Color {
let effective = brightness * self.opacity;
if effective < 0.1 {
return solarized::BASE03; }
match color {
Color::Rgb(r, g, b) => Color::Rgb(
(r as f32 * effective) as u8,
(g as f32 * effective) as u8,
(b as f32 * effective) as u8,
),
c => {
if effective < 0.5 {
solarized::BASE02
} else {
c
}
}
}
}
#[inline]
fn ease_out(t: f32) -> f32 {
1.0 - (1.0 - t).powi(3)
}
#[allow(clippy::too_many_lines)]
fn render_nika_pattern(&self, area: Rect, buf: &mut Buffer, _rng: &mut SmallRng) {
let center_x = area.x + area.width / 2;
let center_y = area.y + area.height / 2;
let pattern_start_x = center_x.saturating_sub((NIKA_PATTERN_WIDTH as u16 * 2) / 2);
let pattern_start_y = center_y.saturating_sub(NIKA_PATTERN_HEIGHT as u16 / 2);
let pattern_center_x = pattern_start_x + (NIKA_PATTERN_WIDTH as u16);
let pattern_center_y = pattern_start_y + (NIKA_PATTERN_HEIGHT as u16 / 2);
let raw_progress = (self.explosion_frame as f32 / 13.0).min(1.0);
let progress = Self::ease_out(raw_progress);
let exploding = self.explosion_frame > 0;
let fade_in = if self.explosion_frame == 0 {
((self.frame % 10) as f32 / 2.0).min(1.0)
} else {
1.0
};
if progress < 0.7 {
let sparkle_fade = (1.0 - progress / 0.7) * fade_in;
let deco_positions: [(i16, i16); 8] = [
(-3, -1),
(48, -1), (-4, 4),
(49, 4), (-3, 9),
(48, 9), (22, -2),
(22, 10), ];
for (i, (dx, dy)) in deco_positions.iter().enumerate() {
let x = (pattern_start_x as i16 + dx).max(area.x as i16) as u16;
let y = (pattern_start_y as i16 + dy).max(area.y as i16) as u16;
if x < area.x + area.width - 1 && y < area.y + area.height {
let emoji_idx = (i + (self.frame as usize / 2)) % DECO_EMOJIS.len();
let emoji = DECO_EMOJIS[emoji_idx];
let color = RAIN_COLORS[i % RAIN_COLORS.len()];
buf.set_string(
x,
y,
emoji,
Style::default().fg(self.apply_opacity(color, sparkle_fade)),
);
}
}
}
let mut butterfly_idx = 0usize;
for (row_idx, row) in NIKA_PATTERN.iter().enumerate() {
let base_y = pattern_start_y + row_idx as u16;
if base_y < area.y || base_y >= area.y + area.height {
continue;
}
for (col_pos, ch) in row.chars().enumerate() {
if ch == 'X' {
let base_x = pattern_start_x + (col_pos as u16) * 2;
let dist_from_center = ((base_x as f32 - pattern_center_x as f32).abs()
+ (base_y as f32 - pattern_center_y as f32).abs() * 2.0)
/ 50.0;
let local_progress = (progress - dist_from_center * 0.15).clamp(0.0, 1.0);
let local_eased = Self::ease_out(local_progress);
let (final_x, final_y, local_fade) = if exploding && local_progress > 0.0 {
let seed_offset = (row_idx * 50 + butterfly_idx) as u64;
let angle: f32 = ((seed_offset.wrapping_mul(7919) % 1000) as f32) / 1000.0
* std::f32::consts::TAU;
let max_dist =
((seed_offset.wrapping_mul(6991) % 500) as f32 / 500.0 + 0.5) * 20.0;
let dist = local_eased * max_dist;
let offset_x = (angle.cos() * dist) as i16;
let offset_y = (angle.sin() * dist * 0.5) as i16;
let new_x = (base_x as i16 + offset_x)
.max(area.x as i16)
.min((area.x + area.width - 2) as i16)
as u16;
let new_y = (base_y as i16 + offset_y)
.max(area.y as i16)
.min((area.y + area.height - 1) as i16)
as u16;
let fade = (1.0 - local_eased.powf(1.5)).max(0.0);
(new_x, new_y, fade)
} else {
(base_x, base_y, fade_in)
};
if local_fade < 0.05 {
butterfly_idx += 1;
continue;
}
if final_x >= area.x
&& final_x < area.x + area.width - 1
&& final_y >= area.y
&& final_y < area.y + area.height
{
let color_idx =
(row_idx + butterfly_idx + self.frame as usize / 4) % RAIN_COLORS.len();
let base_color = RAIN_COLORS[color_idx];
let color = self.apply_opacity(base_color, local_fade);
buf.set_string(final_x, final_y, "🦋", Style::default().fg(color));
}
butterfly_idx += 1;
}
}
}
}
}
impl Widget for MatrixRain {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.opacity < 0.05 {
return;
}
let seed = self.seed.wrapping_add(self.frame as u64);
let mut rng = SmallRng::seed_from_u64(seed);
let mut col = 0u16;
while col < area.width {
let drops = self.generate_drops(col, area.height, &mut rng);
for drop in drops {
let current_y = drop.y + (self.frame as i16 / drop.speed as i16);
for trail_offset in 0..=drop.trail {
let y = current_y - trail_offset as i16;
if y >= 0 && y < area.height as i16 {
let x = area.x + col;
let y_pos = area.y + y as u16;
if x < area.x + area.width && y_pos < area.y + area.height {
let brightness = if trail_offset == 0 {
0.8 } else {
0.4 - (trail_offset as f32 / drop.trail as f32) * 0.3
};
let color = self.apply_opacity(drop.color, brightness);
let glyph_width = drop.glyph.width();
if x + glyph_width <= area.x + area.width {
if matches!(drop.glyph, RainGlyph::Char(_)) || trail_offset == 0 {
drop.glyph.write_to_buf(
buf,
x,
y_pos,
Style::default().fg(color),
);
}
}
}
}
}
}
col += 3; }
if self.nika_pattern {
self.render_nika_pattern(area, buf, &mut rng);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matrix_rain_new() {
let rain = MatrixRain::new();
assert_eq!(rain.frame, 0);
assert!((rain.density - 0.15).abs() < 0.01);
assert!((rain.opacity - 1.0).abs() < 0.01);
assert!(rain.with_mascots);
}
#[test]
fn test_matrix_rain_builder() {
let rain = MatrixRain::new()
.frame(42)
.density(0.3)
.opacity(0.5)
.with_mascots(false)
.seed(123);
assert_eq!(rain.frame, 42);
assert!((rain.density - 0.3).abs() < 0.01);
assert!((rain.opacity - 0.5).abs() < 0.01);
assert!(!rain.with_mascots);
assert_eq!(rain.seed, 123);
}
#[test]
fn test_density_clamping() {
let rain = MatrixRain::new().density(2.0);
assert!((rain.density - 1.0).abs() < 0.01);
let rain = MatrixRain::new().density(-0.5);
assert!((rain.density - 0.0).abs() < 0.01);
}
#[test]
fn test_opacity_clamping() {
let rain = MatrixRain::new().opacity(1.5);
assert!((rain.opacity - 1.0).abs() < 0.01);
let rain = MatrixRain::new().opacity(-0.2);
assert!((rain.opacity - 0.0).abs() < 0.01);
}
#[test]
fn test_render_empty_area() {
let rain = MatrixRain::new();
let area = Rect::new(0, 0, 0, 0);
let mut buf = Buffer::empty(area);
rain.render(area, &mut buf);
}
#[test]
fn test_render_zero_opacity() {
let rain = MatrixRain::new().opacity(0.0);
let area = Rect::new(0, 0, 10, 5);
let mut buf = Buffer::empty(area);
rain.render(area, &mut buf);
}
#[test]
fn test_nika_mascots_list() {
assert!(NIKA_MASCOTS.contains(&"🦋"));
assert!(NIKA_MASCOTS.contains(&"🦀"));
assert!(NIKA_MASCOTS.contains(&"🐔"));
assert!(NIKA_MASCOTS.contains(&"⚡"));
assert!(NIKA_MASCOTS.contains(&"🪐"));
}
#[test]
fn test_katakana_list() {
assert!(KATAKANA_HALF.contains(&'ア'));
assert!(KATAKANA_HALF.contains(&'カ'));
assert!(KATAKANA_HALF.contains(&'ン'));
assert_eq!(KATAKANA_HALF.len(), 45); }
#[test]
fn test_nika_pattern_builder() {
let rain = MatrixRain::new().with_nika_pattern(true);
assert!(rain.nika_pattern);
let rain = MatrixRain::new().with_nika_pattern(false);
assert!(!rain.nika_pattern);
}
#[test]
fn test_explosion_frame_builder() {
let rain = MatrixRain::new().explosion_frame(42);
assert_eq!(rain.explosion_frame, 42);
}
#[test]
fn test_nika_pattern_dimensions() {
assert_eq!(NIKA_PATTERN.len(), NIKA_PATTERN_HEIGHT);
for row in NIKA_PATTERN {
assert!(row.contains('X'));
}
}
#[test]
fn test_deco_emojis() {
assert!(DECO_EMOJIS.len() >= 6);
assert!(DECO_EMOJIS.contains(&"🦋"));
assert!(DECO_EMOJIS.contains(&"🌌"));
}
#[test]
fn test_render_with_nika_pattern() {
let rain = MatrixRain::new().with_nika_pattern(true).opacity(1.0);
let area = Rect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(area);
rain.render(area, &mut buf);
}
#[test]
fn test_render_with_explosion() {
let rain = MatrixRain::new()
.with_nika_pattern(true)
.explosion_frame(30)
.opacity(1.0);
let area = Rect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(area);
rain.render(area, &mut buf);
}
#[test]
fn test_ease_out_function() {
assert!((MatrixRain::ease_out(0.0) - 0.0).abs() < 0.001);
assert!((MatrixRain::ease_out(1.0) - 1.0).abs() < 0.001);
assert!(MatrixRain::ease_out(0.5) > 0.5);
assert!(MatrixRain::ease_out(0.3) < MatrixRain::ease_out(0.7));
}
#[test]
fn test_smooth_explosion_progression() {
let area = Rect::new(0, 0, 120, 40);
for frame in 0..30 {
let rain = MatrixRain::new()
.frame(frame)
.with_nika_pattern(true)
.explosion_frame(frame)
.opacity(1.0);
let mut buf = Buffer::empty(area);
rain.render(area, &mut buf);
}
}
#[test]
fn test_wave_pattern_center_first() {
let center_x: f32 = 50.0;
let center_y: f32 = 12.0;
let dist_center = (0.0_f32.abs() + 0.0_f32.abs() * 2.0) / 50.0;
let dist_edge = ((40.0_f32 - center_x).abs() + (5.0_f32 - center_y).abs() * 2.0) / 50.0;
assert!(dist_center < dist_edge);
}
}