1use super::{Animation, Easing};
4use crate::{Duration, Interpolate, Time};
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(mut self, at: Time, value: Value, easing: Easing) -> Self {
62 assert!(
63 at > self.last().at,
64 "keyframe times must be strictly increasing"
65 );
66 self.points.push(Keyframe {
67 at,
68 value,
69 easing: Some(easing.validated()),
70 });
71 self
72 }
73
74 fn last(&self) -> &Keyframe<Value> {
75 self.points
76 .last()
77 .expect("keyframes contain an initial value")
78 }
79}
80
81impl<Value: Clone> Keyframes<Value> {
82 pub fn hold(mut self, duration: Duration) -> Self {
88 assert!(duration >= Time::ZERO, "keyframe hold must be nonnegative");
89 if duration == Time::ZERO {
90 return self;
91 }
92 let last = self.last();
93 let point = Keyframe {
94 at: last.at + duration,
95 value: last.value.clone(),
96 easing: None,
97 };
98 self.points.push(point);
99 self
100 }
101}
102
103impl<Value: Interpolate + Send + Sync> Animation<Value> for Keyframes<Value> {
104 fn sample(&self, time: Time) -> Value {
105 let upper = self.points.partition_point(|point| point.at <= time);
106 if upper == 0 {
107 return self.points[0].value.clone();
108 }
109 let left = &self.points[upper - 1];
110 if upper == self.points.len() || time == left.at {
111 return left.value.clone();
112 }
113 let right = &self.points[upper];
114 let Some(easing) = &right.easing else {
115 return left.value.clone();
116 };
117 let duration = right.at - left.at;
118 let progress = easing.apply((time - left.at) / duration);
119 left.value.interpolate(&right.value, progress)
120 }
121
122 fn duration(&self) -> Option<Duration> {
123 Some(self.last().at)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::{Point, TimeLiteral, Tween};
131
132 #[test]
133 fn absolute_destinations_relative_holds_and_arriving_easing_are_seekable() {
134 let track = Keyframes::new(0.0_f64)
135 .to(1.seconds(), 10.0, Easing::QuadraticIn)
136 .hold(500.milliseconds())
137 .to(2.5.seconds(), 20.0, Easing::Linear);
138 assert_eq!(track.duration(), Some(2.5.seconds()));
139 for (milliseconds, expected) in [
140 (2_000, 15.0),
141 (0, 0.0),
142 (500, 2.5),
143 (1_000, 10.0),
144 (1_250, 10.0),
145 (1_500, 10.0),
146 (2_500, 20.0),
147 (500, 2.5),
148 ] {
149 assert_eq!(track.sample(milliseconds.milliseconds()), expected);
150 }
151 assert_eq!(track.sample(Time::from_ticks(i64::MIN)), 0.0);
152 assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 20.0);
153 }
154
155 #[test]
156 fn progress_and_adjacent_ticks_survive_large_absolute_timestamps() {
157 let track =
158 Keyframes::new(0.0_f64).to(Time::from_ticks(10_000_000_000), 1.0, Easing::Linear);
159 let progress = track.sample(Time::from_ticks(5_000_000_001));
160 assert_eq!(progress, 0.500_000_000_1);
161 assert_ne!(progress, f64::from(progress as f32));
162 let start = Time::from_ticks(1_i64 << 53);
163 let end = start + Time::from_ticks(3);
164 let track = Keyframes::new(0.0_f64)
165 .hold(start)
166 .to(end, 3.0, Easing::Linear)
167 .hold(Time::from_ticks(i64::MAX) - end);
168 assert_eq!(track.duration(), Some(Time::from_ticks(i64::MAX)));
169 assert_eq!(track.sample(start), 0.0);
170 assert_eq!(track.sample(start + Time::from_ticks(1)), 1.0);
171 assert_eq!(track.sample(start + Time::from_ticks(2)), 2.0);
172 assert_eq!(track.sample(end), 3.0);
173 assert_eq!(track.sample(Time::from_ticks(i64::MAX)), 3.0);
174 let last_tick = Keyframes::new(2.0_f64)
175 .hold(Time::from_ticks(i64::MAX - 1))
176 .to(Time::from_ticks(i64::MAX), 7.0, Easing::Linear);
177 assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX - 1)), 2.0);
178 assert_eq!(last_tick.sample(Time::from_ticks(i64::MAX)), 7.0);
179 }
180
181 #[test]
182 fn holds_and_exact_points_never_call_value_interpolation() {
183 #[derive(Clone, Debug, PartialEq)]
184 struct ExactOnly(u32);
185
186 impl Interpolate for ExactOnly {
187 fn interpolate(&self, _target: &Self, _progress: f64) -> Self {
188 panic!("hold or exact timestamp must return the stored value");
189 }
190 }
191 let track = Keyframes::new(ExactOnly(1))
192 .to(Time::from_ticks(10), ExactOnly(2), Easing::QuadraticOut)
193 .hold(Time::from_ticks(5));
194 for (ticks, expected) in [
195 (i64::MIN, 1),
196 (0, 1),
197 (10, 2),
198 (12, 2),
199 (15, 2),
200 (i64::MAX, 2),
201 ] {
202 assert_eq!(track.sample(Time::from_ticks(ticks)), ExactOnly(expected));
203 }
204 let constant = Keyframes::new(ExactOnly(3));
205 assert_eq!(constant.duration(), Some(Time::ZERO));
206 assert_eq!(constant.sample(Time::ZERO), ExactOnly(3));
207 assert_eq!(constant.sample(Time::from_ticks(i64::MAX)), ExactOnly(3));
208 }
209
210 #[test]
211 fn invalid_order_negative_holds_and_overflow_fail_during_building() {
212 for ticks in [i64::MIN, -1, 0] {
213 assert!(
214 std::panic::catch_unwind(|| {
215 Keyframes::new(0.0_f64).to(Time::from_ticks(ticks), 1.0, Easing::Linear)
216 })
217 .is_err()
218 );
219 }
220
221 fn track() -> Keyframes<f64> {
222 Keyframes::new(0.0_f64).hold(Time::from_ticks(10))
223 }
224 for ticks in [9, 10] {
225 assert!(
226 std::panic::catch_unwind(|| {
227 track().to(Time::from_ticks(ticks), 1.0, Easing::Linear)
228 })
229 .is_err()
230 );
231 }
232 assert!(std::panic::catch_unwind(|| track().hold(Time::from_ticks(-1))).is_err());
233 let unchanged = track().hold(Time::ZERO);
234 assert_eq!(unchanged.points.len(), 2);
235 assert_eq!(unchanged.duration(), Some(Time::from_ticks(10)));
236 assert!(
237 std::panic::catch_unwind(|| {
238 Keyframes::new(0.0_f64)
239 .hold(Time::from_ticks(i64::MAX))
240 .hold(Time::from_ticks(1))
241 })
242 .is_err()
243 );
244 }
245
246 #[test]
247 fn tracks_use_existing_combinators_dynamic_dispatch_and_local_frames() -> crate::Result {
248 let track = crate::Keyframes::new(0.0_f64)
249 .to(1.seconds(), 10.0, Easing::Linear)
250 .hold(500.milliseconds())
251 .to(2.seconds(), 20.0, Easing::Linear);
252 let cache = crate::SceneCache::new(0);
253 let local = crate::SceneContext::new(
254 1500.milliseconds(),
255 crate::FrameRate::from(60),
256 crate::Size::new(64.0, 64.0),
257 &cache,
258 )
259 .shifted(1.seconds());
260 assert_eq!(local.sample(&track), 5.0);
261 let boxed = track
262 .clone()
263 .map(|value| Point::new(value, value * 2.0))
264 .boxed();
265 assert_eq!(local.sample(boxed.as_ref()), Point::new(5.0, 10.0));
266 assert_eq!(track.clone().reverse().sample(500.milliseconds()), 10.0);
267 let repeated = track.clone().repeat(2).delay(250.milliseconds());
268 assert_eq!(repeated.duration(), Some(4.25.seconds()));
269 assert_eq!(repeated.sample(2.25.seconds()), 0.0);
270 assert_eq!(repeated.sample(4.25.seconds()), 20.0);
271 let joined = track.then(Tween::new(20.0_f64, 40.0, 1.seconds()));
272 assert_eq!(joined.duration(), Some(3.seconds()));
273 assert_eq!(joined.sample(2.5.seconds()), 30.0);
274 assert_eq!(
275 std::thread::spawn(move || boxed.sample(1.25.seconds()))
276 .join()
277 .unwrap(),
278 Point::new(10.0, 20.0)
279 );
280 Ok(())
281 }
282
283 #[test]
284 fn moving_segments_match_double_precision_tweens_including_overshoot() {
285 for easing in [
286 Easing::Linear,
287 Easing::QuadraticIn,
288 Easing::QuadraticOut,
289 Easing::QuadraticInOut,
290 Easing::CubicBezier {
291 x1: 0.3,
292 y1: 1.8,
293 x2: 0.7,
294 y2: 1.8,
295 },
296 ] {
297 let track = Keyframes::new(10.0_f64).hold(300.milliseconds()).to(
298 2.3.seconds(),
299 30.0,
300 easing.clone(),
301 );
302 let tween = Tween::new(10.0_f64, 30.0, 2.seconds()).easing(easing);
303 for milliseconds in [1_000, 0, 1, 500, 1_999, 2_000] {
304 let time = milliseconds.milliseconds();
305 assert_eq!(track.sample(300.milliseconds() + time), tween.sample(time));
306 }
307 }
308 }
309}