1use super::{Easing, FiniteAnimation, Signal};
4use crate::{Duration, Interpolate, Time, ValidationError};
5
6#[derive(Clone, Debug)]
7struct Keyframe<Value> {
8 at: Time,
9 value: Value,
10 easing: Option<Easing>,
11}
12
13#[derive(Clone, Debug)]
37pub struct Keyframes<Value> {
38 points: Vec<Keyframe<Value>>,
39}
40
41impl<Value> Keyframes<Value> {
42 pub fn new(value: Value) -> Self {
45 Self {
46 points: vec![Keyframe {
47 at: Time::ZERO,
48 value,
49 easing: None,
50 }],
51 }
52 }
53
54 pub fn to(self, at: Duration, value: Value, easing: Easing) -> Self {
62 self.try_to(at, value, easing)
63 .unwrap_or_else(|error| panic!("{}", error.message))
64 }
65
66 pub fn try_to(
68 mut self,
69 at: Duration,
70 value: Value,
71 easing: Easing,
72 ) -> Result<Self, ValidationError> {
73 let at = at
74 .checked_offset()
75 .map(|offset| Time::ZERO + offset)
76 .filter(|at| *at > self.last().at)
77 .ok_or_else(|| {
78 ValidationError::invalid(
79 "keyframes",
80 "at",
81 "keyframe times must be strictly increasing",
82 )
83 })?;
84 self.points.push(Keyframe {
85 at,
86 value,
87 easing: Some(easing),
88 });
89 Ok(self)
90 }
91
92 fn last(&self) -> &Keyframe<Value> {
93 self.points
94 .last()
95 .expect("keyframes contain an initial value")
96 }
97}
98
99impl<Value: Clone> Keyframes<Value> {
100 pub fn hold(self, duration: Duration) -> Self {
106 self.try_hold(duration)
107 .unwrap_or_else(|error| panic!("{}", error.message))
108 }
109
110 pub fn try_hold(mut self, duration: Duration) -> Result<Self, ValidationError> {
112 if duration == Duration::ZERO {
113 return Ok(self);
114 }
115 let last = self.last();
116 let at = last.at.checked_add(duration).ok_or_else(|| {
117 ValidationError::invalid(
118 "keyframes",
119 "hold",
120 "keyframe hold overflows the tick range",
121 )
122 })?;
123 let point = Keyframe {
124 at,
125 value: last.value.clone(),
126 easing: None,
127 };
128 self.points.push(point);
129 Ok(self)
130 }
131}
132
133impl<Value: Interpolate + Send + Sync> Signal<Value> for Keyframes<Value> {
134 fn sample(&self, time: Time) -> Value {
135 let upper = self.points.partition_point(|point| point.at <= time);
136 if upper == 0 {
137 return self.points[0].value.clone();
138 }
139 let left = &self.points[upper - 1];
140 if upper == self.points.len() || time == left.at {
141 return left.value.clone();
142 }
143 let right = &self.points[upper];
144 let Some(easing) = &right.easing else {
145 return left.value.clone();
146 };
147 let duration = right.at - left.at;
148 let progress = easing.apply((time - left.at).ticks() as f64 / duration.ticks() as f64);
149 left.value.interpolate(&right.value, progress)
150 }
151}
152
153impl<Value: Interpolate + Send + Sync> FiniteAnimation<Value> for Keyframes<Value> {
154 fn duration(&self) -> Duration {
155 self.last().at.since_origin().magnitude()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::{Point, TimeLiteral, TimeOffset, Tween};
163
164 #[test]
165 fn absolute_destinations_relative_holds_and_arriving_easing_are_seekable() {
166 let track = Keyframes::new(0.0_f64)
167 .to(1.seconds(), 10.0, Easing::QuadraticIn)
168 .hold(500.milliseconds())
169 .to(2.5.seconds(), 20.0, Easing::Linear);
170 assert_eq!(track.duration(), 2.5.seconds());
171 for (milliseconds, expected) in [
172 (2_000, 15.0),
173 (0, 0.0),
174 (500, 2.5),
175 (1_000, 10.0),
176 (1_250, 10.0),
177 (1_500, 10.0),
178 (2_500, 20.0),
179 (500, 2.5),
180 ] {
181 assert_eq!(
182 track.sample(milliseconds.milliseconds().after_origin()),
183 expected
184 );
185 }
186 assert_eq!(track.sample(Time::from_ticks(i64::MIN)), 0.0);
187 assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 20.0);
188 }
189
190 #[test]
191 fn progress_and_adjacent_ticks_survive_large_absolute_timestamps() {
192 let track =
193 Keyframes::new(0.0_f64).to(Duration::from_ticks(10_000_000_000), 1.0, Easing::Linear);
194 let progress = track.sample(Time::from_ticks(5_000_000_001));
195 assert_eq!(progress, 0.500_000_000_1);
196 assert_ne!(progress, f64::from(progress as f32));
197 let start = Duration::from_ticks(1_u64 << 53);
198 let end = start + Duration::from_ticks(3);
199 let track = Keyframes::new(0.0_f64)
200 .hold(start)
201 .to(end, 3.0, Easing::Linear)
202 .hold(Duration::from_ticks(i64::MAX as u64) - end);
203 assert_eq!(track.duration(), Duration::from_ticks(i64::MAX as u64));
204 assert_eq!(track.sample(start.after_origin()), 0.0);
205 assert_eq!(
206 track.sample((start + Duration::from_ticks(1)).after_origin()),
207 1.0
208 );
209 assert_eq!(
210 track.sample((start + Duration::from_ticks(2)).after_origin()),
211 2.0
212 );
213 assert_eq!(track.sample(end.after_origin()), 3.0);
214 assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 3.0);
215 let last_tick = Keyframes::new(2.0_f64)
216 .hold(Duration::from_ticks(i64::MAX as u64 - 1))
217 .to(Duration::from_ticks(i64::MAX as u64), 7.0, Easing::Linear);
218 assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX - 1)), 2.0);
219 assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX)), 7.0);
220 }
221
222 #[test]
223 fn holds_and_exact_points_never_call_value_interpolation() {
224 #[derive(Clone, Debug, PartialEq)]
225 struct ExactOnly(u32);
226
227 impl Interpolate for ExactOnly {
228 fn interpolate(&self, _target: &Self, _progress: f64) -> Self {
229 panic!("hold or exact timestamp must return the stored value");
230 }
231 }
232 let track = Keyframes::new(ExactOnly(1))
233 .to(Duration::from_ticks(10), ExactOnly(2), Easing::QuadraticOut)
234 .hold(Duration::from_ticks(5));
235 for (ticks, expected) in [
236 (i64::MIN, 1),
237 (0, 1),
238 (10, 2),
239 (12, 2),
240 (15, 2),
241 (i64::MAX, 2),
242 ] {
243 assert_eq!(track.sample(Time::from_ticks(ticks)), ExactOnly(expected));
244 }
245 let constant = Keyframes::new(ExactOnly(3));
246 assert_eq!(constant.duration(), Duration::ZERO);
247 assert_eq!(constant.sample(Time::ZERO), ExactOnly(3));
248 assert_eq!(constant.sample(Time::from_ticks(i64::MAX)), ExactOnly(3));
249 }
250
251 #[test]
252 fn invalid_order_and_hold_overflow_fail_during_building() {
253 assert!(
254 std::panic::catch_unwind(|| {
255 Keyframes::new(0.0_f64).to(Duration::ZERO, 1.0, Easing::Linear)
256 })
257 .is_err()
258 );
259
260 fn track() -> Keyframes<f64> {
261 Keyframes::new(0.0_f64).hold(Duration::from_ticks(10))
262 }
263 for ticks in [9, 10] {
264 assert!(
265 std::panic::catch_unwind(|| {
266 track().to(Duration::from_ticks(ticks), 1.0, Easing::Linear)
267 })
268 .is_err()
269 );
270 }
271 let unchanged = track().hold(Duration::ZERO);
272 assert_eq!(unchanged.points.len(), 2);
273 assert_eq!(unchanged.duration(), Duration::from_ticks(10));
274 assert!(
275 std::panic::catch_unwind(|| {
276 Keyframes::new(0.0_f64)
277 .hold(Duration::from_ticks(i64::MAX as u64))
278 .hold(Duration::from_ticks(1))
279 })
280 .is_err()
281 );
282 }
283
284 #[test]
285 fn tracks_use_existing_combinators_dynamic_dispatch_and_local_frames() -> crate::Result {
286 let track = crate::Keyframes::new(0.0_f64)
287 .to(1.seconds(), 10.0, Easing::Linear)
288 .hold(500.milliseconds())
289 .to(2.seconds(), 20.0, Easing::Linear);
290 let cache = crate::SceneCache::new(0);
291 let local = crate::SceneContext::new(
292 1500.milliseconds().after_origin(),
293 crate::fps!(60),
294 crate::Size::new(64.0, 64.0),
295 &cache,
296 )
297 .shifted(TimeOffset::from_seconds(1.0));
298 assert_eq!(local.sample(&track), 5.0);
299 let boxed = track
300 .clone()
301 .map(|value| Point::new(value, value * 2.0))
302 .boxed();
303 assert_eq!(local.sample(boxed.as_ref()), Point::new(5.0, 10.0));
304 assert_eq!(
305 track
306 .clone()
307 .reverse()
308 .sample(500.milliseconds().after_origin()),
309 10.0
310 );
311 let repeated = track.clone().repeat(2).delay(250.milliseconds());
312 assert_eq!(repeated.duration(), 4.25.seconds());
313 assert_eq!(repeated.sample(2.25.seconds().after_origin()), 0.0);
314 assert_eq!(repeated.sample(4.25.seconds().after_origin()), 20.0);
315 let joined = track.then(Tween::new(20.0_f64, 40.0, 1.seconds()));
316 assert_eq!(joined.duration(), 3.seconds());
317 assert_eq!(joined.sample(2.5.seconds().after_origin()), 30.0);
318 assert_eq!(
319 std::thread::spawn(move || boxed.sample(1.25.seconds().after_origin()))
320 .join()
321 .unwrap(),
322 Point::new(10.0, 20.0)
323 );
324 Ok(())
325 }
326
327 #[test]
328 fn moving_segments_match_double_precision_tweens_including_overshoot() {
329 for easing in [
330 Easing::Linear,
331 Easing::QuadraticIn,
332 Easing::QuadraticOut,
333 Easing::QuadraticInOut,
334 Easing::CubicBezier {
335 x1: 0.3,
336 y1: 1.8,
337 x2: 0.7,
338 y2: 1.8,
339 },
340 ] {
341 let track = Keyframes::new(10.0_f64).hold(300.milliseconds()).to(
342 2.3.seconds(),
343 30.0,
344 easing.clone(),
345 );
346 let tween = Tween::new(10.0_f64, 30.0, 2.seconds()).easing(easing);
347 for milliseconds in [1_000, 0, 1, 500, 1_999, 2_000] {
348 let time = milliseconds.milliseconds();
349 assert_eq!(
350 track.sample((300.milliseconds() + time).after_origin()),
351 tween.sample(time.after_origin())
352 );
353 }
354 }
355 }
356}