#[derive(Debug, Clone)]
pub struct ScrollRevealConfig {
pub enable_blur: bool,
pub base_opacity: f32,
pub base_rotation: f32,
pub blur_strength: f32,
pub stagger: f32,
}
impl Default for ScrollRevealConfig {
fn default() -> Self {
Self {
enable_blur: true,
base_opacity: 0.1,
base_rotation: 3.0,
blur_strength: 4.0,
stagger: 0.05,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct WordState {
pub opacity: f32,
pub blur: f32,
}
pub struct ScrollReveal {
config: ScrollRevealConfig,
word_count: usize,
}
impl ScrollReveal {
pub fn new(word_count: usize, config: ScrollRevealConfig) -> Self {
Self { config, word_count }
}
pub fn with_word_count(word_count: usize) -> Self {
Self::new(word_count, ScrollRevealConfig::default())
}
pub fn compute_rotation(&self, scroll_progress: f32) -> f32 {
let progress = scroll_progress.clamp(0.0, 1.0);
self.config.base_rotation * (1.0 - progress)
}
pub fn compute_word_states(&self, scroll_progress: f32) -> Vec<WordState> {
let progress = scroll_progress.clamp(0.0, 1.0);
(0..self.word_count)
.map(|index| {
let word_progress = self.compute_word_progress(progress, index);
WordState {
opacity: self.interpolate_opacity(word_progress),
blur: if self.config.enable_blur {
self.interpolate_blur(word_progress)
} else {
0.0
},
}
})
.collect()
}
fn compute_word_progress(&self, scroll_progress: f32, word_index: usize) -> f32 {
let stagger_offset = word_index as f32 * self.config.stagger;
let total_stagger = (self.word_count - 1) as f32 * self.config.stagger;
let adjusted_progress = scroll_progress * (1.0 + total_stagger);
(adjusted_progress - stagger_offset).clamp(0.0, 1.0)
}
fn interpolate_opacity(&self, progress: f32) -> f32 {
self.config.base_opacity + (1.0 - self.config.base_opacity) * progress
}
fn interpolate_blur(&self, progress: f32) -> f32 {
self.config.blur_strength * (1.0 - progress)
}
pub fn set_config(&mut self, config: ScrollRevealConfig) {
self.config = config;
}
pub fn config(&self) -> &ScrollRevealConfig {
&self.config
}
pub fn set_word_count(&mut self, count: usize) {
self.word_count = count;
}
pub fn word_count(&self) -> usize {
self.word_count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rotation_interpolation() {
let reveal = ScrollReveal::with_word_count(5);
assert_eq!(reveal.compute_rotation(0.0), 3.0);
assert_eq!(reveal.compute_rotation(1.0), 0.0);
assert!((reveal.compute_rotation(0.5) - 1.5).abs() < 0.001);
}
#[test]
fn test_word_states_at_start() {
let reveal = ScrollReveal::with_word_count(3);
let states = reveal.compute_word_states(0.0);
assert_eq!(states.len(), 3);
assert!((states[0].opacity - 0.1).abs() < 0.001);
assert!((states[0].blur - 4.0).abs() < 0.001);
}
#[test]
fn test_word_states_at_end() {
let reveal = ScrollReveal::with_word_count(3);
let states = reveal.compute_word_states(1.0);
assert_eq!(states.len(), 3);
for state in &states {
assert!((state.opacity - 1.0).abs() < 0.001);
assert!(state.blur < 0.001);
}
}
#[test]
fn test_stagger_effect() {
let reveal = ScrollReveal::with_word_count(3);
let states = reveal.compute_word_states(0.3);
assert!(states[0].opacity > states[1].opacity);
assert!(states[1].opacity > states[2].opacity);
assert!(states[0].blur < states[1].blur);
assert!(states[1].blur < states[2].blur);
}
#[test]
fn test_blur_disabled() {
let config = ScrollRevealConfig {
enable_blur: false,
..Default::default()
};
let reveal = ScrollReveal::new(3, config);
let states = reveal.compute_word_states(0.0);
for state in &states {
assert_eq!(state.blur, 0.0);
}
}
#[test]
fn test_custom_config() {
let config = ScrollRevealConfig {
enable_blur: true,
base_opacity: 0.3,
base_rotation: 5.0,
blur_strength: 10.0,
stagger: 0.1,
};
let reveal = ScrollReveal::new(2, config);
assert_eq!(reveal.compute_rotation(0.0), 5.0);
let states = reveal.compute_word_states(0.0);
assert!((states[0].opacity - 0.3).abs() < 0.001);
assert!((states[0].blur - 10.0).abs() < 0.001);
}
}