pub type Phase = f32;
pub fn linear(t: Phase) -> Phase {
t.clamp(0.0, 1.0)
}
pub fn ease_in(t: Phase) -> Phase {
let t = t.clamp(0.0, 1.0);
t * t
}
pub fn ease_out(t: Phase) -> Phase {
let t = t.clamp(0.0, 1.0);
1.0 - (1.0 - t) * (1.0 - t)
}
pub fn ease_in_out(t: Phase) -> Phase {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
let f = -2.0 * t + 2.0;
1.0 - f * f * f / 2.0
}
}
pub fn ping_pong(frame: u64, period: u64) -> Phase {
if period == 0 {
return 0.0;
}
let pos = frame % period;
let half = period as f32 / 2.0;
let up = pos as f32 / half;
if up <= 1.0 { up } else { 2.0 - up }
}
pub fn sawtooth(frame: u64, period: u64) -> Phase {
if period == 0 {
return 0.0;
}
(frame % period) as f32 / period as f32
}
pub type Easing = fn(Phase) -> Phase;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Repeat {
#[default]
Once,
Loop,
PingPong,
}
#[derive(Clone, Copy, Debug)]
struct Keyframe {
at: u64,
value: f32,
easing: Easing,
}
#[derive(Clone, Debug, Default)]
pub struct Timeline {
keyframes: Vec<Keyframe>,
repeat: Repeat,
}
impl Timeline {
pub fn new() -> Self {
Self::default()
}
pub fn keyframe(self, at: u64, value: f32) -> Self {
self.ease(at, value, linear)
}
pub fn ease(mut self, at: u64, value: f32, easing: Easing) -> Self {
let kf = Keyframe { at, value, easing };
match self.keyframes.binary_search_by_key(&at, |k| k.at) {
Ok(i) => self.keyframes[i] = kf,
Err(i) => self.keyframes.insert(i, kf),
}
self
}
pub fn repeat(mut self, repeat: Repeat) -> Self {
self.repeat = repeat;
self
}
pub fn duration(&self) -> u64 {
self.keyframes.last().map(|k| k.at).unwrap_or(0)
}
pub fn is_complete(&self, frame: u64) -> bool {
matches!(self.repeat, Repeat::Once) && frame >= self.duration()
}
fn local_time(&self, frame: u64) -> u64 {
let duration = self.duration();
if duration == 0 {
return 0;
}
match self.repeat {
Repeat::Once => frame.min(duration),
Repeat::Loop => frame % duration,
Repeat::PingPong => {
let pos = frame % duration;
if (frame / duration).is_multiple_of(2) {
pos
} else {
duration - pos
}
}
}
}
pub fn sample(&self, frame: u64) -> f32 {
match self.keyframes.as_slice() {
[] => 0.0,
[only] => only.value,
keyframes => {
let t = self.local_time(frame);
let hi = keyframes
.iter()
.position(|k| k.at >= t)
.unwrap_or(keyframes.len() - 1)
.max(1);
let (lo, hi) = (&keyframes[hi - 1], &keyframes[hi]);
let span = hi.at - lo.at;
if span == 0 {
return hi.value;
}
let p = (t - lo.at) as f32 / span as f32;
let eased = (hi.easing)(p);
lo.value + (hi.value - lo.value) * eased
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_endpoints_and_midpoints() {
for f in [linear, ease_in, ease_out, ease_in_out] {
assert!((f(0.0) - 0.0).abs() < 1e-6);
assert!((f(1.0) - 1.0).abs() < 1e-6);
}
assert!((ease_in_out(0.5) - 0.5).abs() < 1e-6);
assert_eq!(linear(2.0), 1.0);
assert_eq!(ease_out(-1.0), 0.0);
}
#[test]
fn ping_pong_and_sawtooth_shapes() {
assert!((ping_pong(0, 60) - 0.0).abs() < 1e-6);
assert!((ping_pong(30, 60) - 1.0).abs() < 1e-6); assert!((ping_pong(60, 60) - 0.0).abs() < 1e-6); assert!((sawtooth(0, 10) - 0.0).abs() < 1e-6);
assert!((sawtooth(5, 10) - 0.5).abs() < 1e-6);
assert!((sawtooth(10, 10) - 0.0).abs() < 1e-6); }
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "{a} != {b}");
}
#[test]
fn timeline_empty_and_single_stop() {
approx(Timeline::new().sample(0), 0.0);
approx(Timeline::new().sample(100), 0.0);
let one = Timeline::new().keyframe(5, 42.0);
approx(one.sample(0), 42.0);
approx(one.sample(999), 42.0);
assert_eq!(one.duration(), 5);
}
#[test]
fn timeline_linear_interpolates_and_holds() {
let t = Timeline::new().keyframe(0, 0.0).keyframe(10, 100.0);
approx(t.sample(0), 0.0);
approx(t.sample(5), 50.0);
approx(t.sample(10), 100.0);
approx(t.sample(50), 100.0);
assert!(t.is_complete(10));
assert!(!t.is_complete(9));
}
#[test]
fn timeline_insertion_order_is_normalized() {
let a = Timeline::new()
.keyframe(20, 2.0)
.keyframe(0, 0.0)
.keyframe(10, 1.0);
approx(a.sample(5), 0.5);
approx(a.sample(15), 1.5);
let b = Timeline::new().keyframe(10, 1.0).keyframe(10, 9.0);
approx(b.sample(10), 9.0);
assert_eq!(b.duration(), 10);
}
#[test]
fn timeline_easing_shapes_the_segment() {
let eased = Timeline::new().keyframe(0, 0.0).ease(10, 1.0, ease_in);
assert!(eased.sample(5) < 0.5);
approx(eased.sample(0), 0.0);
approx(eased.sample(10), 1.0);
}
#[test]
fn timeline_loop_and_ping_pong() {
let looping = Timeline::new()
.keyframe(0, 0.0)
.keyframe(10, 1.0)
.keyframe(20, 0.0)
.repeat(Repeat::Loop);
approx(looping.sample(10), 1.0);
approx(looping.sample(30), 1.0); assert!(!looping.is_complete(1_000_000));
let pp = Timeline::new()
.keyframe(0, 0.0)
.keyframe(10, 1.0)
.repeat(Repeat::PingPong);
approx(pp.sample(0), 0.0);
approx(pp.sample(10), 1.0); approx(pp.sample(15), 0.5); approx(pp.sample(20), 0.0); }
}