use crate::duration::TimeStamp;
#[derive(Clone, Debug)]
pub struct Animation {
pub property: AnimatedProperty,
pub keyframes: Vec<Keyframe>,
pub easing: Easing,
pub repeat: Repeat,
}
impl Animation {
pub fn new(
property: AnimatedProperty,
mut keyframes: Vec<Keyframe>,
easing: Easing,
repeat: Repeat,
) -> Self {
keyframes.sort_by_key(|k| k.time);
Animation {
property,
keyframes,
easing,
repeat,
}
}
pub fn sample(&self, t: TimeStamp) -> Option<KeyframeValue> {
if self.keyframes.is_empty() {
return None;
}
let t = match self.repeat {
Repeat::Once => t,
Repeat::Loop => {
let span = self.span()?;
if span <= 0 {
t
} else {
self.keyframes[0].time + ((t - self.keyframes[0].time).rem_euclid(span))
}
}
Repeat::PingPong => {
let span = self.span()?;
if span <= 0 {
t
} else {
let offset = (t - self.keyframes[0].time).rem_euclid(span * 2);
if offset < span {
self.keyframes[0].time + offset
} else {
self.keyframes[0].time + span * 2 - offset
}
}
}
};
if t <= self.keyframes[0].time {
return Some(self.keyframes[0].value.clone());
}
if t >= self.keyframes[self.keyframes.len() - 1].time {
return Some(self.keyframes[self.keyframes.len() - 1].value.clone());
}
let idx = self
.keyframes
.binary_search_by_key(&t, |k| k.time)
.unwrap_or_else(|i| i.saturating_sub(1));
let (a, b) = (&self.keyframes[idx], &self.keyframes[idx + 1]);
let span = (b.time - a.time) as f32;
let raw = if span <= 0.0 {
0.0
} else {
(t - a.time) as f32 / span
};
let segment_easing = b.easing.unwrap_or(self.easing);
let f = segment_easing.apply(raw);
Some(KeyframeValue::interpolate(&a.value, &b.value, f))
}
fn span(&self) -> Option<TimeStamp> {
let first = self.keyframes.first()?.time;
let last = self.keyframes.last()?.time;
Some(last - first)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AnimatedProperty {
Position,
Scale,
Rotation,
Opacity,
Skew,
Anchor,
Volume,
EffectParam {
effect_idx: usize,
param: &'static str,
},
Custom(String),
}
#[derive(Clone, Debug)]
pub struct Keyframe {
pub time: TimeStamp,
pub value: KeyframeValue,
pub easing: Option<Easing>,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum KeyframeValue {
Scalar(f32),
Vec2(f32, f32),
Color(u32),
Discrete(String),
}
impl KeyframeValue {
pub fn interpolate(a: &KeyframeValue, b: &KeyframeValue, t: f32) -> KeyframeValue {
match (a, b) {
(KeyframeValue::Scalar(x), KeyframeValue::Scalar(y)) => {
KeyframeValue::Scalar(x + (y - x) * t)
}
(KeyframeValue::Vec2(x1, y1), KeyframeValue::Vec2(x2, y2)) => {
KeyframeValue::Vec2(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t)
}
(KeyframeValue::Color(a), KeyframeValue::Color(b)) => {
KeyframeValue::Color(lerp_color(*a, *b, t))
}
(KeyframeValue::Discrete(a), KeyframeValue::Discrete(b)) => {
KeyframeValue::Discrete(if t < 1.0 { a.clone() } else { b.clone() })
}
_ => a.clone(),
}
}
}
fn lerp_color(a: u32, b: u32, t: f32) -> u32 {
let ac = [
((a >> 24) & 0xff) as f32,
((a >> 16) & 0xff) as f32,
((a >> 8) & 0xff) as f32,
(a & 0xff) as f32,
];
let bc = [
((b >> 24) & 0xff) as f32,
((b >> 16) & 0xff) as f32,
((b >> 8) & 0xff) as f32,
(b & 0xff) as f32,
];
let mut out = 0u32;
for i in 0..4 {
let v = (ac[i] + (bc[i] - ac[i]) * t).clamp(0.0, 255.0) as u32;
out |= v << ((3 - i) * 8);
}
out
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Repeat {
#[default]
Once,
Loop,
PingPong,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Easing {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(f32, f32, f32, f32),
Step(u32),
Hold,
}
impl Easing {
pub fn apply(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseIn => t * t,
Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Easing::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - 2.0 * (1.0 - t) * (1.0 - t)
}
}
Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier_eval(t, *x1, *y1, *x2, *y2),
Easing::Step(n) if *n > 0 => {
let n = *n as f32;
(t * n).floor() / n
}
Easing::Step(_) => 0.0,
Easing::Hold => {
if t >= 1.0 {
1.0
} else {
0.0
}
}
}
}
}
fn cubic_bezier_eval(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
fn b(t: f32, p1: f32, p2: f32) -> f32 {
let u = 1.0 - t;
3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t
}
fn db(t: f32, p1: f32, p2: f32) -> f32 {
let u = 1.0 - t;
3.0 * u * u * p1 + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (1.0 - p2)
}
let mut guess = t;
for _ in 0..4 {
let d = db(guess, x1, x2);
if d.abs() < 1e-6 {
break;
}
let x = b(guess, x1, x2) - t;
guess -= x / d;
guess = guess.clamp(0.0, 1.0);
}
let mut lo = 0.0;
let mut hi = 1.0;
for _ in 0..16 {
let x = b(guess, x1, x2);
if (x - t).abs() < 1e-5 {
break;
}
if x < t {
lo = guess;
} else {
hi = guess;
}
guess = 0.5 * (lo + hi);
}
b(guess, y1, y2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_before_first_keyframe_clamps() {
let anim = Animation::new(
AnimatedProperty::Opacity,
vec![
Keyframe {
time: 10,
value: KeyframeValue::Scalar(0.0),
easing: None,
},
Keyframe {
time: 20,
value: KeyframeValue::Scalar(1.0),
easing: None,
},
],
Easing::Linear,
Repeat::Once,
);
assert_eq!(anim.sample(0), Some(KeyframeValue::Scalar(0.0)));
assert_eq!(anim.sample(10), Some(KeyframeValue::Scalar(0.0)));
}
#[test]
fn sample_linear_midpoint() {
let anim = Animation::new(
AnimatedProperty::Opacity,
vec![
Keyframe {
time: 0,
value: KeyframeValue::Scalar(0.0),
easing: None,
},
Keyframe {
time: 10,
value: KeyframeValue::Scalar(10.0),
easing: None,
},
],
Easing::Linear,
Repeat::Once,
);
match anim.sample(5).unwrap() {
KeyframeValue::Scalar(v) => assert!((v - 5.0).abs() < 1e-3),
_ => panic!("wrong variant"),
}
}
#[test]
fn easing_in_out_crosses_half_at_half() {
assert!((Easing::EaseInOut.apply(0.5) - 0.5).abs() < 1e-5);
}
#[test]
fn cubic_bezier_endpoints() {
let e = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
assert!((e.apply(0.0) - 0.0).abs() < 1e-3);
assert!((e.apply(1.0) - 1.0).abs() < 1e-3);
}
#[test]
fn repeat_loop_wraps() {
let anim = Animation::new(
AnimatedProperty::Opacity,
vec![
Keyframe {
time: 0,
value: KeyframeValue::Scalar(0.0),
easing: None,
},
Keyframe {
time: 10,
value: KeyframeValue::Scalar(10.0),
easing: None,
},
],
Easing::Linear,
Repeat::Loop,
);
match anim.sample(15).unwrap() {
KeyframeValue::Scalar(v) => assert!((v - 5.0).abs() < 1e-3),
_ => panic!("wrong variant"),
}
}
#[test]
fn color_interpolation_halfway() {
let c = KeyframeValue::interpolate(
&KeyframeValue::Color(0xFF0000FF),
&KeyframeValue::Color(0x0000FFFF),
0.5,
);
match c {
KeyframeValue::Color(v) => {
let r = (v >> 24) & 0xff;
let b = (v >> 8) & 0xff;
assert!(r > 100 && r < 155, "r={r}");
assert!(b > 100 && b < 155, "b={b}");
}
_ => panic!("wrong variant"),
}
}
}