use super::{Easing, FiniteAnimation, Signal};
use crate::{Duration, Interpolate, Time, ValidationError};
#[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(self, at: Duration, value: Value, easing: Easing) -> Self {
self.try_to(at, value, easing)
.unwrap_or_else(|error| panic!("{}", error.message))
}
pub fn try_to(
mut self,
at: Duration,
value: Value,
easing: Easing,
) -> Result<Self, ValidationError> {
let at = at
.checked_offset()
.map(|offset| Time::ZERO + offset)
.filter(|at| *at > self.last().at)
.ok_or_else(|| {
ValidationError::invalid(
"keyframes",
"at",
"keyframe times must be strictly increasing",
)
})?;
self.points.push(Keyframe {
at,
value,
easing: Some(easing),
});
Ok(self)
}
fn last(&self) -> &Keyframe<Value> {
self.points
.last()
.expect("keyframes contain an initial value")
}
}
impl<Value: Clone> Keyframes<Value> {
pub fn hold(self, duration: Duration) -> Self {
self.try_hold(duration)
.unwrap_or_else(|error| panic!("{}", error.message))
}
pub fn try_hold(mut self, duration: Duration) -> Result<Self, ValidationError> {
if duration == Duration::ZERO {
return Ok(self);
}
let last = self.last();
let at = last.at.checked_add(duration).ok_or_else(|| {
ValidationError::invalid(
"keyframes",
"hold",
"keyframe hold overflows the tick range",
)
})?;
let point = Keyframe {
at,
value: last.value.clone(),
easing: None,
};
self.points.push(point);
Ok(self)
}
}
impl<Value: Interpolate + Send + Sync> Signal<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).ticks() as f64 / duration.ticks() as f64);
left.value.interpolate(&right.value, progress)
}
}
impl<Value: Interpolate + Send + Sync> FiniteAnimation<Value> for Keyframes<Value> {
fn duration(&self) -> Duration {
self.last().at.since_origin().magnitude()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Point, TimeLiteral, TimeOffset, 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(), 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().after_origin()),
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(Duration::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 = Duration::from_ticks(1_u64 << 53);
let end = start + Duration::from_ticks(3);
let track = Keyframes::new(0.0_f64)
.hold(start)
.to(end, 3.0, Easing::Linear)
.hold(Duration::from_ticks(i64::MAX as u64) - end);
assert_eq!(track.duration(), Duration::from_ticks(i64::MAX as u64));
assert_eq!(track.sample(start.after_origin()), 0.0);
assert_eq!(
track.sample((start + Duration::from_ticks(1)).after_origin()),
1.0
);
assert_eq!(
track.sample((start + Duration::from_ticks(2)).after_origin()),
2.0
);
assert_eq!(track.sample(end.after_origin()), 3.0);
assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 3.0);
let last_tick = Keyframes::new(2.0_f64)
.hold(Duration::from_ticks(i64::MAX as u64 - 1))
.to(Duration::from_ticks(i64::MAX as u64), 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(Duration::from_ticks(10), ExactOnly(2), Easing::QuadraticOut)
.hold(Duration::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(), Duration::ZERO);
assert_eq!(constant.sample(Time::ZERO), ExactOnly(3));
assert_eq!(constant.sample(Time::from_ticks(i64::MAX)), ExactOnly(3));
}
#[test]
fn invalid_order_and_hold_overflow_fail_during_building() {
assert!(
std::panic::catch_unwind(|| {
Keyframes::new(0.0_f64).to(Duration::ZERO, 1.0, Easing::Linear)
})
.is_err()
);
fn track() -> Keyframes<f64> {
Keyframes::new(0.0_f64).hold(Duration::from_ticks(10))
}
for ticks in [9, 10] {
assert!(
std::panic::catch_unwind(|| {
track().to(Duration::from_ticks(ticks), 1.0, Easing::Linear)
})
.is_err()
);
}
let unchanged = track().hold(Duration::ZERO);
assert_eq!(unchanged.points.len(), 2);
assert_eq!(unchanged.duration(), Duration::from_ticks(10));
assert!(
std::panic::catch_unwind(|| {
Keyframes::new(0.0_f64)
.hold(Duration::from_ticks(i64::MAX as u64))
.hold(Duration::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().after_origin(),
crate::fps!(60),
crate::Size::new(64.0, 64.0),
&cache,
)
.shifted(TimeOffset::from_seconds(1.0));
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().after_origin()),
10.0
);
let repeated = track.clone().repeat(2).delay(250.milliseconds());
assert_eq!(repeated.duration(), 4.25.seconds());
assert_eq!(repeated.sample(2.25.seconds().after_origin()), 0.0);
assert_eq!(repeated.sample(4.25.seconds().after_origin()), 20.0);
let joined = track.then(Tween::new(20.0_f64, 40.0, 1.seconds()));
assert_eq!(joined.duration(), 3.seconds());
assert_eq!(joined.sample(2.5.seconds().after_origin()), 30.0);
assert_eq!(
std::thread::spawn(move || boxed.sample(1.25.seconds().after_origin()))
.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).after_origin()),
tween.sample(time.after_origin())
);
}
}
}
}