ff_filter/animation/
track.rs1use 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
134impl<T: Lerp> Default for AnimationTrack<T> {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::animation::Easing;
146
147 fn kf(ms: u64, v: f64) -> Keyframe<f64> {
148 Keyframe::new(Duration::from_millis(ms), v, Easing::Linear)
149 }
150
151 #[test]
152 fn animation_track_should_return_first_value_before_first_keyframe() {
153 let track = AnimationTrack::new()
154 .push(kf(500, 10.0))
155 .push(kf(1000, 20.0));
156
157 let v = track.value_at(Duration::from_millis(0));
158 assert!((v - 10.0).abs() < f64::EPSILON, "expected 10.0, got {v}");
159
160 let v2 = track.value_at(Duration::from_millis(499));
161 assert!((v2 - 10.0).abs() < f64::EPSILON, "expected 10.0, got {v2}");
162 }
163
164 #[test]
165 fn animation_track_should_return_last_value_after_last_keyframe() {
166 let track = AnimationTrack::new().push(kf(0, 0.0)).push(kf(1000, 50.0));
167
168 let v = track.value_at(Duration::from_millis(1000));
169 assert!((v - 50.0).abs() < f64::EPSILON, "expected 50.0, got {v}");
170
171 let v2 = track.value_at(Duration::from_millis(9999));
172 assert!((v2 - 50.0).abs() < f64::EPSILON, "expected 50.0, got {v2}");
173 }
174
175 #[test]
176 fn animation_track_should_interpolate_between_keyframes() {
177 let track = AnimationTrack::new().push(kf(0, 0.0)).push(kf(1000, 1.0));
179
180 let v = track.value_at(Duration::from_millis(500));
181 assert!((v - 0.5).abs() < 1e-9, "expected 0.5 at midpoint, got {v}");
182
183 let v2 = track.value_at(Duration::from_millis(250));
184 assert!(
185 (v2 - 0.25).abs() < 1e-9,
186 "expected 0.25 at quarter-point, got {v2}"
187 );
188 }
189
190 #[test]
191 fn fade_shorthand_should_produce_linear_ramp() {
192 let track = AnimationTrack::fade(
194 0.0,
195 1.0,
196 Duration::ZERO,
197 Duration::from_secs(2),
198 Easing::Linear,
199 );
200
201 assert_eq!(track.len(), 2, "fade must produce exactly 2 keyframes");
202
203 let mid = track.value_at(Duration::from_secs(1));
204 assert!(
205 (mid - 0.5).abs() < 1e-9,
206 "expected 0.5 at midpoint (1 s), got {mid}"
207 );
208
209 let quarter = track.value_at(Duration::from_millis(500));
210 assert!(
211 (quarter - 0.25).abs() < 1e-9,
212 "expected 0.25 at quarter-point (500 ms), got {quarter}"
213 );
214 }
215
216 #[test]
217 fn fade_shorthand_should_hold_before_start_and_after_end() {
218 let track = AnimationTrack::fade(
219 10.0,
220 20.0,
221 Duration::from_secs(1),
222 Duration::from_secs(3),
223 Easing::Linear,
224 );
225
226 let before = track.value_at(Duration::ZERO);
228 assert!(
229 (before - 10.0).abs() < f64::EPSILON,
230 "expected 10.0 before start, got {before}"
231 );
232 let at_start = track.value_at(Duration::from_millis(999));
233 assert!(
234 (at_start - 10.0).abs() < f64::EPSILON,
235 "expected 10.0 just before start, got {at_start}"
236 );
237
238 let after = track.value_at(Duration::from_secs(3));
240 assert!(
241 (after - 20.0).abs() < f64::EPSILON,
242 "expected 20.0 at end, got {after}"
243 );
244 let long_after = track.value_at(Duration::from_secs(9999));
245 assert!(
246 (long_after - 20.0).abs() < f64::EPSILON,
247 "expected 20.0 long after end, got {long_after}"
248 );
249 }
250}