use crate::animation::{Animatable, Curve};
use crate::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Repeat {
#[default]
Once,
Times(u32),
Infinite,
PingPong,
}
#[derive(Debug, Clone)]
pub struct Keyframe<T> {
pub offset: f64,
pub value: T,
pub curve: Option<Curve>,
}
#[derive(Debug, Clone)]
pub struct Keyframes<T = Style> {
pub frames: Vec<Keyframe<T>>,
pub duration_ms: f64,
pub repeat: Repeat,
pub default_curve: Curve,
}
impl<T: Animatable> Default for Keyframes<T> {
fn default() -> Self {
Self {
frames: Vec::new(),
duration_ms: 1000.0,
repeat: Repeat::Once,
default_curve: Curve::linear(),
}
}
}
impl<T: Animatable> Keyframes<T> {
pub fn new() -> Self {
Self::default()
}
pub fn keyframe(mut self, offset: f64, value: T) -> Self {
self.frames.push(Keyframe {
offset: offset.clamp(0.0, 1.0),
value,
curve: None,
});
self.frames
.sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap());
self
}
pub fn keyframe_with_curve(mut self, offset: f64, value: T, curve: Curve) -> Self {
self.frames.push(Keyframe {
offset: offset.clamp(0.0, 1.0),
value,
curve: Some(curve),
});
self.frames
.sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap());
self
}
pub fn duration_ms(mut self, ms: f64) -> Self {
self.duration_ms = ms.max(1.0);
self
}
pub fn repeat(mut self, repeat: Repeat) -> Self {
self.repeat = repeat;
self
}
pub fn curve(mut self, curve: Curve) -> Self {
self.default_curve = curve;
self
}
pub fn evaluate(&self, elapsed_ms: f64) -> (T, bool) {
if self.frames.is_empty() {
panic!("Cannot evaluate Keyframes with 0 frames");
}
if self.frames.len() == 1 {
return (self.frames[0].value.clone(), false);
}
let duration = self.duration_ms.max(1.0);
let raw_cycles = elapsed_ms / duration;
let (t, is_active) = match self.repeat {
Repeat::Once => {
if raw_cycles >= 1.0 {
(1.0, false)
} else {
(raw_cycles.clamp(0.0, 1.0), true)
}
}
Repeat::Times(n) => {
if raw_cycles >= n as f64 {
(1.0, false)
} else {
(raw_cycles.fract(), true)
}
}
Repeat::Infinite => (raw_cycles.fract(), true),
Repeat::PingPong => {
let cycle_idx = raw_cycles.floor() as u64;
let fract = raw_cycles.fract();
let pingpong_t = if cycle_idx % 2 == 0 {
fract
} else {
1.0 - fract
};
(pingpong_t, true)
}
};
let mut prev_idx = 0;
let mut next_idx = self.frames.len() - 1;
for (i, frame) in self.frames.iter().enumerate() {
if frame.offset <= t {
prev_idx = i;
}
if frame.offset >= t {
next_idx = i;
break;
}
}
if prev_idx == next_idx {
return (self.frames[prev_idx].value.clone(), is_active);
}
let start_frame = &self.frames[prev_idx];
let end_frame = &self.frames[next_idx];
let span = (end_frame.offset - start_frame.offset).max(1e-6);
let local_t = ((t - start_frame.offset) / span).clamp(0.0, 1.0);
let curve = end_frame.curve.as_ref().unwrap_or(&self.default_curve);
let progress = curve.eval(local_t);
let interpolated = T::interpolate(&start_frame.value, &end_frame.value, progress);
(interpolated, is_active)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keyframes_evaluation() {
let kf = Keyframes::<f32>::new()
.keyframe(0.0, 0.0)
.keyframe(0.5, 100.0)
.keyframe(1.0, 0.0)
.duration_ms(1000.0)
.curve(Curve::linear());
let (val_0, active_0) = kf.evaluate(0.0);
assert_eq!(val_0, 0.0);
assert!(active_0);
let (val_250, _) = kf.evaluate(250.0);
assert!((val_250 - 50.0).abs() < 1e-3);
let (val_500, _) = kf.evaluate(500.0);
assert!((val_500 - 100.0).abs() < 1e-3);
let (val_750, _) = kf.evaluate(750.0);
assert!((val_750 - 50.0).abs() < 1e-3);
let (val_1000, active_1000) = kf.evaluate(1000.0);
assert_eq!(val_1000, 0.0);
assert!(!active_1000);
}
#[test]
fn test_keyframes_ping_pong() {
let kf = Keyframes::<f32>::new()
.keyframe(0.0, 10.0)
.keyframe(1.0, 20.0)
.duration_ms(1000.0)
.repeat(Repeat::PingPong)
.curve(Curve::linear());
let (val_forward, _) = kf.evaluate(500.0);
assert!((val_forward - 15.0).abs() < 1e-3);
let (val_backward, _) = kf.evaluate(1500.0);
assert!((val_backward - 15.0).abs() < 1e-3);
}
}