From 828191a5073b00b4a3f1df31d1c82cde6c3695d9 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:50:02 +0530 Subject: [PATCH 1/8] Handle unicode widths (somewhat) --- src/matrix/entity.rs | 5 ++++- src/matrix/stream.rs | 16 +++++++++------- src/symbols.rs | 8 ++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/matrix/entity.rs b/src/matrix/entity.rs index 5a0ccca..2c6a580 100644 --- a/src/matrix/entity.rs +++ b/src/matrix/entity.rs @@ -25,6 +25,8 @@ pub struct Entity { /// The symbol the entity represents symbol: char, + /// Width of the symbol that is visible in the terminal + width: usize, /// The color of the symbol color: colors::RGBColor, /// The character set to use for the symbols @@ -56,6 +58,7 @@ impl Entity { speed_y, color, symbol: ' ', + width: config.mode.width(), mode: config.mode.clone(), frame_count: 0, switch_interval: utils::random_between::(1, config.switch_interval * config.fps), @@ -64,7 +67,7 @@ impl Entity { /// Rain. Updates the position of the [Entity] using the rain speed. pub fn rain(&mut self) { - self.x += self.speed_x; + self.x += self.speed_x * self.width as f32; self.y += self.speed_y; } diff --git a/src/matrix/stream.rs b/src/matrix/stream.rs index 766fc8a..609fcb5 100644 --- a/src/matrix/stream.rs +++ b/src/matrix/stream.rs @@ -87,15 +87,16 @@ impl Stream { let color = gradient.interpolate(i as f32 / self.count as f32); // Determine the entity starting x and y positions based on the direction of flow + let width_adjusted_i = i as f32 * config.mode.width() as f32; let (x, y) = match config.direction { Direction::Down => (self.x, self.y - i as f32), Direction::Up => (self.x, self.y + i as f32), - Direction::Right => (self.x - i as f32, self.y), - Direction::Left => (self.x + i as f32, self.y), - Direction::DiagonalRight => (self.x - i as f32, self.y - i as f32), - Direction::DiagonalRightReverse => (self.x + i as f32, self.y + i as f32), - Direction::DiagonalLeft => (self.x + i as f32, self.y - i as f32), - Direction::DiagonalLeftReverse => (self.x - i as f32, self.y + i as f32), + Direction::Right => (self.x - width_adjusted_i, self.y), + Direction::Left => (self.x + width_adjusted_i, self.y), + Direction::DiagonalRight => (self.x - width_adjusted_i, self.y - i as f32), + Direction::DiagonalRightReverse => (self.x + width_adjusted_i, self.y + i as f32), + Direction::DiagonalLeft => (self.x + width_adjusted_i, self.y - i as f32), + Direction::DiagonalLeftReverse => (self.x - width_adjusted_i, self.y + i as f32), }; // Create the entity and add it to the entities vector @@ -119,9 +120,10 @@ impl Stream { // by the next frame, except for the trailing entity. So we manually overwrite it so that // the stream doesn't leave a trail. if !config.leave_trail { + let space = " ".repeat(config.mode.width()); stdout .queue(cursor::MoveTo(e.x as u16, e.y as u16))? - .queue(Print(" "))?; + .queue(Print(space))?; } // This is also a good time to check if the last entity is off the screen, diff --git a/src/symbols.rs b/src/symbols.rs index 1a913ef..50432fd 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -45,6 +45,14 @@ impl FromStr for Symbols { } impl Symbols { + /// Get the width of the character in the symbol set + pub fn width(&self) -> usize { + match self { + Self::Original | Self::Cursed => 2, // Katakana and Emojis + _ => 1, // Default to width of 1 for most characters + } + } + /// Get a random character from the symbol set pub fn get_random(&self) -> char { match self { From 48d9f25220666d74757414691cb1677183609d37 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:02:58 +0530 Subject: [PATCH 2/8] Take character width in account when checkout for "out-of-bounds" calculations --- src/matrix/entity.rs | 8 ++++++-- src/matrix/stream.rs | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/matrix/entity.rs b/src/matrix/entity.rs index 2c6a580..9bd40fc 100644 --- a/src/matrix/entity.rs +++ b/src/matrix/entity.rs @@ -26,7 +26,7 @@ pub struct Entity { /// The symbol the entity represents symbol: char, /// Width of the symbol that is visible in the terminal - width: usize, + pub width: usize, /// The color of the symbol color: colors::RGBColor, /// The character set to use for the symbols @@ -93,7 +93,11 @@ impl Entity { stdout: &mut std::io::Stdout, ) -> std::io::Result<()> { // Don't render if the entity is off-screen - if self.x < 0.0 || self.x >= columns as f32 || self.y < 0.0 || self.y >= rows as f32 { + if self.x < 0.0 + || self.x + self.width as f32 >= columns as f32 + || self.y < 0.0 + || self.y >= rows as f32 + { return Ok(()); } diff --git a/src/matrix/stream.rs b/src/matrix/stream.rs index 609fcb5..90efaa9 100644 --- a/src/matrix/stream.rs +++ b/src/matrix/stream.rs @@ -131,11 +131,15 @@ impl Stream { let should_regenerate = match config.direction { Direction::Down => e.y >= rows as f32, Direction::Up => e.y < 0.0, - Direction::Right => e.x >= columns as f32, + Direction::Right => e.x + e.width as f32 >= columns as f32, Direction::Left => e.x < 0.0, Direction::DiagonalLeft => e.x < 0.0 && e.y >= rows as f32, - Direction::DiagonalLeftReverse => e.x >= columns as f32 && e.y < 0.0, - Direction::DiagonalRight => e.x >= columns as f32 && e.y >= rows as f32, + Direction::DiagonalLeftReverse => { + e.x + e.width as f32 >= columns as f32 && e.y < 0.0 + } + Direction::DiagonalRight => { + e.x + e.width as f32 >= columns as f32 && e.y >= rows as f32 + } Direction::DiagonalRightReverse => e.x < 0.0 && e.y < 0.0, }; From b47cf89a05aebf0ebdc03a97240d5e00833a85a6 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:21:26 +0530 Subject: [PATCH 3/8] Handle trail clearing exception In the case when moving from right-to-left, printing whitespace to clear the trail was causing issues when clearing the last element which had already been cleared. The logic now checks if the x position is less than the width of the symbol to render and adjusts the clear logic accordingly --- src/matrix/stream.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/stream.rs b/src/matrix/stream.rs index 90efaa9..c147322 100644 --- a/src/matrix/stream.rs +++ b/src/matrix/stream.rs @@ -119,7 +119,7 @@ impl Stream { // Clean up the last entity. As the stream moves down, all entities will be overwritten // by the next frame, except for the trailing entity. So we manually overwrite it so that // the stream doesn't leave a trail. - if !config.leave_trail { + if !config.leave_trail && e.x <= e.width as f32 { let space = " ".repeat(config.mode.width()); stdout .queue(cursor::MoveTo(e.x as u16, e.y as u16))? @@ -132,7 +132,7 @@ impl Stream { Direction::Down => e.y >= rows as f32, Direction::Up => e.y < 0.0, Direction::Right => e.x + e.width as f32 >= columns as f32, - Direction::Left => e.x < 0.0, + Direction::Left => e.x + (e.width as f32) < 0.0, Direction::DiagonalLeft => e.x < 0.0 && e.y >= rows as f32, Direction::DiagonalLeftReverse => { e.x + e.width as f32 >= columns as f32 && e.y < 0.0 From b700c55527e9a24cf2280ba6ccf2e43d74634e33 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:42:15 +0530 Subject: [PATCH 4/8] Extract `is_offscreen` logic into a helper function --- src/matrix/entity.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/matrix/entity.rs b/src/matrix/entity.rs index 9bd40fc..ba9e6d0 100644 --- a/src/matrix/entity.rs +++ b/src/matrix/entity.rs @@ -93,11 +93,7 @@ impl Entity { stdout: &mut std::io::Stdout, ) -> std::io::Result<()> { // Don't render if the entity is off-screen - if self.x < 0.0 - || self.x + self.width as f32 >= columns as f32 - || self.y < 0.0 - || self.y >= rows as f32 - { + if self.is_offscreen(columns, rows) { return Ok(()); } @@ -113,4 +109,12 @@ impl Entity { Ok(()) } + + /// Returns true if the [Entity] is offscreen + fn is_offscreen(&mut self, columns: i32, rows: i32) -> bool { + self.x < 0.0 + || self.x + self.width as f32 >= columns as f32 + || self.y < 0.0 + || self.y >= rows as f32 + } } From a341b7f70fdd105af23634d869b6eea43110d0b7 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:44:47 +0530 Subject: [PATCH 5/8] Make `width` field private and add getter method for symbol width --- src/matrix/entity.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/matrix/entity.rs b/src/matrix/entity.rs index ba9e6d0..5c8d3c3 100644 --- a/src/matrix/entity.rs +++ b/src/matrix/entity.rs @@ -26,7 +26,7 @@ pub struct Entity { /// The symbol the entity represents symbol: char, /// Width of the symbol that is visible in the terminal - pub width: usize, + width: usize, /// The color of the symbol color: colors::RGBColor, /// The character set to use for the symbols @@ -65,6 +65,11 @@ impl Entity { } } + /// Returns the width of the entity's symbol in terminal columns + pub fn width(&self) -> usize { + self.width + } + /// Rain. Updates the position of the [Entity] using the rain speed. pub fn rain(&mut self) { self.x += self.speed_x * self.width as f32; From 2a08867bb779c9dc43578c3137a44888871724da Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:45:11 +0530 Subject: [PATCH 6/8] Change `is_offscreen` method to take an immutable reference Doesn't need to be mutable --- src/matrix/entity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/entity.rs b/src/matrix/entity.rs index 5c8d3c3..0c95700 100644 --- a/src/matrix/entity.rs +++ b/src/matrix/entity.rs @@ -116,7 +116,7 @@ impl Entity { } /// Returns true if the [Entity] is offscreen - fn is_offscreen(&mut self, columns: i32, rows: i32) -> bool { + fn is_offscreen(&self, columns: i32, rows: i32) -> bool { self.x < 0.0 || self.x + self.width as f32 >= columns as f32 || self.y < 0.0 From 5b4bb51993b26d4b07d42847e9011452cf2c3424 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:45:53 +0530 Subject: [PATCH 7/8] Optimize random symbol generation in background population for correct column width --- src/matrix/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/matrix/mod.rs b/src/matrix/mod.rs index ca82ec8..4a4e838 100644 --- a/src/matrix/mod.rs +++ b/src/matrix/mod.rs @@ -152,8 +152,11 @@ impl Matrix { )))?; // Iterate over each row and pre-render random faint symbols for _ in 0..self.rows { - let random_symbol_str = - String::from_iter((0..self.columns).map(|_| config.mode.get_random())); + let random_symbol_str = String::from_iter( + (0..self.columns) + .step_by(config.mode.width()) + .map(|_| config.mode.get_random()), + ); stdout .queue(Print(random_symbol_str))? .queue(MoveToNextLine(1))?; From 283676e50eb98af08aa31f7ca7d7e270c5f6abb1 Mon Sep 17 00:00:00 2001 From: Shresht Srivastav <59516096+Shresht7@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:46:10 +0530 Subject: [PATCH 8/8] Fix boundary checks for entity rendering and regeneration conditions --- src/matrix/stream.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/matrix/stream.rs b/src/matrix/stream.rs index c147322..7dadd50 100644 --- a/src/matrix/stream.rs +++ b/src/matrix/stream.rs @@ -119,7 +119,7 @@ impl Stream { // Clean up the last entity. As the stream moves down, all entities will be overwritten // by the next frame, except for the trailing entity. So we manually overwrite it so that // the stream doesn't leave a trail. - if !config.leave_trail && e.x <= e.width as f32 { + if !config.leave_trail && e.x >= 0.0 { let space = " ".repeat(config.mode.width()); stdout .queue(cursor::MoveTo(e.x as u16, e.y as u16))? @@ -131,14 +131,14 @@ impl Stream { let should_regenerate = match config.direction { Direction::Down => e.y >= rows as f32, Direction::Up => e.y < 0.0, - Direction::Right => e.x + e.width as f32 >= columns as f32, - Direction::Left => e.x + (e.width as f32) < 0.0, + Direction::Right => e.x + e.width() as f32 >= columns as f32, + Direction::Left => e.x + (e.width() as f32) < 0.0, Direction::DiagonalLeft => e.x < 0.0 && e.y >= rows as f32, Direction::DiagonalLeftReverse => { - e.x + e.width as f32 >= columns as f32 && e.y < 0.0 + e.x + e.width() as f32 >= columns as f32 && e.y < 0.0 } Direction::DiagonalRight => { - e.x + e.width as f32 >= columns as f32 && e.y >= rows as f32 + e.x + e.width() as f32 >= columns as f32 && e.y >= rows as f32 } Direction::DiagonalRightReverse => e.x < 0.0 && e.y < 0.0, };