Add Matrix effect

This commit is contained in:
Kevin Alberts 2025-08-15 11:22:17 +02:00
parent dde1af7a6b
commit c91ea0db01
4 changed files with 333 additions and 3 deletions

97
Cargo.lock generated
View file

@ -332,6 +332,18 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
@ -489,7 +501,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys",
]
@ -549,6 +561,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.97"
@ -567,6 +588,41 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "ratatui"
version = "0.29.0"
@ -594,6 +650,7 @@ version = "0.1.0"
dependencies = [
"crossterm 0.29.0",
"futures",
"rand",
"ratatui",
"terminput",
"terminput-crossterm",
@ -866,6 +923,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -960,3 +1026,32 @@ name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
crossterm = { version = "0.29.0", features = ["event-stream"] }
futures = "0.3.31"
rand = "0.9.2"
ratatui = "0.29.0"
terminput = "0.5.3"
terminput-crossterm = "0.3.3"

234
src/app_matrix.rs Normal file
View 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) {
()
}
}

View file

@ -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};