Add Matrix effect
This commit is contained in:
parent
dde1af7a6b
commit
c91ea0db01
4 changed files with 333 additions and 3 deletions
234
src/app_matrix.rs
Normal file
234
src/app_matrix.rs
Normal file
|
@ -0,0 +1,234 @@
|
|||
use ratatui::{layout::Rect, style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, Paragraph}};
|
||||
use terminput::KeyCode;
|
||||
use rand::prelude::*;
|
||||
use rand::rng;
|
||||
|
||||
use crate::Demo;
|
||||
|
||||
struct MatrixColumn {
|
||||
x: u16,
|
||||
y: u16,
|
||||
length: u16,
|
||||
speed: u16,
|
||||
chars: Vec<char>,
|
||||
}
|
||||
|
||||
impl MatrixColumn {
|
||||
fn new(x: u16, _frame_counter: usize) -> Self {
|
||||
let mut rng = rng();
|
||||
let length = rng.random_range(5..15);
|
||||
let speed = rng.random_range(1..4);
|
||||
let chars = (0..length)
|
||||
.map(|_| Self::random_matrix_char(&mut rng))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
x,
|
||||
y: 0,
|
||||
length,
|
||||
speed,
|
||||
chars,
|
||||
}
|
||||
}
|
||||
|
||||
fn random_matrix_char(rng: &mut ThreadRng) -> char {
|
||||
let matrix_chars = [
|
||||
// Japanese katakana
|
||||
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ',
|
||||
'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト',
|
||||
'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ',
|
||||
'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ',
|
||||
'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン',
|
||||
// Numbers
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
// Some ASCII symbols
|
||||
':', '・', '"', '=', '*', '+', '-', '<', '>', '¦', '|', 'Z',
|
||||
];
|
||||
*matrix_chars.choose(rng).unwrap()
|
||||
}
|
||||
|
||||
fn update(&mut self, height: u16, frame_counter: usize, speed_multiplier: usize) {
|
||||
// Calculate effective speed with multiplier (higher multiplier = faster)
|
||||
let effective_speed = if speed_multiplier == 0 {
|
||||
self.speed as usize * 4 // Much slower when multiplier is 0
|
||||
} else {
|
||||
(self.speed as usize * 2).saturating_sub(speed_multiplier.saturating_sub(1)).max(1)
|
||||
};
|
||||
|
||||
if frame_counter % effective_speed == 0 {
|
||||
self.y = self.y.wrapping_add(1);
|
||||
|
||||
// Reset column when it goes off screen
|
||||
if self.y > height + self.length {
|
||||
self.y = 0;
|
||||
let mut rng = rng();
|
||||
self.length = rng.random_range(5..15);
|
||||
self.speed = rng.random_range(1..4);
|
||||
self.chars = (0..self.length)
|
||||
.map(|_| Self::random_matrix_char(&mut rng))
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Occasionally change characters (also affected by speed multiplier)
|
||||
let char_change_rate = (10 + (speed_multiplier * 5).min(40)) as f64 / 100.0;
|
||||
if rng().random_bool(char_change_rate) {
|
||||
let mut rng = rng();
|
||||
for ch in &mut self.chars {
|
||||
if rng.random_bool(0.3) {
|
||||
*ch = Self::random_matrix_char(&mut rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MyApp {
|
||||
counter: usize,
|
||||
frame_counter: usize,
|
||||
columns: Vec<MatrixColumn>,
|
||||
initialized: bool,
|
||||
density: usize,
|
||||
}
|
||||
|
||||
impl Demo for MyApp {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
counter: 1,
|
||||
frame_counter: 0,
|
||||
columns: Vec::new(),
|
||||
initialized: false,
|
||||
density: 3, // Default density (lower = more dense)
|
||||
}
|
||||
}
|
||||
|
||||
async fn frame(&mut self, term: &mut crate::Term) -> Result<(), String> {
|
||||
term.draw(|fr| {
|
||||
self.frame_counter = self.frame_counter.wrapping_add(1);
|
||||
|
||||
let style = Style::new().green().on_black();
|
||||
let area = fr.area();
|
||||
|
||||
let block = Block::bordered()
|
||||
.style(style)
|
||||
.title("Matrix Rain")
|
||||
.title_bottom("By: Kurocon (Kevin Alberts), kurocon@snt.utwente.nl");
|
||||
|
||||
let barea = block.inner(area);
|
||||
|
||||
fr.render_widget(block, area);
|
||||
|
||||
// Initialize columns on first frame or when density changes
|
||||
if !self.initialized {
|
||||
self.columns.clear();
|
||||
|
||||
// Create multiple layers based on density
|
||||
// Lower density value = more layers = more dense
|
||||
let layers = match self.density {
|
||||
1 => 12, // Very dense - 12 layers
|
||||
2 => 8, // Dense - 8 layers
|
||||
3 => 6, // Normal - 6 layers
|
||||
4 => 4, // Sparse - 4 layers
|
||||
5 => 3, // More sparse - 3 layers
|
||||
_ => 2, // Very sparse - 2 layers
|
||||
};
|
||||
|
||||
let base_spacing = (self.density * 2).max(2) as u16;
|
||||
|
||||
for layer in 0..layers {
|
||||
let layer_offset = (layer as u16 * base_spacing / layers as u16).max(1);
|
||||
let spacing = base_spacing;
|
||||
|
||||
let layer_columns: Vec<MatrixColumn> = (barea.x..(barea.x + barea.width))
|
||||
.step_by(spacing as usize)
|
||||
.map(|x| {
|
||||
let offset_x = (x + layer_offset) % (barea.x + barea.width);
|
||||
let mut col = MatrixColumn::new(offset_x, self.frame_counter + layer * 100);
|
||||
// Give each layer slightly different starting positions
|
||||
col.y = (layer as u16 * barea.height / layers as u16) % (barea.height + col.length);
|
||||
col
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.columns.extend(layer_columns);
|
||||
}
|
||||
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
// Update and draw columns
|
||||
let buf = fr.buffer_mut();
|
||||
for column in &mut self.columns {
|
||||
column.update(barea.height, self.frame_counter, self.counter);
|
||||
|
||||
for (i, &ch) in column.chars.iter().enumerate() {
|
||||
let char_y = column.y.saturating_sub(i as u16);
|
||||
|
||||
if char_y >= barea.y && char_y < barea.y + barea.height {
|
||||
let style = if i == 0 {
|
||||
// Head of the stream - bright white
|
||||
Style::new().white().bold()
|
||||
} else if i < 3 {
|
||||
// Near head - bright green
|
||||
Style::new().light_green().bold()
|
||||
} else if i < column.length as usize / 2 {
|
||||
// Middle - regular green
|
||||
Style::new().green()
|
||||
} else {
|
||||
// Tail - dark green
|
||||
Style::new().dark_gray()
|
||||
};
|
||||
|
||||
buf.set_span(column.x, char_y, &Span::from(ch.to_string()).style(style), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center text with instructions and counter
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
lines.push("W/S: Speed | E/D: Density | Enter: Exit".into());
|
||||
lines.push(format!("Speed multiplier: {}", self.counter).into());
|
||||
lines.push(format!("Density: {} ({})", self.density, if self.density <= 2 { "Dense" } else if self.density <= 4 { "Normal" } else { "Sparse" }).into());
|
||||
lines.push("".into());
|
||||
lines.push("\"Wake up, Neo...\"".light_green().into());
|
||||
|
||||
let text = Paragraph::new(lines).centered().style(Style::new().on_black());
|
||||
|
||||
let text_width = 45;
|
||||
let text_height = 7;
|
||||
let text_x = (barea.width.saturating_sub(text_width)) / 2 + barea.x;
|
||||
let text_y = (barea.height.saturating_sub(text_height)) / 2 + barea.y;
|
||||
|
||||
fr.render_widget(text, Rect::new(text_x, text_y, text_width, text_height));
|
||||
}).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn input(&mut self, event: terminput::Event) -> Result<(), String> {
|
||||
if let terminput::Event::Key(kv) = event {
|
||||
match kv.code {
|
||||
KeyCode::Char('w') => {
|
||||
self.counter = self.counter.wrapping_add(1);
|
||||
},
|
||||
KeyCode::Char('s') => {
|
||||
self.counter = self.counter.saturating_sub(1);
|
||||
},
|
||||
KeyCode::Char('e') => {
|
||||
self.density = self.density.saturating_sub(1).max(1);
|
||||
self.initialized = false; // Force reinitialization
|
||||
},
|
||||
KeyCode::Char('d') => {
|
||||
self.density = (self.density + 1).min(10);
|
||||
self.initialized = false; // Force reinitialization
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnected(&mut self) {
|
||||
()
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@ use crossterm::event::Event;
|
|||
use ratatui::{prelude::CrosstermBackend, Terminal};
|
||||
use futures::{future::FutureExt, StreamExt};
|
||||
|
||||
mod app;
|
||||
use app::MyApp;
|
||||
mod app_matrix;
|
||||
use app_matrix::MyApp;
|
||||
use terminput::KeyCode;
|
||||
use terminput_crossterm::to_terminput;
|
||||
use termion::raw::{IntoRawMode, RawTerminal};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue