use super::{Animation, Easing};
use crate::{Duration, Interpolate, Time};
#[derive(Clone, Debug)]
struct Keyframe<Value> {
at: Time,
value: Value,
easing: Option<Easing>,
}
#[derive(Clone, Debug)]
pub struct Keyframes<Value> {
points: Vec<Keyframe<Value>>,
}
impl<Value> Keyframes<Value> {
pub fn new(value: Value) -> Self {
Self {
points: vec![Keyframe {
at: Time::ZERO,
value,
easing: None,
}],
}
}
pub fn to(mut self, at: Time, value: Value, easing: Easing) -> Self {
assert!(
at > self.last().at,
"keyframe times must be strictly increasing"
);
self.points.push(Keyframe {
at,
value,
easing: Some(easing.validated()),
});
self
}
fn last(&self) -> &Keyframe<Value> {
self.points
.last()
.expect("keyframes contain an initial value")
}
}
impl<Value: Clone> Keyframes<Value> {
pub fn hold(mut self, duration: Duration) -> Self {
assert!(duration >= Time::ZERO, "keyframe hold must be nonnegative");
if duration == Time::ZERO {
return self;
}
let last = self.last();
let point = Keyframe {
at: last.at + duration,
value: last.value.clone(),
easing: None,
};
self.points.push(point);
self
}
}
impl<Value: Interpolate + Send + Sync> Animation<Value> for Keyframes<Value> {
fn sample(&self, time: Time) -> Value {
let upper = self.points.partition_point(|point| point.at <= time);
if upper == 0 {
return self.points[0].value.clone();
}
let left = &self.points[upper - 1];
if upper == self.points.len() || time == left.at {
return left.value.clone();
}
let right = &self.points[upper];
let Some(easing) = &right.easing else {
return left.value.clone();
};
let duration = right.at - left.at;
let progress = easing.apply((time - left.at) / duration);
left.value.interpolate(&right.value, progress)
}
fn duration(&self) -> Option<Duration> {
Some(self.last().at)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Point, TimeLiteral, Tween};
#[test]
fn absolute_destinations_relative_holds_and_arriving_easing_are_seekable() {
let track = Keyframes::new(0.0_f64)
.to(1.seconds(), 10.0, Easing::QuadraticIn)
.hold(500.milliseconds())
.to(2.5.seconds(), 20.0, Easing::Linear);
assert_eq!(track.duration(), Some(2.5.seconds()));
for (milliseconds, expected) in [
(2_000, 15.0),
(0, 0.0),
(500, 2.5),
(1_000, 10.0),
(1_250, 10.0),
(1_500, 10.0),
(2_500, 20.0),
(500, 2.5),
] {
assert_eq!(track.sample(milliseconds.milliseconds()), expected);
}
assert_eq!(track.sample(Time::from_ticks(i64::MIN)), 0.0);
assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 20.0);
}
#[test]
fn progress_and_adjacent_ticks_survive_large_absolute_timestamps() {
let track =
Keyframes::new(0.0_f64).to(Time::from_ticks(10_000_000_000), 1.0, Easing::Linear);
let progress = track.sample(Time::from_ticks(5_000_000_001));
assert_eq!(progress, 0.500_000_000_1);
assert_ne!(progress, f64::from(progress as f32));
let start = Time::from_ticks(1_i64 << 53);
let end = start + Time::from_ticks(3);
let track = Keyframes::new(0.0_f64)
.hold(start)
.to(end, 3.0, Easing::Linear)
.hold(Time::from_ticks(i64::MAX) - end);
assert_eq!(track.duration(), Some(Time::from_ticks(i64::MAX)));
assert_eq!(track.sample(start), 0.0);
assert_eq!(track.sample(start + Time::from_ticks(1)), 1.0);
assert_eq!(track.sample(start + Time::from_ticks(2)), 2.0);
assert_eq!(track.sample(end), 3.0);
assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 3.0);
let last_tick = Keyframes::new(2.0_f64)
.hold(Time::from_ticks(i64::MAX - 1))
.to(Time::from_ticks(i64::MAX), 7.0, Easing::Linear);
assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX - 1)), 2.0);
assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX)), 7.0);
}
#[test]
fn holds_and_exact_points_never_call_value_interpolation() {
#[derive(Clone, Debug, PartialEq)]
struct ExactOnly(u32);
impl Interpolate for ExactOnly {
fn interpolate(&self, _target: &Self, _progress: f64) -> Self {
panic!("hold or exact timestamp must return the stored value");
}
}
let track = Keyframes::new(ExactOnly(1))
.to(Time::from_ticks(10), ExactOnly(2), Easing::QuadraticOut)
.hold(Time::from_ticks(5));
for (ticks, expected) in [
(i64::MIN, 1),
(0, 1),
(10, 2),
(12, 2),
(15, 2),
(i64::MAX, 2),
] {
assert_eq!(track.sample(Time::from_ticks(ticks)), ExactOnly(expected));
}
let constant = Keyframes::new(ExactOnly(3));
assert_eq!(constant.duration(), Some(Time::ZERO));
assert_eq!(constant.sample(Time::ZERO), ExactOnly(3));
assert_eq!(constant.sample(Time::from_ticks(i64::MAX)), ExactOnly(3));
}
#[test]
fn invalid_order_negative_holds_and_overflow_fail_during_building() {
for ticks in [i64::MIN, -1, 0] {
assert!(
std::panic::catch_unwind(|| {
Keyframes::new(0.0_f64).to(Time::from_ticks(ticks), 1.0, Easing::Linear)
})
.is_err()
);
}
fn track() -> Keyframes<f64> {
Keyframes::new(0.0_f64).hold(Time::from_ticks(10))
}
for ticks in [9, 10] {
assert!(
std::panic::catch_unwind(|| {
track().to(Time::from_ticks(ticks), 1.0, Easing::Linear)
})
.is_err()
);
}
assert!(std::panic::catch_unwind(|| track().hold(Time::from_ticks(-1))).is_err());
let unchanged = track().hold(Time::ZERO);
assert_eq!(unchanged.points.len(), 2);
assert_eq!(unchanged.duration(), Some(Time::from_ticks(10)));
assert!(
std::panic::catch_unwind(|| {
Keyframes::new(0.0_f64)
.hold(Time::from_ticks(i64::MAX))
.hold(Time::from_ticks(1))
})
.is_err()
);
}
#[test]
fn tracks_use_existing_combinators_dynamic_dispatch_and_local_frames() -> crate::Result {
let track = crate::Keyframes::new(0.0_f64)
.to(1.seconds(), 10.0, Easing::Linear)
.hold(500.milliseconds())
.to(2.seconds(), 20.0, Easing::Linear);
let cache = crate::SceneCache::new(0);
let local = crate::SceneContext::new(
1500.milliseconds(),
crate::FrameRate::from(60),
crate::Size::new(64.0, 64.0),
&cache,
)
.shifted(1.seconds());
assert_eq!(local.sample(&track), 5.0);
let boxed = track
.clone()
.map(|value| Point::new(value, value * 2.0))
.boxed();
assert_eq!(local.sample(boxed.as_ref()), Point::new(5.0, 10.0));
assert_eq!(track.clone().reverse().sample(500.milliseconds()), 10.0);
let repeated = track.clone().repeat(2).delay(250.milliseconds());
assert_eq!(repeated.duration(), Some(4.25.seconds()));
assert_eq!(repeated.sample(2.25.seconds()), 0.0);
assert_eq!(repeated.sample(4.25.seconds()), 20.0);
let joined = track.then(Tween::new(20.0_f64, 40.0, 1.seconds()));
assert_eq!(joined.duration(), Some(3.seconds()));
assert_eq!(joined.sample(2.5.seconds()), 30.0);
assert_eq!(
std::thread::spawn(move || boxed.sample(1.25.seconds()))
.join()
.unwrap(),
Point::new(10.0, 20.0)
);
Ok(())
}
#[test]
fn moving_segments_match_double_precision_tweens_including_overshoot() {
for easing in [
Easing::Linear,
Easing::QuadraticIn,
Easing::QuadraticOut,
Easing::QuadraticInOut,
Easing::CubicBezier {
x1: 0.3,
y1: 1.8,
x2: 0.7,
y2: 1.8,
},
] {
let track = Keyframes::new(10.0_f64).hold(300.milliseconds()).to(
2.3.seconds(),
30.0,
easing.clone(),
);
let tween = Tween::new(10.0_f64, 30.0, 2.seconds()).easing(easing);
for milliseconds in [1_000, 0, 1, 500, 1_999, 2_000] {
let time = milliseconds.milliseconds();
assert_eq!(track.sample(300.milliseconds() + time), tween.sample(time));
}
}
}
}