Add Matrix effect
This commit is contained in:
parent
dde1af7a6b
commit
c91ea0db01
4 changed files with 333 additions and 3 deletions
97
Cargo.lock
generated
97
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
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