eazy_keyframe/
keyframe.rs

1//! Keyframe definition for animation tracks.
2//!
3//! A [`Keyframe`] represents a value at a specific point in time,
4//! with optional easing to control interpolation to this keyframe.
5
6use eazy_core::{Curve, Easing};
7use eazy_tweener::Tweenable;
8
9/// A keyframe representing a value at a specific time.
10///
11/// # Type Parameters
12///
13/// - `T`: The value type, must implement [`Tweenable`].
14///
15/// # Examples
16///
17/// ```rust
18/// use eazy_keyframes::Keyframe;
19/// use eazy_core::Easing;
20///
21/// // Keyframe at 50% with OutBounce easing
22/// let kf = Keyframe::new(0.5, 100.0_f32)
23///   .with_easing(Easing::OutBounce);
24/// ```
25#[derive(Debug, Clone)]
26pub struct Keyframe<T: Tweenable> {
27  /// Time position (normalized 0.0-1.0 or absolute seconds).
28  time: f32,
29  /// Value at this keyframe.
30  value: T,
31  /// Easing function used to interpolate TO this keyframe.
32  /// If None, uses linear interpolation.
33  easing: Option<Easing>,
34}
35
36impl<T: Tweenable> Keyframe<T> {
37  /// Create a new keyframe at the given time with the given value.
38  pub fn new(time: f32, value: T) -> Self {
39    Self {
40      time,
41      value,
42      easing: None,
43    }
44  }
45
46  /// Create a keyframe with easing.
47  pub fn with_easing(mut self, easing: Easing) -> Self {
48    self.easing = Some(easing);
49    self
50  }
51
52  /// Set the easing function.
53  pub fn set_easing(&mut self, easing: Option<Easing>) {
54    self.easing = easing;
55  }
56
57  /// Get the time position.
58  pub fn time(&self) -> f32 {
59    self.time
60  }
61
62  /// Get the value.
63  pub fn value(&self) -> T {
64    self.value
65  }
66
67  /// Get the easing function.
68  pub fn easing(&self) -> Option<&Easing> {
69    self.easing.as_ref()
70  }
71
72  /// Check if this keyframe has custom easing.
73  pub fn has_easing(&self) -> bool {
74    self.easing.is_some()
75  }
76
77  /// Interpolate from this keyframe to the next at the given time.
78  ///
79  /// Uses the easing function from `next` (since easing controls
80  /// interpolation TO a keyframe).
81  ///
82  /// # Behavior
83  ///
84  /// - If `time <= self.time`: returns `self.value`
85  /// - If `time >= next.time`: returns `next.value`
86  /// - If `next.time <= self.time`: returns `next.value` (invalid order)
87  /// - Otherwise: interpolates using `next.easing` (or linear if None)
88  ///
89  /// # Examples
90  ///
91  /// ```rust
92  /// use eazy_keyframes::Keyframe;
93  /// use eazy_core::Easing;
94  ///
95  /// let kf1 = Keyframe::new(0.0, 0.0_f32);
96  /// let kf2 = Keyframe::new(1.0, 100.0_f32).with_easing(Easing::OutBounce);
97  ///
98  /// let value = kf1.tween_to(&kf2, 0.5);
99  /// ```
100  #[inline]
101  pub fn tween_to(&self, next: &Keyframe<T>, time: f32) -> T {
102    // Before this keyframe.
103    if time <= self.time {
104      return self.value;
105    }
106
107    // After next keyframe.
108    if time >= next.time {
109      return next.value;
110    }
111
112    // Invalid ordering (next is before self).
113    if next.time <= self.time {
114      return next.value;
115    }
116
117    // Normalize time to [0, 1] between the two keyframes.
118    let t = (time - self.time) / (next.time - self.time);
119
120    // Apply easing from the target keyframe.
121    let eased_t = match &next.easing {
122      Some(easing) => easing.y(t),
123      None => t, // Linear.
124    };
125
126    self.value.lerp(next.value, eased_t)
127  }
128}
129
130impl<T: Tweenable> PartialEq for Keyframe<T> {
131  fn eq(&self, other: &Self) -> bool {
132    self.time == other.time
133  }
134}
135
136// --- From implementations for tuple syntax ---
137
138/// Create keyframe from `(time, value)` tuple with linear easing.
139impl<T: Tweenable> From<(f32, T)> for Keyframe<T> {
140  #[inline]
141  fn from((time, value): (f32, T)) -> Self {
142    Keyframe::new(time, value)
143  }
144}
145
146/// Create keyframe from `(time, value, easing)` tuple.
147impl<T: Tweenable> From<(f32, T, Easing)> for Keyframe<T> {
148  #[inline]
149  fn from((time, value, easing): (f32, T, Easing)) -> Self {
150    Keyframe::new(time, value).with_easing(easing)
151  }
152}
153
154impl<T: Tweenable> PartialOrd for Keyframe<T> {
155  fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
156    self.time.partial_cmp(&other.time)
157  }
158}
159
160/// Convenience function to create a keyframe.
161pub fn keyframe<T: Tweenable>(time: f32, value: T) -> Keyframe<T> {
162  Keyframe::new(time, value)
163}
164
165/// Convenience function to create a keyframe with easing.
166pub fn keyframe_eased<T: Tweenable>(
167  time: f32,
168  value: T,
169  easing: Easing,
170) -> Keyframe<T> {
171  Keyframe::new(time, value).with_easing(easing)
172}
173
174#[cfg(test)]
175mod tests {
176  use super::*;
177
178  #[test]
179  fn test_keyframe_creation() {
180    let kf = Keyframe::new(0.5, 100.0_f32);
181
182    assert_eq!(kf.time(), 0.5);
183    assert_eq!(kf.value(), 100.0);
184    assert!(kf.easing().is_none());
185  }
186
187  #[test]
188  fn test_keyframe_with_easing() {
189    let kf = Keyframe::new(0.5, 100.0_f32).with_easing(Easing::OutBounce);
190
191    assert!(kf.has_easing());
192  }
193
194  #[test]
195  fn test_keyframe_ordering() {
196    let kf1 = Keyframe::new(0.2, 50.0_f32);
197    let kf2 = Keyframe::new(0.5, 100.0_f32);
198    let kf3 = Keyframe::new(0.8, 150.0_f32);
199
200    assert!(kf1 < kf2);
201    assert!(kf2 < kf3);
202    assert!(kf1 < kf3);
203  }
204
205  #[test]
206  fn test_keyframe_array() {
207    let kf = Keyframe::new(0.5, [1.0_f32, 2.0, 3.0]);
208
209    assert_eq!(kf.value(), [1.0, 2.0, 3.0]);
210  }
211
212  #[test]
213  fn test_tween_to_linear() {
214    let kf1 = Keyframe::new(0.0, 0.0_f32);
215    let kf2 = Keyframe::new(1.0, 100.0_f32);
216
217    // Linear interpolation (no easing on kf2).
218    assert_eq!(kf1.tween_to(&kf2, 0.0), 0.0);
219    assert_eq!(kf1.tween_to(&kf2, 0.5), 50.0);
220    assert_eq!(kf1.tween_to(&kf2, 1.0), 100.0);
221  }
222
223  #[test]
224  fn test_tween_to_clamping() {
225    let kf1 = Keyframe::new(0.2, 10.0_f32);
226    let kf2 = Keyframe::new(0.8, 80.0_f32);
227
228    // Before kf1 -> returns kf1.value.
229    assert_eq!(kf1.tween_to(&kf2, 0.0), 10.0);
230    assert_eq!(kf1.tween_to(&kf2, 0.1), 10.0);
231
232    // After kf2 -> returns kf2.value.
233    assert_eq!(kf1.tween_to(&kf2, 0.9), 80.0);
234    assert_eq!(kf1.tween_to(&kf2, 1.0), 80.0);
235  }
236
237  #[test]
238  fn test_tween_to_with_easing() {
239    let kf1 = Keyframe::new(0.0, 0.0_f32);
240    let kf2 = Keyframe::new(1.0, 100.0_f32).with_easing(Easing::InQuadratic);
241
242    // InQuadratic at t=0.5 gives 0.25, so value = 25.0.
243    let value = kf1.tween_to(&kf2, 0.5);
244
245    assert_eq!(value, 25.0);
246  }
247
248  #[test]
249  fn test_tween_to_array() {
250    let kf1 = Keyframe::new(0.0, [0.0_f32, 0.0, 0.0]);
251    let kf2 = Keyframe::new(1.0, [100.0, 200.0, 300.0]);
252
253    let value = kf1.tween_to(&kf2, 0.5);
254
255    assert_eq!(value, [50.0, 100.0, 150.0]);
256  }
257}