use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use animato_core::{Playable, Update};
use animato_tween::Loop;
use core::fmt;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum At<'a> {
Absolute(f32),
Start,
End,
Label(&'a str),
Offset(f32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimelineState {
Idle,
Playing,
Paused,
Completed,
}
struct TimelineEntry {
label: String,
animation: Box<dyn Playable + Send>,
start_at: f32,
duration: f32,
completed: bool,
}
impl fmt::Debug for TimelineEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimelineEntry")
.field("label", &self.label)
.field("start_at", &self.start_at)
.field("duration", &self.duration)
.field("completed", &self.completed)
.finish()
}
}
impl TimelineEntry {
fn end_at(&self) -> f32 {
self.start_at + self.duration
}
}
#[cfg(feature = "std")]
struct EntryCallback {
label: String,
callback: Box<dyn FnMut() + Send + 'static>,
}
#[cfg(feature = "std")]
impl fmt::Debug for EntryCallback {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EntryCallback")
.field("label", &self.label)
.finish()
}
}
#[cfg(feature = "std")]
#[derive(Default)]
struct TimelineCallbacks {
entry_complete: Vec<EntryCallback>,
complete: Option<Box<dyn FnMut() + Send + 'static>>,
complete_fired: bool,
}
#[cfg(feature = "std")]
impl fmt::Debug for TimelineCallbacks {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimelineCallbacks")
.field("entry_complete", &self.entry_complete)
.field("has_complete", &self.complete.is_some())
.field("complete_fired", &self.complete_fired)
.finish()
}
}
#[cfg(feature = "std")]
impl TimelineCallbacks {
fn fire_entry_complete(&mut self, completed_labels: &[String]) {
for completed_label in completed_labels {
for callback in self.entry_complete.iter_mut() {
if callback.label == *completed_label {
(callback.callback)();
}
}
}
}
fn fire_complete(&mut self) {
if self.complete_fired {
return;
}
self.complete_fired = true;
if let Some(callback) = self.complete.as_mut() {
callback();
}
}
fn reset_completion(&mut self) {
self.complete_fired = false;
}
}
pub struct Timeline {
entries: Vec<TimelineEntry>,
elapsed: f32,
state: TimelineState,
pub looping: Loop,
pub time_scale: f32,
#[cfg(feature = "std")]
callbacks: TimelineCallbacks,
#[cfg(feature = "tokio")]
completion_tx: tokio::sync::watch::Sender<bool>,
}
impl fmt::Debug for Timeline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Timeline")
.field("entries", &self.entries)
.field("elapsed", &self.elapsed)
.field("state", &self.state)
.field("looping", &self.looping)
.field("time_scale", &self.time_scale)
.field("callbacks", &{
#[cfg(feature = "std")]
{
&self.callbacks
}
#[cfg(not(feature = "std"))]
{
&"disabled"
}
})
.finish()
}
}
impl Default for Timeline {
fn default() -> Self {
Self::new()
}
}
impl Timeline {
pub fn new() -> Self {
Self {
entries: Vec::new(),
elapsed: 0.0,
state: TimelineState::Idle,
looping: Loop::Once,
time_scale: 1.0,
#[cfg(feature = "std")]
callbacks: TimelineCallbacks::default(),
#[cfg(feature = "tokio")]
completion_tx: tokio::sync::watch::channel(false).0,
}
}
pub fn add<A>(mut self, label: impl Into<String>, animation: A, at: At<'_>) -> Self
where
A: Playable + Send + 'static,
{
let start_at = self.resolve_start(at);
let duration = animation.duration().max(0.0);
self.entries.push(TimelineEntry {
label: label.into(),
animation: Box::new(animation),
start_at,
duration,
completed: false,
});
self
}
pub(crate) fn add_boxed_with_duration(
mut self,
label: impl Into<String>,
animation: Box<dyn Playable + Send>,
at: At<'_>,
duration: f32,
) -> Self {
let start_at = self.resolve_start(at);
self.entries.push(TimelineEntry {
label: label.into(),
animation,
start_at,
duration: duration.max(0.0),
completed: false,
});
self
}
pub fn looping(mut self, mode: Loop) -> Self {
self.looping = mode;
self
}
pub fn time_scale(mut self, scale: f32) -> Self {
self.set_time_scale(scale);
self
}
pub fn set_time_scale(&mut self, scale: f32) {
self.time_scale = scale.max(0.0);
}
#[cfg(feature = "std")]
pub fn on_entry_complete(
mut self,
label: impl Into<String>,
f: impl FnMut() + Send + 'static,
) -> Self {
self.callbacks.entry_complete.push(EntryCallback {
label: label.into(),
callback: Box::new(f),
});
self
}
#[cfg(feature = "std")]
pub fn on_complete(mut self, f: impl FnMut() + Send + 'static) -> Self {
self.callbacks.complete = Some(Box::new(f));
self
}
#[cfg(feature = "tokio")]
pub fn wait(&self) -> impl core::future::Future<Output = ()> + Send + 'static {
let mut rx = self.completion_tx.subscribe();
async move {
loop {
if *rx.borrow() {
return;
}
if rx.changed().await.is_err() {
return;
}
}
}
}
pub fn play(&mut self) {
if self.state == TimelineState::Completed {
self.reset();
}
if self.duration() == 0.0 {
self.state = TimelineState::Completed;
self.notify_completion_state(true);
} else {
self.state = TimelineState::Playing;
self.notify_completion_state(false);
self.sync_to_elapsed();
}
}
pub fn pause(&mut self) {
if self.state == TimelineState::Playing {
self.state = TimelineState::Paused;
}
}
pub fn resume(&mut self) {
if self.state == TimelineState::Paused {
self.state = TimelineState::Playing;
}
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.state = TimelineState::Idle;
self.reset_completion_callbacks();
self.notify_completion_state(false);
for entry in self.entries.iter_mut() {
entry.animation.reset();
entry.completed = false;
}
}
pub fn seek(&mut self, progress: f32) {
let total = self.playback_duration();
let seek_duration = if total.is_finite() {
total
} else {
self.duration()
};
self.seek_abs(seek_duration * progress.clamp(0.0, 1.0));
}
pub fn seek_abs(&mut self, secs: f32) {
let total = self.playback_duration();
let secs = secs.max(0.0);
self.elapsed = if total.is_finite() {
secs.min(total)
} else {
secs
};
self.sync_to_elapsed();
if total.is_finite() && self.elapsed >= total {
self.state = TimelineState::Completed;
self.notify_completion_state(true);
} else if self.state == TimelineState::Completed {
self.state = TimelineState::Playing;
self.notify_completion_state(false);
}
}
pub fn duration(&self) -> f32 {
self.entries
.iter()
.map(TimelineEntry::end_at)
.fold(0.0, f32::max)
}
pub fn progress(&self) -> f32 {
let total = self.playback_duration();
if total == 0.0 {
return 1.0;
}
if total.is_finite() {
(self.elapsed / total).clamp(0.0, 1.0)
} else {
let base = self.duration();
if base == 0.0 {
1.0
} else {
(self.local_time_for_elapsed(self.elapsed) / base).clamp(0.0, 1.0)
}
}
}
pub fn is_complete(&self) -> bool {
self.state == TimelineState::Completed
}
pub fn state(&self) -> TimelineState {
self.state
}
pub fn elapsed(&self) -> f32 {
self.elapsed
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn get<T>(&self, label: &str) -> Option<&T>
where
T: Playable + 'static,
{
self.entries
.iter()
.find(|entry| entry.label == label)
.and_then(|entry| entry.animation.as_any().downcast_ref::<T>())
}
pub fn get_mut<T>(&mut self, label: &str) -> Option<&mut T>
where
T: Playable + 'static,
{
self.entries
.iter_mut()
.find(|entry| entry.label == label)
.and_then(|entry| entry.animation.as_any_mut().downcast_mut::<T>())
}
fn fire_entry_callbacks(&mut self, completed_labels: &[String]) {
#[cfg(feature = "std")]
self.callbacks.fire_entry_complete(completed_labels);
#[cfg(not(feature = "std"))]
let _ = completed_labels;
}
fn fire_complete_callback(&mut self) {
#[cfg(feature = "std")]
self.callbacks.fire_complete();
}
fn reset_completion_callbacks(&mut self) {
#[cfg(feature = "std")]
self.callbacks.reset_completion();
}
fn notify_completion_state(&self, complete: bool) {
#[cfg(feature = "tokio")]
let _ = self.completion_tx.send_replace(complete);
#[cfg(not(feature = "tokio"))]
let _ = complete;
}
fn complete_from_update(&mut self) -> bool {
self.state = TimelineState::Completed;
self.fire_complete_callback();
self.notify_completion_state(true);
false
}
fn resolve_start(&self, at: At<'_>) -> f32 {
match at {
At::Absolute(secs) => secs.max(0.0),
At::Start => 0.0,
At::End => self.duration(),
At::Label(label) => self
.entries
.iter()
.find(|entry| entry.label == label)
.map_or_else(|| self.duration(), |entry| entry.start_at),
At::Offset(offset) => (self.duration() + offset).max(0.0),
}
}
fn playback_duration(&self) -> f32 {
let base = self.duration();
if base == 0.0 {
return 0.0;
}
match self.looping {
Loop::Once => base,
Loop::Times(n) => base * n.max(1) as f32,
Loop::Forever | Loop::PingPong => f32::INFINITY,
}
}
fn local_time_for_elapsed(&self, elapsed: f32) -> f32 {
let base = self.duration();
if base == 0.0 {
return 0.0;
}
match self.looping {
Loop::Once => elapsed.min(base),
Loop::Times(n) => {
let total = base * n.max(1) as f32;
if elapsed >= total {
base
} else {
elapsed % base
}
}
Loop::Forever => elapsed % base,
Loop::PingPong => {
let cycle = elapsed % (base * 2.0);
if cycle <= base {
cycle
} else {
base * 2.0 - cycle
}
}
}
}
fn entry_completion_labels_between(&self, prev: f32, next: f32, base: f32) -> Vec<String> {
let mut labels = Vec::new();
if next <= prev || base <= 0.0 {
return labels;
}
let (max_cycles, period) = match self.looping {
Loop::Once => (Some(1), base),
Loop::Times(n) => (Some(n.max(1)), base),
Loop::Forever => (None, base),
Loop::PingPong => (None, base * 2.0),
};
if period <= 0.0 {
return labels;
}
let mut cycle = (prev / period).max(0.0) as u32;
loop {
if let Some(max_cycles) = max_cycles {
if cycle >= max_cycles {
break;
}
}
let cycle_start = cycle as f32 * period;
if cycle_start > next {
break;
}
for entry in self.entries.iter() {
let completion = cycle_start + entry.end_at();
if prev < completion && completion <= next {
labels.push(entry.label.clone());
}
}
cycle = cycle.saturating_add(1);
if cycle == u32::MAX {
break;
}
}
labels
}
fn tick_forward(&mut self, prev: f32, next: f32) -> Vec<String> {
let mut completed_labels = Vec::new();
for entry in self.entries.iter_mut() {
let start = entry.start_at;
let end = entry.end_at();
let was_completed = entry.completed;
if next < start {
entry.animation.reset();
entry.completed = false;
continue;
}
if prev <= start && next >= start {
entry.animation.reset();
entry.completed = false;
}
if entry.duration == 0.0 {
if next >= start {
entry.animation.seek_to(1.0);
entry.completed = true;
}
if !was_completed && entry.completed {
completed_labels.push(entry.label.clone());
}
continue;
}
let overlap_start = prev.max(start);
let overlap_end = next.min(end);
if overlap_end > overlap_start {
let still_running = entry.animation.update(overlap_end - overlap_start);
if !still_running {
entry.completed = true;
}
}
if next >= end {
entry.animation.seek_to(1.0);
entry.completed = true;
}
if !was_completed && entry.completed {
completed_labels.push(entry.label.clone());
}
}
completed_labels
}
fn sync_to_elapsed(&mut self) {
let local_time = self.local_time_for_elapsed(self.elapsed);
for entry in self.entries.iter_mut() {
let start = entry.start_at;
let end = entry.end_at();
if local_time <= start {
entry.animation.reset();
entry.completed = false;
} else if local_time >= end || entry.duration == 0.0 {
entry.animation.seek_to(1.0);
entry.completed = true;
} else {
let progress = (local_time - start) / entry.duration;
entry.animation.seek_to(progress);
entry.completed = false;
}
}
}
}
impl Update for Timeline {
fn update(&mut self, dt: f32) -> bool {
match self.state {
TimelineState::Completed => return false,
TimelineState::Paused | TimelineState::Idle => return true,
TimelineState::Playing => {}
}
let base = self.duration();
if base == 0.0 {
return self.complete_from_update();
}
let dt = dt.max(0.0) * self.time_scale;
let previous_elapsed = self.elapsed;
let next_elapsed = previous_elapsed + dt;
match self.looping {
Loop::Once => {
let prev_local = previous_elapsed.min(base);
let next_local = next_elapsed.min(base);
let completed_labels = self.tick_forward(prev_local, next_local);
self.fire_entry_callbacks(&completed_labels);
self.elapsed = next_elapsed.min(base);
if next_elapsed >= base {
return self.complete_from_update();
}
}
Loop::Times(n) => {
let total = base * n.max(1) as f32;
let completed_labels =
self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
self.elapsed = next_elapsed.min(total);
self.sync_to_elapsed();
self.fire_entry_callbacks(&completed_labels);
if next_elapsed >= total {
return self.complete_from_update();
}
}
Loop::Forever | Loop::PingPong => {
let completed_labels =
self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
self.elapsed = next_elapsed;
self.sync_to_elapsed();
self.fire_entry_callbacks(&completed_labels);
}
}
true
}
}
impl Playable for Timeline {
fn duration(&self) -> f32 {
self.playback_duration()
}
fn reset(&mut self) {
Timeline::reset(self);
}
fn seek_to(&mut self, progress: f32) {
Timeline::seek(self, progress);
}
fn is_complete(&self) -> bool {
Timeline::is_complete(self)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use animato_core::Easing;
use animato_tween::Tween;
fn tween(end: f32, duration: f32) -> Tween<f32> {
Tween::new(0.0_f32, end)
.duration(duration)
.easing(Easing::Linear)
.build()
}
#[test]
fn concurrent_entries_advance_together() {
let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start).add(
"b",
tween(100.0, 1.0),
At::Label("a"),
);
timeline.play();
timeline.update(0.5);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.5);
assert_eq!(timeline.get::<Tween<f32>>("b").unwrap().value(), 50.0);
}
#[test]
fn end_and_offset_position_entries() {
let timeline = Timeline::new()
.add("first", tween(1.0, 1.0), At::Start)
.add("second", tween(1.0, 0.5), At::End)
.add("third", tween(1.0, 0.25), At::Offset(0.25));
assert_eq!(timeline.duration(), 2.0);
}
#[test]
fn seek_abs_synchronizes_children() {
let mut timeline = Timeline::new().add("a", tween(100.0, 2.0), At::Start);
timeline.seek_abs(0.5);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
}
#[test]
fn pause_stops_timeline_progress() {
let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
timeline.play();
timeline.update(0.25);
timeline.pause();
timeline.update(0.5);
assert_eq!(timeline.elapsed(), 0.25);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
}
#[test]
fn resume_continues_after_pause() {
let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
timeline.play();
timeline.update(0.25);
timeline.pause();
timeline.resume();
timeline.update(0.25);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
}
#[test]
fn once_timeline_completes() {
let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
timeline.play();
assert!(!timeline.update(1.0));
assert!(timeline.is_complete());
}
#[test]
fn times_loop_repeats_then_completes() {
let mut timeline = Timeline::new()
.add("a", tween(100.0, 1.0), At::Start)
.looping(Loop::Times(2));
timeline.play();
timeline.update(1.25);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
assert!(!timeline.update(1.0));
assert!(timeline.is_complete());
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 100.0);
}
#[test]
fn ping_pong_reflects_timeline_time() {
let mut timeline = Timeline::new()
.add("a", tween(100.0, 1.0), At::Start)
.looping(Loop::PingPong);
timeline.play();
timeline.update(1.25);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
assert!(!timeline.is_complete());
}
#[test]
fn time_scale_speeds_up_timeline() {
let mut timeline = Timeline::new()
.add("a", tween(100.0, 1.0), At::Start)
.time_scale(2.0);
timeline.play();
timeline.update(0.25);
assert_eq!(timeline.elapsed(), 0.5);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
}
#[test]
fn set_time_scale_clamps_negative_to_zero() {
let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
timeline.set_time_scale(-1.0);
timeline.play();
timeline.update(0.5);
assert_eq!(timeline.elapsed(), 0.0);
assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
}
#[cfg(feature = "std")]
#[test]
fn callbacks_fire_once_during_update() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let entry_count = Arc::new(AtomicUsize::new(0));
let complete_count = Arc::new(AtomicUsize::new(0));
let entry_seen = Arc::clone(&entry_count);
let complete_seen = Arc::clone(&complete_count);
let mut timeline = Timeline::new()
.add("a", tween(100.0, 1.0), At::Start)
.on_entry_complete("a", move || {
entry_seen.fetch_add(1, Ordering::SeqCst);
})
.on_complete(move || {
complete_seen.fetch_add(1, Ordering::SeqCst);
});
timeline.play();
assert!(!timeline.update(1.0));
assert!(!timeline.update(1.0));
assert_eq!(entry_count.load(Ordering::SeqCst), 1);
assert_eq!(complete_count.load(Ordering::SeqCst), 1);
}
#[cfg(feature = "std")]
#[test]
fn callbacks_do_not_fire_on_seek_or_reset() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let entry_count = Arc::new(AtomicUsize::new(0));
let complete_count = Arc::new(AtomicUsize::new(0));
let entry_seen = Arc::clone(&entry_count);
let complete_seen = Arc::clone(&complete_count);
let mut timeline = Timeline::new()
.add("a", tween(100.0, 1.0), At::Start)
.on_entry_complete("a", move || {
entry_seen.fetch_add(1, Ordering::SeqCst);
})
.on_complete(move || {
complete_seen.fetch_add(1, Ordering::SeqCst);
});
timeline.seek(1.0);
timeline.reset();
assert_eq!(entry_count.load(Ordering::SeqCst), 0);
assert_eq!(complete_count.load(Ordering::SeqCst), 0);
}
#[cfg(feature = "tokio")]
#[test]
fn wait_is_ready_after_completion() {
use core::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};
struct NoopWaker;
impl Wake for NoopWaker {
fn wake(self: Arc<Self>) {}
}
let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
timeline.play();
timeline.update(1.0);
let mut wait = Box::pin(timeline.wait());
let waker = Waker::from(Arc::new(NoopWaker));
let mut cx = Context::from_waker(&waker);
assert!(matches!(wait.as_mut().poll(&mut cx), Poll::Ready(())));
}
}