#![forbid(unsafe_code)]
use std::time::Duration;
use super::group::AnimationGroup;
use super::stagger::{StaggerMode, stagger_offsets};
use super::{Animation, EasingFn, Fade, Sequence, Slide, delay, ease_in, ease_out, sequence};
#[must_use]
pub fn cascade_in(
count: usize,
item_duration: Duration,
stagger_delay: Duration,
mode: StaggerMode,
) -> AnimationGroup {
let offsets = stagger_offsets(count, stagger_delay, mode);
let mut group = AnimationGroup::new();
for (i, offset) in offsets.into_iter().enumerate() {
let anim = delay(offset, Fade::new(item_duration).easing(ease_out));
group.insert(&format!("item_{i}"), Box::new(anim));
}
group
}
#[must_use]
pub fn cascade_out(
count: usize,
item_duration: Duration,
stagger_delay: Duration,
mode: StaggerMode,
) -> AnimationGroup {
let offsets = stagger_offsets(count, stagger_delay, mode);
let mut group = AnimationGroup::new();
for (i, offset) in offsets.into_iter().enumerate() {
let anim = delay(
offset,
InvertedFade(Fade::new(item_duration).easing(ease_in)),
);
group.insert(&format!("item_{i}"), Box::new(anim));
}
group
}
#[must_use]
pub fn fan_out(count: usize, item_duration: Duration, total_spread: Duration) -> AnimationGroup {
if count == 0 {
return AnimationGroup::new();
}
let mut group = AnimationGroup::new();
for i in 0..count {
let center = (count as f64 - 1.0) / 2.0;
let dist = if count <= 1 {
0.0
} else {
((i as f64 - center).abs() / center).min(1.0)
};
let eased = 1.0 - (1.0 - dist) * (1.0 - dist);
let offset = total_spread.mul_f64(eased);
let anim = delay(offset, Fade::new(item_duration).easing(ease_out));
group.insert(&format!("item_{i}"), Box::new(anim));
}
group
}
#[must_use]
pub fn typewriter(char_count: usize, total_duration: Duration) -> TypewriterAnim {
TypewriterAnim {
char_count,
fade: Fade::new(total_duration),
}
}
#[derive(Debug, Clone)]
pub struct TypewriterAnim {
char_count: usize,
fade: Fade,
}
impl TypewriterAnim {
pub fn visible_chars(&self) -> usize {
let t = self.fade.value();
let count = (t * self.char_count as f32).round() as usize;
count.min(self.char_count)
}
}
impl Animation for TypewriterAnim {
fn tick(&mut self, dt: Duration) {
self.fade.tick(dt);
}
fn is_complete(&self) -> bool {
self.fade.is_complete()
}
fn value(&self) -> f32 {
self.fade.value()
}
fn reset(&mut self) {
self.fade.reset();
}
fn overshoot(&self) -> Duration {
self.fade.overshoot()
}
}
#[must_use]
pub fn pulse_sequence(
count: usize,
pulse_duration: Duration,
stagger_delay: Duration,
) -> AnimationGroup {
let offsets = stagger_offsets(count, stagger_delay, StaggerMode::Linear);
let mut group = AnimationGroup::new();
for (i, offset) in offsets.into_iter().enumerate() {
let anim = delay(offset, PulseOnce::new(pulse_duration));
group.insert(&format!("pulse_{i}"), Box::new(anim));
}
group
}
#[derive(Debug, Clone, Copy)]
struct PulseOnce {
elapsed: Duration,
duration: Duration,
}
impl PulseOnce {
fn new(duration: Duration) -> Self {
Self {
elapsed: Duration::ZERO,
duration: if duration.is_zero() {
Duration::from_nanos(1)
} else {
duration
},
}
}
}
impl Animation for PulseOnce {
fn tick(&mut self, dt: Duration) {
self.elapsed = self.elapsed.saturating_add(dt);
}
fn is_complete(&self) -> bool {
self.elapsed >= self.duration
}
fn value(&self) -> f32 {
let t = (self.elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0) as f32;
(t * std::f32::consts::PI).sin()
}
fn reset(&mut self) {
self.elapsed = Duration::ZERO;
}
fn overshoot(&self) -> Duration {
self.elapsed.saturating_sub(self.duration)
}
}
#[must_use]
pub fn slide_in_left(distance: i16, duration: Duration) -> Slide {
Slide::new(-distance, 0, duration).easing(ease_out)
}
#[must_use]
pub fn slide_in_right(distance: i16, duration: Duration) -> Slide {
Slide::new(distance, 0, duration).easing(ease_out)
}
#[must_use]
pub fn fade_through(half_duration: Duration) -> Sequence<InvertedFade, Fade> {
let out = InvertedFade(Fade::new(half_duration).easing(ease_in));
let into = Fade::new(half_duration).easing(ease_out);
sequence(out, into)
}
#[derive(Debug, Clone, Copy)]
pub struct InvertedFade(Fade);
impl InvertedFade {
pub fn new(duration: Duration) -> Self {
Self(Fade::new(duration))
}
pub fn easing(mut self, easing: EasingFn) -> Self {
self.0 = self.0.easing(easing);
self
}
}
impl Animation for InvertedFade {
fn tick(&mut self, dt: Duration) {
self.0.tick(dt);
}
fn is_complete(&self) -> bool {
self.0.is_complete()
}
fn value(&self) -> f32 {
1.0 - self.0.value()
}
fn reset(&mut self) {
self.0.reset();
}
fn overshoot(&self) -> Duration {
self.0.overshoot()
}
}
#[cfg(test)]
mod tests {
use super::*;
const MS50: Duration = Duration::from_millis(50);
const MS100: Duration = Duration::from_millis(100);
const MS200: Duration = Duration::from_millis(200);
const MS500: Duration = Duration::from_millis(500);
#[test]
fn cascade_in_empty() {
let group = cascade_in(0, MS200, MS50, StaggerMode::Linear);
assert!(group.is_empty());
assert!(group.all_complete());
}
#[test]
fn cascade_in_single_item() {
let mut group = cascade_in(1, MS200, MS50, StaggerMode::Linear);
assert_eq!(group.len(), 1);
assert!(!group.all_complete());
group.tick(MS200);
assert!(group.all_complete());
}
#[test]
fn cascade_in_multiple_items_staggered() {
let mut group = cascade_in(3, MS200, MS100, StaggerMode::Linear);
assert_eq!(group.len(), 3);
assert!(group.get("item_0").unwrap().value() == 0.0);
group.tick(MS100);
let v0 = group.get("item_0").unwrap().value();
let v1 = group.get("item_1").unwrap().value();
let v2 = group.get("item_2").unwrap().value();
assert!(v0 > 0.0, "item_0 should have progressed");
assert!(v1 == 0.0, "item_1 just started (delay elapsed)");
assert!(v2 == 0.0, "item_2 hasn't started yet");
group.tick(Duration::from_millis(300));
assert!(group.all_complete());
}
#[test]
fn cascade_in_values_increase() {
let mut group = cascade_in(5, MS500, MS100, StaggerMode::EaseOut);
let mut prev = 0.0f32;
for _ in 0..20 {
group.tick(MS50);
let val = group.overall_progress();
assert!(val >= prev, "overall progress should not decrease");
prev = val;
}
}
#[test]
fn cascade_out_starts_near_one() {
let mut group = cascade_out(3, MS200, MS50, StaggerMode::Linear);
group.tick(Duration::from_nanos(1));
let v0 = group.get("item_0").unwrap().value();
assert!(
(v0 - 1.0).abs() < 0.01,
"cascade_out should start near 1.0, got {v0}"
);
}
#[test]
fn cascade_out_ends_at_zero() {
let mut group = cascade_out(3, MS200, MS50, StaggerMode::Linear);
group.tick(Duration::from_secs(1));
for i in 0..3 {
let v = group.get(&format!("item_{i}")).unwrap().value();
assert!(
v < 0.01,
"item_{i} should be near 0.0 after completion, got {v}"
);
}
}
#[test]
fn fan_out_empty() {
let group = fan_out(0, MS200, MS200);
assert!(group.is_empty());
}
#[test]
fn fan_out_single() {
let group = fan_out(1, MS200, MS200);
assert_eq!(group.len(), 1);
let v = group.get("item_0").unwrap().value();
assert!((v - 0.0).abs() < 0.01);
}
#[test]
fn fan_out_center_starts_first() {
let mut group = fan_out(5, MS200, MS200);
group.tick(Duration::from_millis(10));
let center = group.get("item_2").unwrap().value();
let edge = group.get("item_0").unwrap().value();
assert!(
center >= edge,
"center ({center}) should start before edges ({edge})"
);
}
#[test]
fn fan_out_symmetric() {
let group = fan_out(5, MS200, MS200);
let v0 = group.get("item_0").unwrap().value();
let v4 = group.get("item_4").unwrap().value();
assert!(
(v0 - v4).abs() < 0.01,
"symmetric items should match: {v0} vs {v4}"
);
let v1 = group.get("item_1").unwrap().value();
let v3 = group.get("item_3").unwrap().value();
assert!(
(v1 - v3).abs() < 0.01,
"symmetric items should match: {v1} vs {v3}"
);
}
#[test]
fn typewriter_starts_at_zero() {
let tw = typewriter(100, MS500);
assert_eq!(tw.visible_chars(), 0);
}
#[test]
fn typewriter_ends_at_full() {
let mut tw = typewriter(100, MS500);
tw.tick(MS500);
assert_eq!(tw.visible_chars(), 100);
assert!(tw.is_complete());
}
#[test]
fn typewriter_progresses_monotonically() {
let mut tw = typewriter(50, MS500);
let mut prev = 0;
for _ in 0..20 {
tw.tick(Duration::from_millis(25));
let chars = tw.visible_chars();
assert!(
chars >= prev,
"visible chars should not decrease: {prev} -> {chars}"
);
prev = chars;
}
}
#[test]
fn typewriter_zero_chars() {
let mut tw = typewriter(0, MS200);
assert_eq!(tw.visible_chars(), 0);
tw.tick(MS200);
assert_eq!(tw.visible_chars(), 0);
assert!(tw.is_complete());
}
#[test]
fn pulse_sequence_empty() {
let group = pulse_sequence(0, MS200, MS100);
assert!(group.is_empty());
}
#[test]
fn pulse_sequence_peaks_then_returns() {
let mut group = pulse_sequence(1, MS200, MS100);
assert!(group.get("pulse_0").unwrap().value() < 0.01);
group.tick(MS100);
let mid = group.get("pulse_0").unwrap().value();
assert!(mid > 0.9, "pulse midpoint should be near 1.0, got {mid}");
group.tick(MS100);
let end = group.get("pulse_0").unwrap().value();
assert!(end < 0.1, "pulse end should be near 0.0, got {end}");
}
#[test]
fn pulse_sequence_items_staggered() {
let mut group = pulse_sequence(3, MS200, MS200);
group.tick(MS100);
let p0 = group.get("pulse_0").unwrap().value();
let p1 = group.get("pulse_1").unwrap().value();
assert!(p0 > 0.9, "pulse_0 should be at peak");
assert!(p1 < 0.01, "pulse_1 should not have started");
}
#[test]
fn slide_in_left_starts_offscreen() {
let slide = slide_in_left(20, MS200);
assert_eq!(slide.position(), -20);
}
#[test]
fn slide_in_left_ends_at_zero() {
let mut slide = slide_in_left(20, MS200);
slide.tick(MS200);
assert_eq!(slide.position(), 0);
assert!(slide.is_complete());
}
#[test]
fn slide_in_right_starts_offscreen() {
let slide = slide_in_right(20, MS200);
assert_eq!(slide.position(), 20);
}
#[test]
fn slide_in_right_ends_at_zero() {
let mut slide = slide_in_right(20, MS200);
slide.tick(MS200);
assert_eq!(slide.position(), 0);
}
#[test]
fn fade_through_starts_at_one() {
let ft = fade_through(MS200);
assert!((ft.value() - 1.0).abs() < 0.01, "should start at 1.0");
}
#[test]
fn fade_through_midpoint_near_zero() {
let mut ft = fade_through(MS200);
ft.tick(MS200);
assert!(
ft.value() < 0.1,
"midpoint should be near 0.0, got {}",
ft.value()
);
}
#[test]
fn fade_through_ends_at_one() {
let mut ft = fade_through(MS200);
ft.tick(Duration::from_millis(400));
assert!(ft.is_complete());
assert!(
(ft.value() - 1.0).abs() < 0.01,
"should end at 1.0, got {}",
ft.value()
);
}
#[test]
fn inverted_fade_starts_at_one() {
let f = InvertedFade::new(MS200);
assert!((f.value() - 1.0).abs() < 0.001);
}
#[test]
fn inverted_fade_ends_at_zero() {
let mut f = InvertedFade::new(MS200);
f.tick(MS200);
assert!(f.value() < 0.001);
assert!(f.is_complete());
}
#[test]
fn inverted_fade_reset() {
let mut f = InvertedFade::new(MS200);
f.tick(MS200);
assert!(f.is_complete());
f.reset();
assert!(!f.is_complete());
assert!((f.value() - 1.0).abs() < 0.001);
}
#[test]
fn cascade_in_deterministic() {
let run = || {
let mut group = cascade_in(5, MS200, MS50, StaggerMode::EaseInOut);
let mut values = Vec::new();
for _ in 0..10 {
group.tick(MS50);
values.push(group.overall_progress());
}
values
};
assert_eq!(run(), run(), "cascade_in must be deterministic");
}
#[test]
fn typewriter_deterministic() {
let run = || {
let mut tw = typewriter(100, MS500);
let mut counts = Vec::new();
for _ in 0..20 {
tw.tick(Duration::from_millis(25));
counts.push(tw.visible_chars());
}
counts
};
assert_eq!(run(), run(), "typewriter must be deterministic");
}
#[test]
fn fan_out_deterministic() {
let run = || {
let mut group = fan_out(7, MS200, MS200);
let mut values = Vec::new();
for _ in 0..10 {
group.tick(MS50);
values.push(group.overall_progress());
}
values
};
assert_eq!(run(), run(), "fan_out must be deterministic");
}
}