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
}
}
pub struct Timeline {
entries: Vec<TimelineEntry>,
elapsed: f32,
state: TimelineState,
pub looping: Loop,
}
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)
.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,
}
}
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 play(&mut self) {
if self.state == TimelineState::Completed {
self.reset();
}
if self.duration() == 0.0 {
self.state = TimelineState::Completed;
} else {
self.state = TimelineState::Playing;
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;
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;
} else if self.state == TimelineState::Completed {
self.state = TimelineState::Playing;
}
}
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 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 tick_forward(&mut self, prev: f32, next: f32) {
for entry in self.entries.iter_mut() {
let start = entry.start_at;
let end = entry.end_at();
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;
}
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;
}
}
}
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 {
self.state = TimelineState::Completed;
return false;
}
let dt = dt.max(0.0);
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);
self.tick_forward(prev_local, next_local);
self.elapsed = next_elapsed.min(base);
if next_elapsed >= base {
self.state = TimelineState::Completed;
return false;
}
}
Loop::Times(n) => {
let total = base * n.max(1) as f32;
self.elapsed = next_elapsed.min(total);
self.sync_to_elapsed();
if next_elapsed >= total {
self.state = TimelineState::Completed;
return false;
}
}
Loop::Forever | Loop::PingPong => {
self.elapsed = next_elapsed;
self.sync_to_elapsed();
}
}
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());
}
}