use std::time::Instant;
#[derive(Debug, Clone)]
pub enum AnimKind {
Counter { from: u32, to: u32 },
Splash, Checkmark, }
#[derive(Debug, Clone)]
pub struct Animation {
pub kind: AnimKind,
pub started: Instant,
pub duration_ms: u64,
pub completed: bool,
}
impl Animation {
pub fn new(kind: AnimKind, duration_ms: u64) -> Self {
Self {
kind,
started: Instant::now(),
duration_ms,
completed: false,
}
}
#[allow(clippy::cast_precision_loss)]
pub fn progress(&self) -> f64 {
if self.completed {
return 1.0;
}
let elapsed = self.started.elapsed().as_millis() as f64;
let duration = self.duration_ms as f64;
let t = (elapsed / duration).clamp(0.0, 1.0);
(1.0 - t).powi(2).mul_add(-1.0, 1.0)
}
#[allow(clippy::cast_precision_loss)]
pub fn current_value_f64(&self) -> f64 {
let p = self.progress();
match &self.kind {
AnimKind::Counter { from, to } => {
let f = f64::from(*from);
let t = f64::from(*to);
(t - f).mul_add(p, f)
}
AnimKind::Splash => p,
AnimKind::Checkmark => {
let phase = (p * 3.0) % 2.0;
if phase < 1.0 { 1.0 } else { 0.0 }
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn current_value_u32(&self) -> u32 {
self.current_value_f64().round() as u32
}
pub fn is_done(&self) -> bool {
self.completed || self.started.elapsed().as_millis() >= u128::from(self.duration_ms)
}
}
pub struct AnimationState {
pub active: Vec<Animation>,
pub enabled: bool,
}
impl AnimationState {
pub const fn new(enabled: bool) -> Self {
Self {
active: Vec::new(),
enabled,
}
}
pub const fn active(&self) -> bool {
self.enabled && !self.active.is_empty()
}
pub fn step(&mut self) {
for anim in &mut self.active {
if anim.is_done() {
anim.completed = true;
}
}
self.active.retain(|a| !a.completed);
}
pub fn push(&mut self, anim: Animation) {
if self.enabled {
self.active.push(anim);
}
}
pub fn counter_value(&self) -> Option<u32> {
self.active
.iter()
.rev()
.find_map(|a| match &a.kind {
AnimKind::Counter { .. } => Some(a.current_value_u32()),
_ => None,
})
}
pub fn splash_opacity(&self) -> Option<f64> {
self.active
.iter()
.find_map(|a| match &a.kind {
AnimKind::Splash => Some(a.progress()),
_ => None,
})
}
pub fn start_splash(&mut self) {
self.push(Animation::new(AnimKind::Splash, 500));
}
pub fn start_checkmark(&mut self) {
self.push(Animation::new(AnimKind::Checkmark, 600));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anim_interpolation() {
let anim = Animation::new(
AnimKind::Counter { from: 0, to: 100 },
100, );
let v = anim.current_value_f64();
assert!(v >= 0.0 && v <= 100.0);
}
#[test]
fn anim_completion() {
let mut anim = Animation::new(
AnimKind::Counter { from: 50, to: 80 },
1, );
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(anim.is_done());
anim.completed = true;
assert_eq!(anim.progress(), 1.0);
assert_eq!(anim.current_value_u32(), 80);
}
#[test]
fn anim_gc() {
let mut state = AnimationState::new(true);
state.push(Animation::new(
AnimKind::Checkmark,
1, ));
assert!(!state.active.is_empty());
std::thread::sleep(std::time::Duration::from_millis(5));
state.step();
assert!(state.active.is_empty(), "Completed animations should be GC'd");
}
#[test]
fn anim_disabled_noop() {
let mut state = AnimationState::new(false);
state.push(Animation::new(
AnimKind::Counter { from: 0, to: 100 },
500,
));
assert!(state.active.is_empty(), "Disabled state should not accept animations");
assert!(!state.active(), "Disabled state should report inactive");
}
#[test]
fn splash_opacity_during_animation() {
let mut state = AnimationState::new(true);
state.start_splash();
assert!(state.splash_opacity().is_some());
let opacity = state.splash_opacity().unwrap();
assert!(opacity >= 0.0 && opacity <= 1.0);
}
#[test]
fn splash_completes_and_disappears() {
let mut state = AnimationState::new(true);
state.start_splash();
assert!(state.splash_opacity().is_some());
std::thread::sleep(std::time::Duration::from_millis(600)); state.step();
assert!(state.splash_opacity().is_none(), "Splash should be GC'd after completion");
}
}