use crate::animation::Easing;
use std::any::Any;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Instant;
pub type Callback = Rc<dyn Fn()>;
pub type LoopCallback = Rc<dyn Fn(u32)>;
pub type ActCallback = Rc<dyn Fn(&str)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StaggerOrder {
#[default]
Forward,
Reverse,
CenterOut,
EdgesIn,
Random,
}
impl StaggerOrder {
pub fn delay_factor(&self, index: usize, count: usize) -> f64 {
if count <= 1 {
return 0.0;
}
let max_idx = count - 1;
match self {
StaggerOrder::Forward => index as f64 / max_idx as f64,
StaggerOrder::Reverse => (max_idx - index) as f64 / max_idx as f64,
StaggerOrder::CenterOut => {
let center = (count - 1) as f64 / 2.0;
let distance = (index as f64 - center).abs();
let max_distance = center;
if max_distance > 0.0 {
distance / max_distance
} else {
0.0
}
}
StaggerOrder::EdgesIn => {
let center = (count - 1) as f64 / 2.0;
let distance = (index as f64 - center).abs();
let max_distance = center;
if max_distance > 0.0 {
1.0 - (distance / max_distance)
} else {
0.0
}
}
StaggerOrder::Random => {
let hash = (index.wrapping_mul(2654435761) ^ index.wrapping_mul(0x5bd1e995)) % 1000;
hash as f64 / 999.0
}
}
}
}
#[derive(Clone)]
pub struct StaggerConfig<T: Animatable> {
pub count: usize,
pub delay: f64,
pub order: StaggerOrder,
pub from: T,
pub to: T,
pub easing: Easing,
}
impl<T: Animatable> StaggerConfig<T> {
pub fn new(count: usize, from: T, to: T) -> Self {
Self {
count,
delay: 0.1, order: StaggerOrder::Forward,
from,
to,
easing: Easing::EaseOutCubic,
}
}
pub fn delay(mut self, delay: f64) -> Self {
self.delay = delay.clamp(0.0, 1.0);
self
}
pub fn order(mut self, order: StaggerOrder) -> Self {
self.order = order;
self
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn value_at(&self, index: usize, t: f64) -> T {
if self.count == 0 {
return self.from.clone();
}
let delay_factor = self.order.delay_factor(index, self.count);
let total_stagger_time = self.delay * (self.count - 1) as f64;
let item_start = delay_factor * total_stagger_time;
let item_duration = 1.0 - total_stagger_time;
if item_duration <= 0.0 {
return if t >= item_start {
self.to.clone()
} else {
self.from.clone()
};
}
let local_t = if t < item_start {
0.0
} else if t >= item_start + item_duration {
1.0
} else {
(t - item_start) / item_duration
};
let eased_t = self.easing.apply(local_t);
T::lerp(&self.from, &self.to, eased_t)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spring {
pub stiffness: f64,
pub damping: f64,
pub mass: f64,
}
impl Spring {
pub fn new(stiffness: f64, damping: f64) -> Self {
Self {
stiffness,
damping,
mass: 1.0,
}
}
pub fn with_mass(stiffness: f64, damping: f64, mass: f64) -> Self {
Self {
stiffness,
damping,
mass,
}
}
pub fn preset_gentle() -> Self {
Self::new(120.0, 20.0)
}
pub fn preset_bouncy() -> Self {
Self::new(180.0, 12.0)
}
pub fn preset_stiff() -> Self {
Self::new(300.0, 30.0)
}
pub fn preset_slow() -> Self {
Self::new(80.0, 20.0)
}
fn damping_ratio(&self) -> f64 {
self.damping / (2.0 * (self.stiffness * self.mass).sqrt())
}
fn natural_frequency(&self) -> f64 {
(self.stiffness / self.mass).sqrt()
}
pub fn evaluate(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
let zeta = self.damping_ratio();
let omega0 = self.natural_frequency();
if zeta < 1.0 {
let omega_d = omega0 * (1.0 - zeta * zeta).sqrt();
let decay = (-zeta * omega0 * t).exp();
let oscillation = (omega_d * t).cos() + (zeta * omega0 / omega_d) * (omega_d * t).sin();
1.0 - decay * oscillation
} else if (zeta - 1.0).abs() < 0.0001 {
let decay = (-omega0 * t).exp();
1.0 - decay * (1.0 + omega0 * t)
} else {
let sqrt_term = (zeta * zeta - 1.0).sqrt();
let r1 = -omega0 * (zeta - sqrt_term);
let r2 = -omega0 * (zeta + sqrt_term);
let c2 = -r1 / (r2 - r1);
let c1 = 1.0 - c2;
1.0 - c1 * (r1 * t).exp() - c2 * (r2 * t).exp()
}
}
pub fn settling_time(&self) -> f64 {
let zeta = self.damping_ratio();
let omega0 = self.natural_frequency();
if zeta < 1.0 {
4.6 / (zeta * omega0)
} else {
4.6 / omega0
}
}
}
impl Default for Spring {
fn default() -> Self {
Self::preset_gentle()
}
}
pub trait Animatable: Clone + Send + Sync + 'static {
fn lerp(a: &Self, b: &Self, t: f64) -> Self;
}
impl Animatable for f32 {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
a + (b - a) * t as f32
}
}
impl Animatable for f64 {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
a + (b - a) * t
}
}
impl Animatable for i32 {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(*a as f64 + (*b as f64 - *a as f64) * t).round() as i32
}
}
impl Animatable for u8 {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(*a as f64 + (*b as f64 - *a as f64) * t).round() as u8
}
}
impl Animatable for (f32, f32) {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(f32::lerp(&a.0, &b.0, t), f32::lerp(&a.1, &b.1, t))
}
}
impl Animatable for (f64, f64) {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(f64::lerp(&a.0, &b.0, t), f64::lerp(&a.1, &b.1, t))
}
}
impl Animatable for (u8, u8, u8) {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(
u8::lerp(&a.0, &b.0, t),
u8::lerp(&a.1, &b.1, t),
u8::lerp(&a.2, &b.2, t),
)
}
}
impl Animatable for (u8, u8, u8, u8) {
fn lerp(a: &Self, b: &Self, t: f64) -> Self {
(
u8::lerp(&a.0, &b.0, t),
u8::lerp(&a.1, &b.1, t),
u8::lerp(&a.2, &b.2, t),
u8::lerp(&a.3, &b.3, t),
)
}
}
#[derive(Clone)]
struct Keyframe<T: Animatable> {
time: f64,
value: T,
easing: Easing,
}
trait AnyTrack: Send + Sync {
fn value_at(&self, t: f64) -> Box<dyn Any + Send + Sync>;
fn clone_box(&self) -> Box<dyn AnyTrack>;
}
#[derive(Clone)]
pub struct Track<T: Animatable> {
keyframes: Vec<Keyframe<T>>,
}
impl<T: Animatable> Track<T> {
pub fn new() -> Self {
Self {
keyframes: Vec::new(),
}
}
pub fn from_to(from: T, to: T, easing: Easing) -> Self {
Self {
keyframes: vec![
Keyframe {
time: 0.0,
value: from,
easing: Easing::Linear,
},
Keyframe {
time: 1.0,
value: to,
easing,
},
],
}
}
pub fn keyframe(mut self, time: f64, value: T, easing: Easing) -> Self {
self.keyframes.push(Keyframe {
time,
value,
easing,
});
self.keyframes
.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
self
}
pub fn value_at(&self, t: f64) -> T {
if self.keyframes.is_empty() {
panic!("Track has no keyframes");
}
let t = t.clamp(0.0, 1.0);
let mut prev_idx = 0;
let mut next_idx = 0;
for (i, kf) in self.keyframes.iter().enumerate() {
if kf.time <= t {
prev_idx = i;
}
if kf.time >= t && next_idx == 0 {
next_idx = i;
break;
}
}
if next_idx == 0 {
next_idx = self.keyframes.len() - 1;
}
let prev = &self.keyframes[prev_idx];
let next = &self.keyframes[next_idx];
if prev_idx == next_idx || (next.time - prev.time).abs() < f64::EPSILON {
return prev.value.clone();
}
let local_t = (t - prev.time) / (next.time - prev.time);
let eased_t = next.easing.apply(local_t);
T::lerp(&prev.value, &next.value, eased_t)
}
}
impl<T: Animatable> Default for Track<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Animatable> AnyTrack for Track<T> {
fn value_at(&self, t: f64) -> Box<dyn Any + Send + Sync> {
Box::new(self.value_at(t))
}
fn clone_box(&self) -> Box<dyn AnyTrack> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct SpringTrack<T: Animatable> {
from: T,
to: T,
spring: Spring,
}
impl<T: Animatable> SpringTrack<T> {
pub fn new(from: T, to: T, spring: Spring) -> Self {
Self { from, to, spring }
}
pub fn value_at(&self, t: f64) -> T {
let t = t.clamp(0.0, 1.0);
let spring_t = t * self.spring.settling_time();
let spring_progress = self.spring.evaluate(spring_t);
T::lerp(&self.from, &self.to, spring_progress)
}
}
impl<T: Animatable> AnyTrack for SpringTrack<T> {
fn value_at(&self, t: f64) -> Box<dyn Any + Send + Sync> {
Box::new(self.value_at(t))
}
fn clone_box(&self) -> Box<dyn AnyTrack> {
Box::new(self.clone())
}
}
trait AnyStaggerTrack: Send + Sync {
fn value_at_index(&self, index: usize, t: f64) -> Box<dyn Any + Send + Sync>;
fn count(&self) -> usize;
fn clone_box(&self) -> Box<dyn AnyStaggerTrack>;
}
#[derive(Clone)]
pub struct StaggerTrack<T: Animatable> {
config: StaggerConfig<T>,
}
impl<T: Animatable> StaggerTrack<T> {
pub fn new(config: StaggerConfig<T>) -> Self {
Self { config }
}
pub fn simple(count: usize, from: T, to: T, easing: Easing) -> Self {
Self {
config: StaggerConfig::new(count, from, to).easing(easing),
}
}
pub fn value_at(&self, index: usize, t: f64) -> T {
self.config.value_at(index, t)
}
pub fn count(&self) -> usize {
self.config.count
}
}
impl<T: Animatable> AnyStaggerTrack for StaggerTrack<T> {
fn value_at_index(&self, index: usize, t: f64) -> Box<dyn Any + Send + Sync> {
Box::new(self.value_at(index, t))
}
fn count(&self) -> usize {
self.config.count
}
fn clone_box(&self) -> Box<dyn AnyStaggerTrack> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn AnyStaggerTrack> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Clone)]
pub struct Act {
name: String,
duration: f64,
tracks: HashMap<String, Box<dyn AnyTrack>>,
stagger_tracks: HashMap<String, Box<dyn AnyStaggerTrack>>,
on_enter: Option<Callback>,
on_exit: Option<Callback>,
}
impl Act {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
duration: 1.0,
tracks: HashMap::new(),
stagger_tracks: HashMap::new(),
on_enter: None,
on_exit: None,
}
}
pub fn hold(name: impl Into<String>, duration: f64) -> Self {
Self {
name: name.into(),
duration,
tracks: HashMap::new(),
stagger_tracks: HashMap::new(),
on_enter: None,
on_exit: None,
}
}
pub fn transition(name: impl Into<String>, duration: f64) -> Self {
Self {
name: name.into(),
duration,
tracks: HashMap::new(),
stagger_tracks: HashMap::new(),
on_enter: None,
on_exit: None,
}
}
pub fn duration(mut self, seconds: f64) -> Self {
self.duration = seconds;
self
}
pub fn animate<T: Animatable>(
mut self,
property: impl Into<String>,
from: T,
to: T,
easing: Easing,
) -> Self {
let track = Track::from_to(from, to, easing);
self.tracks.insert(property.into(), Box::new(track));
self
}
pub fn track<T: Animatable>(mut self, property: impl Into<String>, track: Track<T>) -> Self {
self.tracks.insert(property.into(), Box::new(track));
self
}
pub fn spring<T: Animatable>(
mut self,
property: impl Into<String>,
from: T,
to: T,
spring: Spring,
) -> Self {
let track = SpringTrack::new(from, to, spring);
self.tracks.insert(property.into(), Box::new(track));
self
}
pub fn spring_track<T: Animatable>(
mut self,
property: impl Into<String>,
track: SpringTrack<T>,
) -> Self {
self.tracks.insert(property.into(), Box::new(track));
self
}
pub fn stagger<T: Animatable>(
mut self,
property: impl Into<String>,
count: usize,
from: T,
to: T,
easing: Easing,
) -> Self {
let track = StaggerTrack::simple(count, from, to, easing);
self.stagger_tracks.insert(property.into(), Box::new(track));
self
}
pub fn stagger_config<T: Animatable>(
mut self,
property: impl Into<String>,
config: StaggerConfig<T>,
) -> Self {
let track = StaggerTrack::new(config);
self.stagger_tracks.insert(property.into(), Box::new(track));
self
}
pub fn stagger_track<T: Animatable>(
mut self,
property: impl Into<String>,
track: StaggerTrack<T>,
) -> Self {
self.stagger_tracks.insert(property.into(), Box::new(track));
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn get_duration(&self) -> f64 {
self.duration
}
fn get_value(&self, property: &str, t: f64) -> Option<Box<dyn Any + Send + Sync>> {
self.tracks.get(property).map(|track| track.value_at(t))
}
fn get_stagger_value(
&self,
property: &str,
index: usize,
t: f64,
) -> Option<Box<dyn Any + Send + Sync>> {
self.stagger_tracks
.get(property)
.map(|track| track.value_at_index(index, t))
}
fn get_stagger_count(&self, property: &str) -> Option<usize> {
self.stagger_tracks.get(property).map(|track| track.count())
}
pub fn has_stagger(&self, property: &str) -> bool {
self.stagger_tracks.contains_key(property)
}
pub fn on_enter<F: Fn() + 'static>(mut self, callback: F) -> Self {
self.on_enter = Some(Rc::new(callback));
self
}
pub fn on_exit<F: Fn() + 'static>(mut self, callback: F) -> Self {
self.on_exit = Some(Rc::new(callback));
self
}
fn fire_enter(&self) {
if let Some(ref cb) = self.on_enter {
cb();
}
}
fn fire_exit(&self) {
if let Some(ref cb) = self.on_exit {
cb();
}
}
}
impl Clone for Box<dyn AnyTrack> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum LoopBehavior {
None,
Loop,
LoopFrom(String),
}
#[derive(Clone)]
pub struct Timeline {
acts: Vec<Act>,
loop_behavior: LoopBehavior,
total_duration: f64,
loop_start_time: f64,
on_loop: Option<LoopCallback>,
on_act_enter: Option<ActCallback>,
on_act_exit: Option<ActCallback>,
}
impl Timeline {
pub fn new() -> Self {
Self {
acts: Vec::new(),
loop_behavior: LoopBehavior::None,
total_duration: 0.0,
loop_start_time: 0.0,
on_loop: None,
on_act_enter: None,
on_act_exit: None,
}
}
pub fn act(mut self, act: Act) -> Self {
self.total_duration += act.duration;
self.acts.push(act);
self
}
pub fn loop_forever(mut self) -> Self {
self.loop_behavior = LoopBehavior::Loop;
self
}
pub fn loop_from(mut self, act_name: impl Into<String>) -> Self {
let name = act_name.into();
let mut time = 0.0;
for act in &self.acts {
if act.name == name {
self.loop_start_time = time;
break;
}
time += act.duration;
}
self.loop_behavior = LoopBehavior::LoopFrom(name);
self
}
pub fn on_loop<F: Fn(u32) + 'static>(mut self, callback: F) -> Self {
self.on_loop = Some(Rc::new(callback));
self
}
pub fn on_act_enter<F: Fn(&str) + 'static>(mut self, callback: F) -> Self {
self.on_act_enter = Some(Rc::new(callback));
self
}
pub fn on_act_exit<F: Fn(&str) + 'static>(mut self, callback: F) -> Self {
self.on_act_exit = Some(Rc::new(callback));
self
}
pub fn then(mut self, other: Timeline) -> Self {
for act in other.acts {
self.total_duration += act.duration;
self.acts.push(act);
}
self.loop_behavior = other.loop_behavior;
if self.on_loop.is_none() {
self.on_loop = other.on_loop;
}
if self.on_act_enter.is_none() {
self.on_act_enter = other.on_act_enter;
}
if self.on_act_exit.is_none() {
self.on_act_exit = other.on_act_exit;
}
if let LoopBehavior::LoopFrom(ref name) = self.loop_behavior {
let mut time = 0.0;
for act in &self.acts {
if &act.name == name {
self.loop_start_time = time;
break;
}
time += act.duration;
}
}
self
}
pub fn duration(&self) -> f64 {
self.total_duration
}
pub fn act_count(&self) -> usize {
self.acts.len()
}
pub fn at(&self, time: f64) -> TimelineState<'_> {
if self.acts.is_empty() {
return TimelineState::empty();
}
let effective_time = match &self.loop_behavior {
LoopBehavior::None => time.min(self.total_duration),
LoopBehavior::Loop => {
if self.total_duration > 0.0 {
time % self.total_duration
} else {
0.0
}
}
LoopBehavior::LoopFrom(_) => {
if time < self.total_duration {
time
} else {
let loop_duration = self.total_duration - self.loop_start_time;
if loop_duration > 0.0 {
let overflow = time - self.total_duration;
self.loop_start_time + (overflow % loop_duration)
} else {
self.loop_start_time
}
}
}
};
let mut accumulated_time = 0.0;
let mut current_act_idx = 0;
let mut act_start_time = 0.0;
for (i, act) in self.acts.iter().enumerate() {
let act_end_time = accumulated_time + act.duration;
if effective_time < act_end_time || i == self.acts.len() - 1 {
current_act_idx = i;
act_start_time = accumulated_time;
break;
}
accumulated_time = act_end_time;
}
let current_act = &self.acts[current_act_idx];
let time_in_act = effective_time - act_start_time;
let act_progress = if current_act.duration > 0.0 {
(time_in_act / current_act.duration).clamp(0.0, 1.0)
} else {
1.0
};
TimelineState {
time: effective_time,
act_name: current_act.name.clone(),
act_index: current_act_idx,
act_progress,
act: current_act,
}
}
pub fn start(&self) -> PlayingTimeline {
PlayingTimeline {
timeline: self.clone(),
start_time: Instant::now(),
paused: false,
paused_at: 0.0,
speed: 1.0,
last_act_index: None,
loop_count: 0,
last_elapsed: 0.0,
}
}
}
impl Default for Timeline {
fn default() -> Self {
Self::new()
}
}
pub struct TimelineState<'a> {
pub time: f64,
pub act_name: String,
pub act_index: usize,
pub act_progress: f64,
act: &'a Act,
}
impl TimelineState<'static> {
fn empty() -> Self {
let act: &'static Act = Box::leak(Box::new(Act::new("empty").duration(0.0)));
Self {
time: 0.0,
act_name: String::new(),
act_index: 0,
act_progress: 0.0,
act,
}
}
}
impl<'a> TimelineState<'a> {
pub fn get<T: Animatable + Clone>(&self, property: &str) -> Option<T> {
self.act
.get_value(property, self.act_progress)
.and_then(|boxed| boxed.downcast::<T>().ok())
.map(|v| *v)
}
pub fn get_or<T: Animatable + Clone>(&self, property: &str, default: T) -> T {
self.get(property).unwrap_or(default)
}
pub fn get_stagger<T: Animatable + Clone>(&self, property: &str, index: usize) -> Option<T> {
self.act
.get_stagger_value(property, index, self.act_progress)
.and_then(|boxed| boxed.downcast::<T>().ok())
.map(|v| *v)
}
pub fn get_stagger_or<T: Animatable + Clone>(
&self,
property: &str,
index: usize,
default: T,
) -> T {
self.get_stagger(property, index).unwrap_or(default)
}
pub fn get_stagger_all<T: Animatable + Clone>(&self, property: &str, default: T) -> Vec<T> {
let count = self.act.get_stagger_count(property).unwrap_or(0);
(0..count)
.map(|i| {
self.get_stagger(property, i)
.unwrap_or_else(|| default.clone())
})
.collect()
}
pub fn stagger_count(&self, property: &str) -> usize {
self.act.get_stagger_count(property).unwrap_or(0)
}
}
#[derive(Clone)]
pub struct PlayingTimeline {
timeline: Timeline,
start_time: Instant,
paused: bool,
paused_at: f64,
speed: f64,
last_act_index: Option<usize>,
loop_count: u32,
last_elapsed: f64,
}
impl PlayingTimeline {
pub fn state(&self) -> TimelineState<'_> {
let time = if self.paused {
self.paused_at
} else {
self.start_time.elapsed().as_secs_f64() * self.speed
};
self.timeline.at(time)
}
pub fn get<T: Animatable + Clone>(&self, property: &str) -> Option<T> {
self.state().get(property)
}
pub fn get_or<T: Animatable + Clone>(&self, property: &str, default: T) -> T {
self.state().get_or(property, default)
}
pub fn get_stagger<T: Animatable + Clone>(&self, property: &str, index: usize) -> Option<T> {
self.state().get_stagger(property, index)
}
pub fn get_stagger_or<T: Animatable + Clone>(
&self,
property: &str,
index: usize,
default: T,
) -> T {
self.state().get_stagger_or(property, index, default)
}
pub fn get_stagger_all<T: Animatable + Clone>(&self, property: &str, default: T) -> Vec<T> {
self.state().get_stagger_all(property, default)
}
pub fn stagger_count(&self, property: &str) -> usize {
self.state().stagger_count(property)
}
pub fn elapsed(&self) -> f64 {
if self.paused {
self.paused_at
} else {
self.start_time.elapsed().as_secs_f64() * self.speed
}
}
pub fn current_act(&self) -> String {
self.state().act_name
}
pub fn act_progress(&self) -> f64 {
self.state().act_progress
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn is_playing(&self) -> bool {
!self.paused
}
pub fn pause(&mut self) {
if !self.paused {
self.paused_at = self.start_time.elapsed().as_secs_f64() * self.speed;
self.paused = true;
}
}
pub fn play(&mut self) {
if self.paused {
self.start_time =
Instant::now() - std::time::Duration::from_secs_f64(self.paused_at / self.speed);
self.paused = false;
}
}
pub fn toggle_pause(&mut self) {
if self.paused {
self.play();
} else {
self.pause();
}
}
pub fn seek(&mut self, time: f64) {
if self.paused {
self.paused_at = time;
} else {
self.start_time =
Instant::now() - std::time::Duration::from_secs_f64(time / self.speed);
}
}
pub fn restart(&mut self) {
self.start_time = Instant::now();
self.paused_at = 0.0;
}
pub fn set_speed(&mut self, speed: f64) {
let current_time = self.elapsed();
self.speed = speed;
self.seek(current_time);
}
pub fn speed(&self) -> f64 {
self.speed
}
pub fn duration(&self) -> f64 {
self.timeline.duration()
}
pub fn progress(&self) -> f64 {
let duration = self.timeline.duration();
if duration > 0.0 {
(self.elapsed() / duration).clamp(0.0, 1.0)
} else {
1.0
}
}
pub fn loop_count(&self) -> u32 {
self.loop_count
}
pub fn update(&mut self) -> bool {
let mut fired = false;
let state = self.state();
let current_act_index = state.act_index;
let current_elapsed = self.elapsed();
let looped = match self.timeline.loop_behavior {
LoopBehavior::Loop | LoopBehavior::LoopFrom(_) => {
current_elapsed < self.last_elapsed && self.last_elapsed > 0.0
}
LoopBehavior::None => false,
};
if looped {
self.loop_count += 1;
if let Some(ref cb) = self.timeline.on_loop {
cb(self.loop_count);
fired = true;
}
}
if self.last_act_index != Some(current_act_index) {
if let Some(prev_idx) = self.last_act_index {
if prev_idx < self.timeline.acts.len() {
let prev_act = &self.timeline.acts[prev_idx];
prev_act.fire_exit();
if let Some(ref cb) = self.timeline.on_act_exit {
cb(&prev_act.name);
}
fired = true;
}
}
if current_act_index < self.timeline.acts.len() {
let current_act = &self.timeline.acts[current_act_index];
current_act.fire_enter();
if let Some(ref cb) = self.timeline.on_act_enter {
cb(¤t_act.name);
}
fired = true;
}
self.last_act_index = Some(current_act_index);
}
self.last_elapsed = current_elapsed;
fired
}
}
#[derive(Debug, Clone)]
pub struct TimelineDebugInfo {
pub duration: f64,
pub elapsed: f64,
pub progress: f64,
pub current_act: String,
pub act_index: usize,
pub act_count: usize,
pub act_progress: f64,
pub act_duration: f64,
pub is_paused: bool,
pub speed: f64,
pub loop_count: u32,
pub loop_behavior: String,
pub acts: Vec<(String, f64)>,
}
impl TimelineDebugInfo {
pub fn to_compact_string(&self) -> String {
format!(
"[{:.2}s/{:.2}s] {} ({:.0}%) {} {:.1}x",
self.elapsed,
self.duration,
self.current_act,
self.act_progress * 100.0,
if self.is_paused { "PAUSED" } else { "PLAYING" },
self.speed,
)
}
pub fn to_debug_string(&self) -> String {
let mut lines = vec![
format!("Timeline Debug Info"),
format!("=================="),
format!(
"Time: {:.2}s / {:.2}s ({:.1}%)",
self.elapsed,
self.duration,
self.progress * 100.0
),
format!(
"Act: {} [{}/{}]",
self.current_act,
self.act_index + 1,
self.act_count
),
format!("Act Progress: {:.1}%", self.act_progress * 100.0),
format!(
"Status: {} at {:.1}x speed",
if self.is_paused { "Paused" } else { "Playing" },
self.speed
),
format!("Loop: {} (count: {})", self.loop_behavior, self.loop_count),
format!(""),
format!("Acts:"),
];
let mut time_offset = 0.0;
for (i, (name, duration)) in self.acts.iter().enumerate() {
let marker = if i == self.act_index { ">>>" } else { " " };
lines.push(format!(
"{} {:2}. {:20} ({:.1}s) @ {:.1}s",
marker,
i + 1,
name,
duration,
time_offset
));
time_offset += duration;
}
lines.join("\n")
}
pub fn progress_bar(&self, width: usize) -> String {
let filled = (self.progress * width as f64) as usize;
let empty = width.saturating_sub(filled);
format!("[{}{}]", "=".repeat(filled), "-".repeat(empty))
}
pub fn act_visualization(&self, width: usize) -> String {
if self.duration == 0.0 || self.acts.is_empty() {
return format!("[{}]", "-".repeat(width));
}
let mut result = String::new();
for (i, (_name, duration)) in self.acts.iter().enumerate() {
let act_width = ((duration / self.duration) * width as f64).round() as usize;
let act_width = act_width.max(1);
let char_to_use = if i == self.act_index { '=' } else { '-' };
let segment = char_to_use.to_string().repeat(act_width);
if !result.is_empty() {
result.push('|');
}
result.push_str(&segment);
}
let pos = ((self.elapsed / self.duration) * width as f64) as usize;
let pos = pos.min(width.saturating_sub(1));
let mut chars: Vec<char> = result.chars().collect();
if pos < chars.len() {
chars[pos] = '*';
}
format!("[{}]", chars.into_iter().collect::<String>())
}
}
impl PlayingTimeline {
pub fn debug_info(&self) -> TimelineDebugInfo {
let state = self.state();
let acts: Vec<(String, f64)> = self
.timeline
.acts
.iter()
.map(|a| (a.name.clone(), a.duration))
.collect();
let loop_behavior = match &self.timeline.loop_behavior {
LoopBehavior::None => "None".to_string(),
LoopBehavior::Loop => "Loop forever".to_string(),
LoopBehavior::LoopFrom(name) => format!("Loop from '{}'", name),
};
let act_duration = self
.timeline
.acts
.get(state.act_index)
.map(|a| a.duration)
.unwrap_or(0.0);
TimelineDebugInfo {
duration: self.timeline.duration(),
elapsed: self.elapsed(),
progress: self.progress(),
current_act: state.act_name,
act_index: state.act_index,
act_count: self.timeline.acts.len(),
act_progress: state.act_progress,
act_duration,
is_paused: self.paused,
speed: self.speed,
loop_count: self.loop_count,
loop_behavior,
acts,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_animatable_f64() {
assert_eq!(f64::lerp(&0.0, &100.0, 0.0), 0.0);
assert_eq!(f64::lerp(&0.0, &100.0, 0.5), 50.0);
assert_eq!(f64::lerp(&0.0, &100.0, 1.0), 100.0);
}
#[test]
fn test_animatable_tuple() {
let a = (0.0f64, 0.0f64);
let b = (100.0f64, 200.0f64);
let mid = <(f64, f64)>::lerp(&a, &b, 0.5);
assert_eq!(mid, (50.0, 100.0));
}
#[test]
fn test_track_from_to() {
let track = Track::from_to(0.0f64, 100.0, Easing::Linear);
assert_eq!(track.value_at(0.0), 0.0);
assert_eq!(track.value_at(0.5), 50.0);
assert_eq!(track.value_at(1.0), 100.0);
}
#[test]
fn test_track_keyframes() {
let track = Track::new()
.keyframe(0.0, 0.0f64, Easing::Linear)
.keyframe(0.5, 100.0, Easing::Linear)
.keyframe(1.0, 50.0, Easing::Linear);
assert_eq!(track.value_at(0.0), 0.0);
assert_eq!(track.value_at(0.5), 100.0);
assert_eq!(track.value_at(1.0), 50.0);
assert_eq!(track.value_at(0.25), 50.0); assert_eq!(track.value_at(0.75), 75.0); }
#[test]
fn test_act_simple() {
let act = Act::new("fade")
.duration(2.0)
.animate("opacity", 0.0f64, 1.0, Easing::Linear);
assert_eq!(act.name(), "fade");
assert_eq!(act.get_duration(), 2.0);
}
#[test]
fn test_timeline_single_act() {
let timeline = Timeline::new().act(Act::new("fade").duration(2.0).animate(
"opacity",
0.0f64,
1.0,
Easing::Linear,
));
let state = timeline.at(0.0);
assert_eq!(state.act_name, "fade");
assert_eq!(state.get::<f64>("opacity"), Some(0.0));
let state = timeline.at(1.0);
assert_eq!(state.get::<f64>("opacity"), Some(0.5));
let state = timeline.at(2.0);
assert_eq!(state.get::<f64>("opacity"), Some(1.0));
}
#[test]
fn test_timeline_multiple_acts() {
let timeline = Timeline::new()
.act(
Act::new("fade_in")
.duration(1.0)
.animate("opacity", 0.0f64, 1.0, Easing::Linear),
)
.act(Act::new("hold").duration(1.0))
.act(Act::new("fade_out").duration(1.0).animate(
"opacity",
1.0f64,
0.0,
Easing::Linear,
));
let state = timeline.at(0.5);
assert_eq!(state.act_name, "fade_in");
assert_eq!(state.get::<f64>("opacity"), Some(0.5));
let state = timeline.at(1.5);
assert_eq!(state.act_name, "hold");
assert_eq!(state.get::<f64>("opacity"), None);
let state = timeline.at(2.5);
assert_eq!(state.act_name, "fade_out");
assert_eq!(state.get::<f64>("opacity"), Some(0.5));
}
#[test]
fn test_timeline_loop() {
let timeline = Timeline::new()
.act(Act::new("a").duration(1.0))
.act(Act::new("b").duration(1.0))
.loop_forever();
assert_eq!(timeline.at(0.5).act_name, "a");
assert_eq!(timeline.at(1.5).act_name, "b");
assert_eq!(timeline.at(2.5).act_name, "a"); assert_eq!(timeline.at(3.5).act_name, "b"); }
#[test]
fn test_timeline_loop_from() {
let timeline = Timeline::new()
.act(Act::new("intro").duration(1.0))
.act(Act::new("loop_a").duration(1.0))
.act(Act::new("loop_b").duration(1.0))
.loop_from("loop_a");
assert_eq!(timeline.at(0.5).act_name, "intro");
assert_eq!(timeline.at(1.5).act_name, "loop_a");
assert_eq!(timeline.at(2.5).act_name, "loop_b");
assert_eq!(timeline.at(3.5).act_name, "loop_a");
assert_eq!(timeline.at(4.5).act_name, "loop_b");
}
#[test]
fn test_playing_timeline() {
let timeline = Timeline::new().act(Act::new("test").duration(1.0).animate(
"value",
0.0f64,
100.0,
Easing::Linear,
));
let mut playing = timeline.start();
playing.pause();
assert!(playing.is_paused());
playing.play();
assert!(playing.is_playing());
playing.seek(0.5);
let value: f64 = playing.get_or("value", 0.0);
assert!((value - 50.0).abs() < 1.0);
playing.restart();
assert!(playing.elapsed() < 0.1);
}
#[test]
fn test_act_callbacks() {
use std::cell::Cell;
use std::rc::Rc;
let entered = Rc::new(Cell::new(false));
let exited = Rc::new(Cell::new(false));
let entered_clone = entered.clone();
let exited_clone = exited.clone();
let timeline = Timeline::new()
.act(
Act::new("first")
.duration(1.0)
.on_enter(move || entered_clone.set(true))
.on_exit(move || exited_clone.set(true)),
)
.act(Act::new("second").duration(1.0));
let mut playing = timeline.start();
playing.update();
assert!(entered.get(), "on_enter should fire on first act");
assert!(!exited.get(), "on_exit should not fire yet");
playing.seek(1.5);
playing.update();
assert!(exited.get(), "on_exit should fire when leaving first act");
}
#[test]
fn test_timeline_callbacks() {
use std::cell::RefCell;
use std::rc::Rc;
let act_entered = Rc::new(RefCell::new(String::new()));
let loop_count = Rc::new(RefCell::new(0u32));
let act_entered_clone = act_entered.clone();
let loop_count_clone = loop_count.clone();
let timeline = Timeline::new()
.act(Act::new("a").duration(0.5))
.act(Act::new("b").duration(0.5))
.loop_forever()
.on_act_enter(move |name| *act_entered_clone.borrow_mut() = name.to_string())
.on_loop(move |count| *loop_count_clone.borrow_mut() = count);
let mut playing = timeline.start();
playing.update();
assert_eq!(*act_entered.borrow(), "a");
playing.seek(0.6);
playing.update();
assert_eq!(*act_entered.borrow(), "b");
playing.seek(1.1); playing.update();
playing.seek(0.1); playing.update();
assert_eq!(*act_entered.borrow(), "a");
}
#[test]
fn test_spring_evaluate() {
let spring = Spring::preset_bouncy();
assert!((spring.evaluate(0.0) - 0.0).abs() < 0.001);
let settling = spring.settling_time();
assert!((spring.evaluate(settling) - 1.0).abs() < 0.05);
}
#[test]
fn test_spring_presets() {
let gentle = Spring::preset_gentle();
let bouncy = Spring::preset_bouncy();
let stiff = Spring::preset_stiff();
let slow = Spring::preset_slow();
for spring in [gentle, bouncy, stiff, slow] {
let settling = spring.settling_time();
let final_val = spring.evaluate(settling);
assert!(
(final_val - 1.0).abs() < 0.1,
"Spring should settle near target"
);
}
}
#[test]
fn test_spring_track() {
let track = SpringTrack::new(0.0f64, 100.0, Spring::preset_gentle());
assert!((track.value_at(0.0) - 0.0).abs() < 0.1);
assert!((track.value_at(1.0) - 100.0).abs() < 5.0);
}
#[test]
fn test_act_with_spring() {
let timeline = Timeline::new().act(Act::new("bounce").duration(1.0).spring(
"position",
0.0f64,
100.0,
Spring::preset_bouncy(),
));
let state = timeline.at(0.0);
let pos: f64 = state.get("position").unwrap();
assert!((pos - 0.0).abs() < 0.1);
let state = timeline.at(1.0);
let pos: f64 = state.get("position").unwrap();
assert!((pos - 100.0).abs() < 5.0);
}
#[test]
fn test_stagger_order_forward() {
let order = StaggerOrder::Forward;
assert_eq!(order.delay_factor(0, 5), 0.0);
assert_eq!(order.delay_factor(2, 5), 0.5);
assert_eq!(order.delay_factor(4, 5), 1.0);
}
#[test]
fn test_stagger_order_reverse() {
let order = StaggerOrder::Reverse;
assert_eq!(order.delay_factor(0, 5), 1.0);
assert_eq!(order.delay_factor(2, 5), 0.5);
assert_eq!(order.delay_factor(4, 5), 0.0);
}
#[test]
fn test_stagger_order_center_out() {
let order = StaggerOrder::CenterOut;
assert_eq!(order.delay_factor(2, 5), 0.0); assert_eq!(order.delay_factor(0, 5), 1.0); assert_eq!(order.delay_factor(4, 5), 1.0);
}
#[test]
fn test_stagger_order_edges_in() {
let order = StaggerOrder::EdgesIn;
assert_eq!(order.delay_factor(0, 5), 0.0); assert_eq!(order.delay_factor(4, 5), 0.0);
assert_eq!(order.delay_factor(2, 5), 1.0); }
#[test]
fn test_stagger_config_basic() {
let config = StaggerConfig::new(3, 0.0f64, 1.0)
.delay(0.2)
.order(StaggerOrder::Forward);
assert!((config.value_at(0, 0.0) - 0.0).abs() < 0.01);
assert!((config.value_at(1, 0.0) - 0.0).abs() < 0.01);
assert!((config.value_at(2, 0.0) - 0.0).abs() < 0.01);
assert!((config.value_at(0, 1.0) - 1.0).abs() < 0.01);
assert!((config.value_at(1, 1.0) - 1.0).abs() < 0.01);
assert!((config.value_at(2, 1.0) - 1.0).abs() < 0.01);
let v0 = config.value_at(0, 0.5);
let v1 = config.value_at(1, 0.5);
let v2 = config.value_at(2, 0.5);
assert!(v0 > v1);
assert!(v1 > v2);
}
#[test]
fn test_stagger_track() {
let track = StaggerTrack::simple(4, 0.0f64, 100.0, Easing::Linear);
assert_eq!(track.count(), 4);
for i in 0..4 {
assert!((track.value_at(i, 0.0) - 0.0).abs() < 0.1);
}
for i in 0..4 {
assert!((track.value_at(i, 1.0) - 100.0).abs() < 0.1);
}
}
#[test]
fn test_act_with_stagger() {
let timeline = Timeline::new().act(Act::new("panels_enter").duration(2.0).stagger(
"opacity",
3,
0.0f64,
1.0,
Easing::Linear,
));
let state = timeline.at(0.0);
assert_eq!(state.stagger_count("opacity"), 3);
assert!((state.get_stagger::<f64>("opacity", 0).unwrap() - 0.0).abs() < 0.01);
let state = timeline.at(2.0);
assert!((state.get_stagger::<f64>("opacity", 0).unwrap() - 1.0).abs() < 0.01);
assert!((state.get_stagger::<f64>("opacity", 1).unwrap() - 1.0).abs() < 0.01);
assert!((state.get_stagger::<f64>("opacity", 2).unwrap() - 1.0).abs() < 0.01);
}
#[test]
fn test_stagger_all() {
let timeline = Timeline::new().act(Act::new("fade").duration(1.0).stagger(
"alpha",
4,
0.0f64,
1.0,
Easing::Linear,
));
let state = timeline.at(1.0);
let values: Vec<f64> = state.get_stagger_all("alpha", 0.0);
assert_eq!(values.len(), 4);
for v in &values {
assert!((v - 1.0).abs() < 0.1);
}
}
#[test]
fn test_playing_timeline_stagger() {
let timeline = Timeline::new().act(Act::new("test").duration(1.0).stagger(
"value",
3,
0.0f64,
100.0,
Easing::Linear,
));
let mut playing = timeline.start();
playing.seek(1.0);
assert_eq!(playing.stagger_count("value"), 3);
let v0: f64 = playing.get_stagger_or("value", 0, 0.0);
let v1: f64 = playing.get_stagger_or("value", 1, 0.0);
let v2: f64 = playing.get_stagger_or("value", 2, 0.0);
assert!((v0 - 100.0).abs() < 1.0);
assert!((v1 - 100.0).abs() < 1.0);
assert!((v2 - 100.0).abs() < 1.0);
let all: Vec<f64> = playing.get_stagger_all("value", 0.0);
assert_eq!(all.len(), 3);
}
#[test]
fn test_debug_info_basic() {
let timeline = Timeline::new()
.act(Act::new("intro").duration(2.0))
.act(Act::new("main").duration(3.0))
.act(Act::new("outro").duration(1.0));
let mut playing = timeline.start();
playing.seek(2.5);
let debug = playing.debug_info();
assert_eq!(debug.duration, 6.0);
assert!((debug.elapsed - 2.5).abs() < 0.1);
assert_eq!(debug.current_act, "main");
assert_eq!(debug.act_index, 1);
assert_eq!(debug.act_count, 3);
assert_eq!(debug.acts.len(), 3);
assert_eq!(debug.loop_behavior, "None");
}
#[test]
fn test_debug_info_compact_string() {
let timeline = Timeline::new().act(Act::new("test").duration(1.0));
let mut playing = timeline.start();
playing.seek(0.5);
let debug = playing.debug_info();
let compact = debug.to_compact_string();
assert!(compact.contains("0.50s"));
assert!(compact.contains("1.00s"));
assert!(compact.contains("test"));
assert!(compact.contains("PLAYING"));
}
#[test]
fn test_debug_info_progress_bar() {
let timeline = Timeline::new().act(Act::new("test").duration(1.0));
let mut playing = timeline.start();
playing.seek(0.5);
let debug = playing.debug_info();
let bar = debug.progress_bar(10);
assert!(bar.starts_with('['));
assert!(bar.ends_with(']'));
assert_eq!(bar.len(), 12); }
#[test]
fn test_debug_info_loop() {
let timeline = Timeline::new()
.act(Act::new("a").duration(1.0))
.act(Act::new("b").duration(1.0))
.loop_from("a");
let playing = timeline.start();
let debug = playing.debug_info();
assert!(debug.loop_behavior.contains("Loop from"));
assert!(debug.loop_behavior.contains("'a'"));
}
}