Skip to main content

tachyonfx/fx/
glitch.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::{fmt::Debug, ops::Range};
3
4use bon::Builder;
5#[cfg(feature = "dsl")]
6use compact_str::ToCompactString;
7use ratatui_core::{
8    buffer::Buffer,
9    layout::{Position, Rect},
10};
11
12use crate::{
13    shader::Shader,
14    simple_rng::{RangeSampler, SimpleRng},
15    CellFilter, Duration, EffectTimer,
16};
17
18/// Type of glitch transformation to apply to a cell.
19#[derive(Clone, Debug)]
20pub enum GlitchType {
21    // fixme: make non-public again
22    ChangeCase,
23    ChangeCharByValue(i8),
24}
25
26/// A glitch effect that can be applied to a cell.
27#[derive(Builder, Clone, Debug)]
28pub struct GlitchCell {
29    cell_idx: usize,
30    glitch_remaining_ms: u32,
31    presleep_remaining_ms: u32,
32    glitch: GlitchType,
33}
34
35/// applies a glitch effect to random parts of the screen.
36#[derive(Builder, Clone, Debug)]
37pub struct Glitch {
38    cell_glitch_ratio: f32,
39    action_start_delay_ms: Range<u32>,
40    action_ms: Range<u32>,
41    #[builder(default)]
42    rng: SimpleRng,
43    #[builder(default)]
44    selection: CellFilter,
45
46    #[builder(skip)]
47    glitch_cells: Vec<GlitchCell>,
48    area: Option<Rect>,
49}
50
51impl Glitch {
52    fn ensure_population(&mut self, screen: Rect) {
53        let total_cells =
54            crate::math::round(screen.width as f32 * screen.height as f32 * self.cell_glitch_ratio)
55                as u32;
56
57        let current_population = self.glitch_cells.len() as u32;
58        if current_population < total_cells {
59            for _ in 0..(total_cells - current_population) {
60                let cell = GlitchCell::builder()
61                    .cell_idx(
62                        self.rng
63                            .gen_range(0..(screen.width * screen.height) as usize),
64                    )
65                    .glitch(self.glitch_type())
66                    .glitch_remaining_ms(self.rng.gen_range(self.action_ms.clone()))
67                    .presleep_remaining_ms(
68                        self.rng
69                            .gen_range(self.action_start_delay_ms.clone()),
70                    )
71                    .build();
72                self.glitch_cells.push(cell);
73            }
74        }
75    }
76
77    fn update_cell(cell: &mut GlitchCell, last_frame_ms: u32) {
78        let f = |v: u32, sub: u32| (v.saturating_sub(sub), sub.saturating_sub(v));
79
80        let (updated, remaining) = f(cell.presleep_remaining_ms, last_frame_ms);
81        cell.presleep_remaining_ms = updated;
82        cell.glitch_remaining_ms = cell.glitch_remaining_ms.saturating_sub(remaining);
83    }
84
85    fn is_running(cell: &GlitchCell) -> bool {
86        cell.glitch_remaining_ms > 0
87    }
88
89    fn glitch_type(&mut self) -> GlitchType {
90        let idx: u32 = self.rng.gen();
91        match idx % 2 {
92            0 => GlitchType::ChangeCase,
93            1 => GlitchType::ChangeCharByValue(-10 + self.rng.gen_range(0..20) as i8),
94            _ => unreachable!(),
95        }
96    }
97}
98
99impl Shader for Glitch {
100    fn name(&self) -> &'static str {
101        "glitch"
102    }
103
104    fn process(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) -> Option<Duration> {
105        // ensure glitch population meets the cell_glitch_ratio
106        self.ensure_population(area);
107
108        // subtract durations
109        let last_frame_ms = duration.as_millis();
110        self.glitch_cells
111            .iter_mut()
112            .for_each(|cell| Self::update_cell(cell, last_frame_ms as _));
113
114        // remove invalid cells (e.g., from resizing)
115        self.glitch_cells
116            .retain(|cell| cell.cell_idx < buf.content.len());
117
118        let predicate = self.selection.predicate(area);
119
120        // apply glitches to buffer
121        self.glitch_cells
122            .iter()
123            .filter(|c| c.presleep_remaining_ms == 0)
124            .for_each(|cell| {
125                let x = cell.cell_idx % area.width as usize;
126                let y = cell.cell_idx / area.width as usize;
127                let pos = Position::new(area.x + x as u16, area.y + y as u16);
128                let c = buf
129                    .cell_mut(Position::new(area.x + x as u16, area.y + y as u16))
130                    .unwrap();
131
132                if !predicate.is_valid(pos, c) {
133                    return;
134                }
135
136                match cell.glitch {
137                    GlitchType::ChangeCase if c.symbol().is_ascii() => {
138                        let ch = c.symbol().chars().next().unwrap();
139                        c.set_char(if ch.is_ascii_uppercase() {
140                            ch.to_ascii_lowercase()
141                        } else {
142                            ch.to_ascii_uppercase()
143                        });
144                    },
145                    GlitchType::ChangeCharByValue(v) if c.symbol().len() == 1 => {
146                        if c.symbol()
147                            .chars()
148                            .next()
149                            .is_some_and(|ch| ch == ' ')
150                        {
151                            return;
152                        }
153
154                        c.set_char(if v > 0 {
155                            c.symbol().as_bytes()[0]
156                                .saturating_add(v as u8)
157                                .clamp(32, 255) as char
158                        } else {
159                            c.symbol().as_bytes()[0]
160                                .saturating_sub(v.unsigned_abs())
161                                .clamp(32, 255) as char
162                        });
163                    },
164                    _ => {},
165                }
166            });
167
168        // remove expired glitches
169        self.glitch_cells.retain(Self::is_running);
170
171        None
172    }
173
174    fn done(&self) -> bool {
175        false
176    }
177
178    fn clone_box(&self) -> Box<dyn Shader> {
179        Box::new(self.clone())
180    }
181
182    fn area(&self) -> Option<Rect> {
183        self.area
184    }
185
186    fn set_area(&mut self, area: Rect) {
187        self.area = Some(area);
188    }
189
190    fn filter(&mut self, strategy: CellFilter) {
191        self.selection = strategy;
192    }
193
194    fn timer_mut(&mut self) -> Option<&mut EffectTimer> {
195        None
196    }
197
198    fn cell_filter(&self) -> Option<&CellFilter> {
199        Some(&self.selection)
200    }
201
202    fn reset(&mut self) {
203        self.glitch_cells.clear();
204    }
205
206    fn set_rng(&mut self, rng: SimpleRng) {
207        self.rng = rng;
208    }
209
210    #[cfg(feature = "dsl")]
211    fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
212        use crate::dsl::DslError;
213
214        Err(DslError::UnsupportedEffect { name: self.name().to_compact_string() })
215    }
216}