#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::easing::Easing;
use crate::traits::{Animatable, Update};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Loop {
Once,
Times(u32),
Forever,
PingPong,
}
#[derive(Clone)]
pub struct Keyframe<T: Animatable> {
pub time: f32,
pub value: T,
pub easing: Easing,
}
impl<T: Animatable + core::fmt::Debug> core::fmt::Debug for Keyframe<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Keyframe")
.field("time", &self.time)
.field("value", &self.value)
.field("easing", &self.easing)
.finish()
}
}
pub struct KeyframeTrack<T: Animatable> {
frames: Vec<Keyframe<T>>,
elapsed: f32,
looping: Loop,
completed: bool,
loop_count: u32,
}
impl<T: Animatable + core::fmt::Debug> core::fmt::Debug for KeyframeTrack<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KeyframeTrack")
.field("frames", &self.frames)
.field("elapsed", &self.elapsed)
.field("looping", &self.looping)
.field("completed", &self.completed)
.finish()
}
}
impl<T: Animatable> KeyframeTrack<T> {
pub fn new() -> Self {
Self {
frames: Vec::new(),
elapsed: 0.0,
looping: Loop::Once,
completed: false,
loop_count: 0,
}
}
pub fn push(mut self, time: f32, value: T) -> Self {
self.frames.push(Keyframe {
time,
value,
easing: Easing::Linear,
});
self.frames.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
self
}
pub fn push_with_easing(mut self, time: f32, value: T, easing: Easing) -> Self {
self.frames.push(Keyframe {
time,
value,
easing,
});
self.frames.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
self
}
pub fn looping(mut self, mode: Loop) -> Self {
self.looping = mode;
self
}
pub fn duration(&self) -> f32 {
self.frames.last().map_or(0.0, |f| f.time)
}
pub fn value_at(&self, t: f32) -> T {
if self.frames.is_empty() {
panic!("KeyframeTrack::value_at called on empty track");
}
if self.frames.len() == 1 {
return self.frames[0].value.clone();
}
let t = t.clamp(0.0, self.duration());
let idx = self
.frames
.iter()
.rposition(|f| f.time <= t)
.unwrap_or(0);
if idx >= self.frames.len() - 1 {
return self.frames.last().unwrap().value.clone();
}
let a = &self.frames[idx];
let b = &self.frames[idx + 1];
let segment_duration = b.time - a.time;
if segment_duration <= 0.0 {
return b.value.clone();
}
let local_t = ((t - a.time) / segment_duration).clamp(0.0, 1.0);
let curved_t = a.easing.apply(local_t);
a.value.lerp(&b.value, curved_t)
}
pub fn value(&self) -> T {
let t = self.effective_time();
self.value_at(t)
}
pub fn is_complete(&self) -> bool {
self.completed
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.completed = false;
self.loop_count = 0;
}
fn effective_time(&self) -> f32 {
let dur = self.duration();
if dur <= 0.0 {
return 0.0;
}
match &self.looping {
Loop::Once => self.elapsed.clamp(0.0, dur),
Loop::Times(_) | Loop::Forever => {
self.elapsed % dur
}
Loop::PingPong => {
let cycle = 2.0 * dur;
let cycle_t = self.elapsed % cycle;
if cycle_t <= dur {
cycle_t
} else {
2.0 * dur - cycle_t
}
}
}
}
}
impl<T: Animatable> Update for KeyframeTrack<T> {
fn update(&mut self, dt: f32) -> bool {
if self.completed {
return false;
}
let dt = dt.max(0.0);
self.elapsed += dt;
let dur = self.duration();
if dur <= 0.0 {
self.completed = true;
return false;
}
match &self.looping {
Loop::Once => {
if self.elapsed >= dur {
self.elapsed = dur;
self.completed = true;
}
}
Loop::Times(n) => {
let loops_done = (self.elapsed / dur).floor() as u32;
if loops_done >= *n {
self.elapsed = dur * (*n as f32);
self.completed = true;
}
}
Loop::Forever | Loop::PingPong => {
}
}
!self.completed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_frame_returns_its_value() {
let track = KeyframeTrack::new().push(0.0, 42.0_f32);
assert!((track.value_at(0.0) - 42.0).abs() < 1e-6);
assert!((track.value_at(999.0) - 42.0).abs() < 1e-6);
}
#[test]
fn two_frames_interpolate() {
let track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0);
assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
}
#[test]
fn three_frames_two_segments() {
let track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0)
.push(2.0, 0.0);
assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
assert!((track.value_at(1.5) - 50.0).abs() < 1e-4);
}
#[test]
fn loop_once_completes() {
let mut track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0)
.looping(Loop::Once);
assert!(track.update(0.5));
assert!(!track.is_complete());
assert!(!track.update(0.5));
assert!(track.is_complete());
}
#[test]
fn loop_forever_never_completes() {
let mut track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0)
.looping(Loop::Forever);
for _ in 0..100 {
assert!(track.update(0.5));
}
assert!(!track.is_complete());
}
#[test]
fn ping_pong_reverses() {
let track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0)
.looping(Loop::PingPong);
assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
}
#[test]
fn loop_times_completes_after_n() {
let mut track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0)
.looping(Loop::Times(2));
assert!(track.update(1.0)); assert!(!track.update(1.0)); assert!(track.is_complete());
}
#[test]
fn out_of_bounds_clamps() {
let track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0);
assert!((track.value_at(-5.0) - 0.0).abs() < 1e-6);
assert!((track.value_at(99.0) - 100.0).abs() < 1e-6);
}
#[test]
fn with_easing() {
let track = KeyframeTrack::new()
.push_with_easing(0.0, 0.0_f32, Easing::EaseInQuad)
.push(1.0, 100.0);
assert!((track.value_at(0.5) - 25.0).abs() < 1e-4);
}
#[test]
fn update_advances_value() {
let mut track = KeyframeTrack::new()
.push(0.0, 0.0_f32)
.push(1.0, 100.0);
track.update(0.5);
assert!((track.value() - 50.0).abs() < 1e-4);
}
}