eazy_keyframe/
track.rs

1//! Keyframe track for sampling interpolated values over time.
2//!
3//! A [`KeyframeTrack`] holds a sequence of keyframes and provides
4//! sampling functionality to get interpolated values at any time.
5
6use crate::keyframe::Keyframe;
7
8use eazy_core::Curve;
9use eazy_core::Easing;
10use eazy_tweener::Tweenable;
11
12/// A track containing keyframes that can be sampled at any time.
13///
14/// # Type Parameters
15///
16/// - `T`: The value type, must implement [`Tweenable`].
17///
18/// # Examples
19///
20/// ```rust
21/// use eazy_keyframes::{KeyframeTrack, Keyframe};
22/// use eazy_core::Easing;
23///
24/// let track = KeyframeTrack::new()
25///   .keyframe(0.0, 0.0_f32)
26///   .keyframe_eased(0.5, 100.0, Easing::OutBounce)
27///   .keyframe(1.0, 50.0);
28///
29/// let value = track.sample(0.25);
30/// ```
31#[derive(Debug, Clone)]
32pub struct KeyframeTrack<T: Tweenable> {
33  /// Sorted list of keyframes.
34  keyframes: Vec<Keyframe<T>>,
35  /// Default easing for keyframes without explicit easing.
36  default_easing: Easing,
37}
38
39impl<T: Tweenable> KeyframeTrack<T> {
40  /// Create a new empty keyframe track.
41  pub fn new() -> Self {
42    Self {
43      keyframes: Vec::new(),
44      default_easing: Easing::Linear,
45    }
46  }
47
48  /// Create a track with a specific default easing.
49  pub fn with_default_easing(easing: Easing) -> Self {
50    Self {
51      keyframes: Vec::new(),
52      default_easing: easing,
53    }
54  }
55
56  /// Add a keyframe to the track.
57  pub fn add(&mut self, keyframe: Keyframe<T>) {
58    self.keyframes.push(keyframe);
59    self.sort();
60  }
61
62  /// Add a keyframe (builder pattern).
63  pub fn keyframe(mut self, time: f32, value: T) -> Self {
64    self.keyframes.push(Keyframe::new(time, value));
65    self.sort();
66    self
67  }
68
69  /// Add a keyframe with easing (builder pattern).
70  pub fn keyframe_eased(mut self, time: f32, value: T, easing: Easing) -> Self {
71    self
72      .keyframes
73      .push(Keyframe::new(time, value).with_easing(easing));
74    self.sort();
75    self
76  }
77
78  /// Sort keyframes by time.
79  fn sort(&mut self) {
80    self
81      .keyframes
82      .sort_by(|a, b| a.time().partial_cmp(&b.time()).unwrap());
83  }
84
85  /// Get the number of keyframes.
86  pub fn len(&self) -> usize {
87    self.keyframes.len()
88  }
89
90  /// Check if the track is empty.
91  pub fn is_empty(&self) -> bool {
92    self.keyframes.is_empty()
93  }
94
95  /// Get the duration of the track (time of last keyframe).
96  pub fn duration(&self) -> f32 {
97    self.keyframes.last().map(|kf| kf.time()).unwrap_or(0.0)
98  }
99
100  /// Get the start time (time of first keyframe).
101  pub fn start_time(&self) -> f32 {
102    self.keyframes.first().map(|kf| kf.time()).unwrap_or(0.0)
103  }
104
105  /// Get a keyframe by index.
106  pub fn get(&self, index: usize) -> Option<&Keyframe<T>> {
107    self.keyframes.get(index)
108  }
109
110  /// Sample the track at a given time.
111  ///
112  /// Returns the interpolated value between keyframes.
113  /// Times before the first keyframe return the first value.
114  /// Times after the last keyframe return the last value.
115  pub fn sample(&self, time: f32) -> T {
116    match self.keyframes.len() {
117      0 => panic!("Cannot sample empty keyframe track"),
118      1 => self.keyframes[0].value(),
119      _ => self.sample_between(time),
120    }
121  }
122
123  /// Sample with clamping to track bounds.
124  pub fn sample_clamped(&self, time: f32) -> T {
125    let clamped = time.clamp(self.start_time(), self.duration());
126
127    self.sample(clamped)
128  }
129
130  /// Find the keyframe pair surrounding the given time.
131  fn find_keyframe_pair(&self, time: f32) -> (usize, usize) {
132    // Find the first keyframe with time > target
133    let next_idx = self
134      .keyframes
135      .iter()
136      .position(|kf| kf.time() > time)
137      .unwrap_or(self.keyframes.len());
138
139    if next_idx == 0 {
140      (0, 0)
141    } else if next_idx >= self.keyframes.len() {
142      let last = self.keyframes.len() - 1;
143
144      (last, last)
145    } else {
146      (next_idx - 1, next_idx)
147    }
148  }
149
150  /// Sample between keyframes.
151  fn sample_between(&self, time: f32) -> T {
152    let (prev_idx, next_idx) = self.find_keyframe_pair(time);
153
154    // Same keyframe (at bounds)
155    if prev_idx == next_idx {
156      return self.keyframes[prev_idx].value();
157    }
158
159    let prev = &self.keyframes[prev_idx];
160    let next = &self.keyframes[next_idx];
161
162    // Calculate local progress between keyframes
163    let segment_duration = next.time() - prev.time();
164
165    if segment_duration == 0.0 {
166      return next.value();
167    }
168
169    let local_progress = (time - prev.time()) / segment_duration;
170
171    // Apply easing (use next keyframe's easing, as it defines how we arrive)
172    let easing = next.easing().unwrap_or(&self.default_easing);
173    let eased_progress = easing.y(local_progress);
174
175    // Interpolate
176    prev.value().lerp(next.value(), eased_progress)
177  }
178
179  /// Iterate over keyframes.
180  pub fn iter(&self) -> impl Iterator<Item = &Keyframe<T>> {
181    self.keyframes.iter()
182  }
183}
184
185impl<T: Tweenable> Default for KeyframeTrack<T> {
186  fn default() -> Self {
187    Self::new()
188  }
189}
190
191impl<T: Tweenable> FromIterator<Keyframe<T>> for KeyframeTrack<T> {
192  fn from_iter<I: IntoIterator<Item = Keyframe<T>>>(iter: I) -> Self {
193    let mut track = Self::new();
194
195    for kf in iter {
196      track.keyframes.push(kf);
197    }
198
199    track.sort();
200    track
201  }
202}
203
204impl<T: Tweenable> From<Vec<Keyframe<T>>> for KeyframeTrack<T> {
205  fn from(keyframes: Vec<Keyframe<T>>) -> Self {
206    let mut track = Self {
207      keyframes,
208      default_easing: Easing::Linear,
209    };
210
211    track.sort();
212    track
213  }
214}
215
216#[cfg(test)]
217mod tests {
218  use super::*;
219
220  #[test]
221  fn test_sample_linear() {
222    let track = KeyframeTrack::new()
223      .keyframe(0.0, 0.0_f32)
224      .keyframe(1.0, 100.0);
225
226    assert_eq!(track.sample(0.0), 0.0);
227    assert!((track.sample(0.5) - 50.0).abs() < 0.001);
228    assert_eq!(track.sample(1.0), 100.0);
229  }
230
231  #[test]
232  fn test_sample_multiple_keyframes() {
233    let track = KeyframeTrack::new()
234      .keyframe(0.0, 0.0_f32)
235      .keyframe(0.5, 100.0)
236      .keyframe(1.0, 50.0);
237
238    assert_eq!(track.sample(0.0), 0.0);
239    assert!((track.sample(0.25) - 50.0).abs() < 0.001);
240    assert_eq!(track.sample(0.5), 100.0);
241    assert!((track.sample(0.75) - 75.0).abs() < 0.001);
242    assert_eq!(track.sample(1.0), 50.0);
243  }
244
245  #[test]
246  fn test_sample_before_first() {
247    let track = KeyframeTrack::new()
248      .keyframe(0.5, 100.0_f32)
249      .keyframe(1.0, 200.0);
250
251    // Before first keyframe should return first value
252    assert_eq!(track.sample(0.0), 100.0);
253    assert_eq!(track.sample(0.25), 100.0);
254  }
255
256  #[test]
257  fn test_sample_after_last() {
258    let track = KeyframeTrack::new()
259      .keyframe(0.0, 0.0_f32)
260      .keyframe(0.5, 100.0);
261
262    // After last keyframe should return last value
263    assert_eq!(track.sample(0.75), 100.0);
264    assert_eq!(track.sample(1.0), 100.0);
265  }
266
267  #[test]
268  fn test_sample_single_keyframe() {
269    let track = KeyframeTrack::new().keyframe(0.5, 100.0_f32);
270
271    assert_eq!(track.sample(0.0), 100.0);
272    assert_eq!(track.sample(0.5), 100.0);
273    assert_eq!(track.sample(1.0), 100.0);
274  }
275
276  #[test]
277  fn test_sample_array() {
278    let track = KeyframeTrack::new()
279      .keyframe(0.0, [0.0_f32, 0.0, 0.0])
280      .keyframe(1.0, [100.0, 200.0, 300.0]);
281
282    let mid = track.sample(0.5);
283
284    assert!((mid[0] - 50.0).abs() < 0.001);
285    assert!((mid[1] - 100.0).abs() < 0.001);
286    assert!((mid[2] - 150.0).abs() < 0.001);
287  }
288
289  #[test]
290  fn test_duration() {
291    let track = KeyframeTrack::new()
292      .keyframe(0.0, 0.0_f32)
293      .keyframe(2.5, 100.0);
294
295    assert_eq!(track.duration(), 2.5);
296  }
297
298  #[test]
299  fn test_keyframe_ordering() {
300    // Add keyframes out of order
301    let track = KeyframeTrack::new()
302      .keyframe(1.0, 100.0_f32)
303      .keyframe(0.0, 0.0)
304      .keyframe(0.5, 50.0);
305
306    // Should be sorted
307    assert_eq!(track.get(0).unwrap().time(), 0.0);
308    assert_eq!(track.get(1).unwrap().time(), 0.5);
309    assert_eq!(track.get(2).unwrap().time(), 1.0);
310  }
311
312  #[test]
313  fn test_from_iterator() {
314    let keyframes =
315      vec![Keyframe::new(1.0, 100.0_f32), Keyframe::new(0.0, 0.0)];
316
317    let track: KeyframeTrack<f32> = keyframes.into_iter().collect();
318
319    assert_eq!(track.len(), 2);
320    assert_eq!(track.get(0).unwrap().time(), 0.0);
321  }
322
323  #[test]
324  fn test_from_vec() {
325    let keyframes =
326      vec![Keyframe::new(1.0, 100.0_f32), Keyframe::new(0.0, 0.0)];
327
328    let track = KeyframeTrack::from(keyframes);
329
330    assert_eq!(track.len(), 2);
331    assert_eq!(track.get(0).unwrap().time(), 0.0);
332  }
333
334  #[test]
335  fn test_keyframes_macro() {
336    use crate::keyframes;
337
338    let track = keyframes![
339      (0.0, 0.0_f32),
340      (0.5, 100.0, Easing::InQuadratic),
341      (1.0, 50.0)
342    ];
343
344    assert_eq!(track.len(), 3);
345    assert_eq!(track.sample(0.0), 0.0);
346    assert_eq!(track.sample(1.0), 50.0);
347
348    // At 0.25 (midpoint of first segment, InQuadratic on second keyframe).
349    // InQuadratic(0.5) = 0.25, so value = 0.0 + (100.0 - 0.0) * 0.25 = 25.0.
350    let val = track.sample(0.25);
351
352    assert_eq!(val, 25.0);
353  }
354
355  #[test]
356  fn test_keyframes_macro_array() {
357    use crate::keyframes;
358
359    let track = keyframes![(0.0, [0.0_f32, 0.0]), (1.0, [100.0, 200.0])];
360
361    let mid = track.sample(0.5);
362
363    assert_eq!(mid, [50.0, 100.0]);
364  }
365}