use rand::Rng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuzzyDirection {
Horizontal,
Vertical,
Both,
}
#[derive(Debug, Clone)]
pub struct FuzzyTextConfig {
pub base_intensity: f32,
pub hover_intensity: f32,
pub fuzz_range: f32,
pub direction: FuzzyDirection,
pub transition_duration: f64,
pub glitch_mode: bool,
pub glitch_interval: f64,
pub glitch_duration: f64,
}
impl Default for FuzzyTextConfig {
fn default() -> Self {
Self {
base_intensity: 0.18,
hover_intensity: 0.5,
fuzz_range: 30.0,
direction: FuzzyDirection::Horizontal,
transition_duration: 0.0,
glitch_mode: false,
glitch_interval: 2.0,
glitch_duration: 0.2,
}
}
}
#[derive(Debug, Clone, Copy)]
enum InteractionState {
Base,
Hovering,
Clicking,
Glitching,
}
#[derive(Debug)]
pub struct FuzzyTextState {
current_intensity: f32,
target_intensity: f32,
interaction_state: InteractionState,
glitch_elapsed: f64,
is_glitch_active: bool,
}
impl FuzzyTextState {
pub fn new(base_intensity: f32) -> Self {
Self {
current_intensity: base_intensity,
target_intensity: base_intensity,
interaction_state: InteractionState::Base,
glitch_elapsed: 0.0,
is_glitch_active: false,
}
}
pub fn update(&mut self, delta_time: f64, config: &FuzzyTextConfig) -> f32 {
if config.glitch_mode {
self.glitch_elapsed += delta_time;
let cycle_duration = config.glitch_interval + config.glitch_duration;
let cycle_time = self.glitch_elapsed % cycle_duration;
self.is_glitch_active = cycle_time >= config.glitch_interval;
}
self.target_intensity = match self.interaction_state {
InteractionState::Clicking => 1.0,
InteractionState::Glitching if self.is_glitch_active => 1.0,
InteractionState::Hovering => config.hover_intensity,
_ => config.base_intensity,
};
if config.transition_duration > 0.0 {
let step = (delta_time / config.transition_duration) as f32;
if self.current_intensity < self.target_intensity {
self.current_intensity =
(self.current_intensity + step).min(self.target_intensity);
} else if self.current_intensity > self.target_intensity {
self.current_intensity =
(self.current_intensity - step).max(self.target_intensity);
}
} else {
self.current_intensity = self.target_intensity;
}
self.current_intensity
}
pub fn calculate_displacements(
&self,
num_rows: usize,
config: &FuzzyTextConfig,
) -> Vec<(f32, f32)> {
let mut rng = rand::thread_rng();
let intensity = self.current_intensity;
(0..num_rows)
.map(|_| {
let dx = if matches!(
config.direction,
FuzzyDirection::Horizontal | FuzzyDirection::Both
) {
intensity * (rng.gen::<f32>() - 0.5) * config.fuzz_range
} else {
0.0
};
let dy = if matches!(
config.direction,
FuzzyDirection::Vertical | FuzzyDirection::Both
) {
intensity * (rng.gen::<f32>() - 0.5) * config.fuzz_range * 0.5
} else {
0.0
};
(dx, dy)
})
.collect()
}
pub fn set_hovering(&mut self, hovering: bool) {
if hovering {
self.interaction_state = InteractionState::Hovering;
} else {
self.interaction_state = InteractionState::Base;
}
}
pub fn set_clicking(&mut self, clicking: bool) {
if clicking {
self.interaction_state = InteractionState::Clicking;
} else {
self.interaction_state = InteractionState::Base;
}
}
pub fn set_glitching(&mut self, enabled: bool) {
if enabled {
self.interaction_state = InteractionState::Glitching;
} else if matches!(self.interaction_state, InteractionState::Glitching) {
self.interaction_state = InteractionState::Base;
}
}
pub fn intensity(&self) -> f32 {
self.current_intensity
}
pub fn reset(&mut self, base_intensity: f32) {
self.current_intensity = base_intensity;
self.target_intensity = base_intensity;
self.interaction_state = InteractionState::Base;
self.glitch_elapsed = 0.0;
self.is_glitch_active = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_intensity() {
let config = FuzzyTextConfig::default();
let mut state = FuzzyTextState::new(config.base_intensity);
let intensity = state.update(0.0, &config);
assert!((intensity - config.base_intensity).abs() < 0.01);
}
#[test]
fn test_hover_intensity() {
let config = FuzzyTextConfig {
transition_duration: 0.0, ..Default::default()
};
let mut state = FuzzyTextState::new(config.base_intensity);
state.set_hovering(true);
let intensity = state.update(0.0, &config);
assert!((intensity - config.hover_intensity).abs() < 0.01);
state.set_hovering(false);
let intensity = state.update(0.0, &config);
assert!((intensity - config.base_intensity).abs() < 0.01);
}
#[test]
fn test_click_intensity() {
let config = FuzzyTextConfig {
transition_duration: 0.0,
..Default::default()
};
let mut state = FuzzyTextState::new(config.base_intensity);
state.set_clicking(true);
let intensity = state.update(0.0, &config);
assert!((intensity - 1.0).abs() < 0.01);
}
#[test]
fn test_transition_duration() {
let config = FuzzyTextConfig {
base_intensity: 0.0,
hover_intensity: 1.0,
transition_duration: 1.0,
..Default::default()
};
let mut state = FuzzyTextState::new(0.0);
state.set_hovering(true);
let i1 = state.update(0.5, &config);
assert!(i1 > 0.4 && i1 < 0.6);
let i2 = state.update(0.5, &config);
assert!((i2 - 1.0).abs() < 0.1);
}
#[test]
fn test_displacements_horizontal() {
let config = FuzzyTextConfig {
direction: FuzzyDirection::Horizontal,
fuzz_range: 30.0,
..Default::default()
};
let mut state = FuzzyTextState::new(1.0); state.update(0.0, &config);
let displacements = state.calculate_displacements(10, &config);
assert_eq!(displacements.len(), 10);
for (dx, dy) in displacements {
assert_ne!(dx, 0.0); assert_eq!(dy, 0.0);
}
}
#[test]
fn test_displacements_vertical() {
let config = FuzzyTextConfig {
direction: FuzzyDirection::Vertical,
fuzz_range: 30.0,
..Default::default()
};
let mut state = FuzzyTextState::new(1.0);
state.update(0.0, &config);
let displacements = state.calculate_displacements(10, &config);
for (dx, dy) in displacements {
assert_eq!(dx, 0.0);
assert_ne!(dy, 0.0); }
}
#[test]
fn test_displacements_both() {
let config = FuzzyTextConfig {
direction: FuzzyDirection::Both,
fuzz_range: 30.0,
..Default::default()
};
let mut state = FuzzyTextState::new(1.0);
state.update(0.0, &config);
let displacements = state.calculate_displacements(10, &config);
let has_dx = displacements.iter().any(|(dx, _)| *dx != 0.0);
let has_dy = displacements.iter().any(|(_, dy)| *dy != 0.0);
assert!(has_dx);
assert!(has_dy);
}
#[test]
fn test_glitch_mode() {
let config = FuzzyTextConfig {
glitch_mode: true,
glitch_interval: 1.0,
glitch_duration: 0.5,
transition_duration: 0.0,
..Default::default()
};
let mut state = FuzzyTextState::new(config.base_intensity);
state.set_glitching(true);
let i1 = state.update(0.5, &config);
assert!((i1 - config.base_intensity).abs() < 0.1);
let i2 = state.update(0.6, &config);
assert!((i2 - 1.0).abs() < 0.1);
let i3 = state.update(0.5, &config);
assert!((i3 - config.base_intensity).abs() < 0.1);
}
#[test]
fn test_reset() {
let config = FuzzyTextConfig::default();
let mut state = FuzzyTextState::new(config.base_intensity);
state.set_hovering(true);
state.update(0.5, &config);
state.reset(config.base_intensity);
assert!((state.current_intensity - config.base_intensity).abs() < 0.01);
assert!(matches!(
state.interaction_state,
InteractionState::Base
));
}
}