1use std::time::Duration;
2
3use super::{Easing, Keyframe, Lerp};
4
5#[derive(Debug, Clone)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(
18 feature = "serde",
19 serde(bound(
20 serialize = "T: serde::Serialize",
21 deserialize = "T: serde::Deserialize<'de>",
22 ))
23)]
24pub struct AnimationTrack<T: Lerp> {
25 keyframes: Vec<Keyframe<T>>,
26}
27
28impl<T: Lerp> AnimationTrack<T> {
29 pub fn new() -> Self {
31 Self {
32 keyframes: Vec::new(),
33 }
34 }
35
36 #[must_use]
40 pub fn push(mut self, kf: Keyframe<T>) -> Self {
41 let pos = self
42 .keyframes
43 .partition_point(|k| k.timestamp < kf.timestamp);
44 if self
45 .keyframes
46 .get(pos)
47 .is_some_and(|k| k.timestamp == kf.timestamp)
48 {
49 self.keyframes[pos] = kf;
50 } else {
51 self.keyframes.insert(pos, kf);
52 }
53 self
54 }
55
56 pub fn value_at(&self, t: Duration) -> T {
66 let len = self.keyframes.len();
67 let pos = self.keyframes.partition_point(|k| k.timestamp <= t);
69
70 if pos == 0 {
71 return self.keyframes[0].value.clone();
73 }
74 if pos >= len {
75 return self.keyframes[len - 1].value.clone();
77 }
78
79 let a = &self.keyframes[pos - 1];
80 let b = &self.keyframes[pos];
81
82 let span = b
83 .timestamp
84 .checked_sub(a.timestamp)
85 .map_or(0.0, |d| d.as_secs_f64());
86 let elapsed = t.checked_sub(a.timestamp).map_or(0.0, |d| d.as_secs_f64());
87 let norm_t = if span > 0.0 { elapsed / span } else { 1.0 };
88
89 let u = a.easing.apply(norm_t);
90 T::lerp(&a.value, &b.value, u)
91 }
92
93 pub fn keyframes(&self) -> &[Keyframe<T>] {
95 &self.keyframes
96 }
97
98 pub fn len(&self) -> usize {
100 self.keyframes.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
105 self.keyframes.is_empty()
106 }
107}
108
109impl AnimationTrack<f64> {
110 pub fn fade(from: f64, to: f64, start: Duration, end: Duration, easing: Easing) -> Self {
128 Self::new()
129 .push(Keyframe::new(start, from, easing))
130 .push(Keyframe::new(end, to, Easing::Linear))
131 }
132
133 pub fn to_ffmpeg_expr(&self, var: &str) -> String {
143 let ks = &self.keyframes;
144 match ks.len() {
145 0 => return "0".to_string(),
146 1 => return format!("{:.6}", ks[0].value),
147 _ => {}
148 }
149 let mut expr = format!("{:.6}", ks[ks.len() - 1].value);
151 for i in (0..ks.len() - 1).rev() {
153 let (a, b) = (&ks[i], &ks[i + 1]);
154 let t1 = b.timestamp.as_secs_f64();
155 let seg = segment_expr(
156 var,
157 a.timestamp.as_secs_f64(),
158 t1,
159 a.value,
160 b.value,
161 &a.easing,
162 );
163 expr = format!("if(lt({var}\\,{t1:.6})\\,{seg}\\,{expr})");
164 }
165 let t0 = ks[0].timestamp.as_secs_f64();
167 format!("if(lt({var}\\,{t0:.6})\\,{:.6}\\,{expr})", ks[0].value)
168 }
169}
170
171fn segment_expr(var: &str, t0: f64, t1: f64, v0: f64, v1: f64, easing: &Easing) -> String {
175 let dt = (t1 - t0).max(1e-9);
176 let u = format!("(({var}-{t0:.6})/{dt:.6})");
178 let eased = match easing {
179 Easing::Hold => "0".to_string(),
181 Easing::Linear | Easing::Bezier { .. } => u,
182 Easing::EaseIn => format!("pow({u}\\,3)"),
183 Easing::EaseOut => format!("(1-pow((1-{u})\\,3))"),
184 Easing::EaseInOut => format!("(3*pow({u}\\,2)-2*pow({u}\\,3))"),
185 };
186 format!("({v0:.6}+({v1:.6}-{v0:.6})*{eased})")
187}
188
189impl<T: Lerp> Default for AnimationTrack<T> {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::animation::Easing;
199
200 fn kf(ms: u64, v: f64) -> Keyframe<f64> {
201 Keyframe::new(Duration::from_millis(ms), v, Easing::Linear)
202 }
203
204 #[test]
205 fn animation_track_should_return_first_value_before_first_keyframe() {
206 let track = AnimationTrack::new()
207 .push(kf(500, 10.0))
208 .push(kf(1000, 20.0));
209
210 let v = track.value_at(Duration::from_millis(0));
211 assert!((v - 10.0).abs() < f64::EPSILON, "expected 10.0, got {v}");
212
213 let v2 = track.value_at(Duration::from_millis(499));
214 assert!((v2 - 10.0).abs() < f64::EPSILON, "expected 10.0, got {v2}");
215 }
216
217 #[test]
218 fn animation_track_should_return_last_value_after_last_keyframe() {
219 let track = AnimationTrack::new().push(kf(0, 0.0)).push(kf(1000, 50.0));
220
221 let v = track.value_at(Duration::from_millis(1000));
222 assert!((v - 50.0).abs() < f64::EPSILON, "expected 50.0, got {v}");
223
224 let v2 = track.value_at(Duration::from_millis(9999));
225 assert!((v2 - 50.0).abs() < f64::EPSILON, "expected 50.0, got {v2}");
226 }
227
228 #[test]
229 fn animation_track_should_interpolate_between_keyframes() {
230 let track = AnimationTrack::new().push(kf(0, 0.0)).push(kf(1000, 1.0));
232
233 let v = track.value_at(Duration::from_millis(500));
234 assert!((v - 0.5).abs() < 1e-9, "expected 0.5 at midpoint, got {v}");
235
236 let v2 = track.value_at(Duration::from_millis(250));
237 assert!(
238 (v2 - 0.25).abs() < 1e-9,
239 "expected 0.25 at quarter-point, got {v2}"
240 );
241 }
242
243 #[test]
244 fn fade_shorthand_should_produce_linear_ramp() {
245 let track = AnimationTrack::fade(
247 0.0,
248 1.0,
249 Duration::ZERO,
250 Duration::from_secs(2),
251 Easing::Linear,
252 );
253
254 assert_eq!(track.len(), 2, "fade must produce exactly 2 keyframes");
255
256 let mid = track.value_at(Duration::from_secs(1));
257 assert!(
258 (mid - 0.5).abs() < 1e-9,
259 "expected 0.5 at midpoint (1 s), got {mid}"
260 );
261
262 let quarter = track.value_at(Duration::from_millis(500));
263 assert!(
264 (quarter - 0.25).abs() < 1e-9,
265 "expected 0.25 at quarter-point (500 ms), got {quarter}"
266 );
267 }
268
269 #[test]
270 fn fade_shorthand_should_hold_before_start_and_after_end() {
271 let track = AnimationTrack::fade(
272 10.0,
273 20.0,
274 Duration::from_secs(1),
275 Duration::from_secs(3),
276 Easing::Linear,
277 );
278
279 let before = track.value_at(Duration::ZERO);
281 assert!(
282 (before - 10.0).abs() < f64::EPSILON,
283 "expected 10.0 before start, got {before}"
284 );
285 let at_start = track.value_at(Duration::from_millis(999));
286 assert!(
287 (at_start - 10.0).abs() < f64::EPSILON,
288 "expected 10.0 just before start, got {at_start}"
289 );
290
291 let after = track.value_at(Duration::from_secs(3));
293 assert!(
294 (after - 20.0).abs() < f64::EPSILON,
295 "expected 20.0 at end, got {after}"
296 );
297 let long_after = track.value_at(Duration::from_secs(9999));
298 assert!(
299 (long_after - 20.0).abs() < f64::EPSILON,
300 "expected 20.0 long after end, got {long_after}"
301 );
302 }
303
304 #[test]
305 fn to_ffmpeg_expr_single_keyframe_is_constant() {
306 let track = AnimationTrack::new().push(Keyframe::new(Duration::ZERO, 42.0, Easing::Linear));
307 assert_eq!(track.to_ffmpeg_expr("t"), "42.000000");
308 }
309
310 #[test]
311 fn to_ffmpeg_expr_two_keyframes_guards_and_interpolates() {
312 let track = AnimationTrack::new()
313 .push(Keyframe::new(Duration::ZERO, 0.0, Easing::Linear))
314 .push(Keyframe::new(Duration::from_secs(2), 100.0, Easing::Linear));
315 let e = track.to_ffmpeg_expr("t");
316 assert!(
318 e.contains("if(lt(t\\,0.000000)"),
319 "before-first guard missing: {e}"
320 );
321 assert!(
322 e.contains("if(lt(t\\,2.000000)"),
323 "segment guard missing: {e}"
324 );
325 assert!(e.contains("100.000000"), "end value missing: {e}");
326 assert!(e.contains("/2.000000"), "span normalisation missing: {e}");
328 }
329
330 #[test]
331 fn to_ffmpeg_expr_ease_in_uses_cubic() {
332 let track = AnimationTrack::new()
333 .push(Keyframe::new(Duration::ZERO, 0.0, Easing::EaseIn))
334 .push(Keyframe::new(Duration::from_secs(1), 1.0, Easing::Linear));
335 assert!(
336 track.to_ffmpeg_expr("t").contains("pow("),
337 "ease-in should use pow()"
338 );
339 }
340}