Skip to main content

animato_core/
traits.rs

1//! Core traits for the Animato animation system.
2//!
3//! This module defines the three fundamental traits:
4//! - [`Interpolate`] — a value that can be linearly blended between two states
5//! - [`Animatable`] — blanket marker for any `Interpolate + Clone + 'static` type
6//! - [`Update`] — anything that advances through time when given a `dt`
7
8use crate::math::round;
9use core::any::Any;
10
11/// A value that supports linear interpolation between two instances.
12///
13/// Implement this trait for any type you want to animate with Animato.
14/// The library ships blanket impls for `f32`, `f64`, `[f32; 2]`,
15/// `[f32; 3]`, `[f32; 4]`, `i32`, and `u8`.
16///
17/// # Contract
18///
19/// - `self.lerp(other, 0.0)` must equal `*self`
20/// - `self.lerp(other, 1.0)` must equal `*other`
21/// - `t` outside `[0.0, 1.0]` is allowed — implementations may extrapolate or clamp
22///
23/// # Example
24///
25/// ```rust
26/// use animato_core::Interpolate;
27///
28/// let a = 0.0_f32;
29/// let b = 100.0_f32;
30/// assert_eq!(a.lerp(&b, 0.0), 0.0);
31/// assert_eq!(a.lerp(&b, 1.0), 100.0);
32/// assert_eq!(a.lerp(&b, 0.5), 50.0);
33/// ```
34pub trait Interpolate: Sized {
35    /// Linearly interpolate from `self` to `other` by factor `t`.
36    ///
37    /// `t = 0.0` returns `self`, `t = 1.0` returns `other`.
38    fn lerp(&self, other: &Self, t: f32) -> Self;
39}
40
41/// Marker trait for types that can be used as animation targets.
42///
43/// Any type implementing `Interpolate + Clone + 'static` automatically
44/// satisfies `Animatable` through a blanket impl. You never implement
45/// this trait manually.
46///
47/// # Example
48///
49/// ```rust
50/// use animato_core::Animatable;
51///
52/// fn animate<T: Animatable>(start: T, end: T) {
53///     let mid = start.lerp(&end, 0.5);
54///     let _ = mid;
55/// }
56///
57/// animate(0.0_f32, 1.0_f32);
58/// animate([0.0_f32; 3], [1.0_f32; 3]);
59/// ```
60pub trait Animatable: Interpolate + Clone + 'static {}
61
62/// Blanket impl — every `Interpolate + Clone + 'static` is automatically `Animatable`.
63impl<T: Interpolate + Clone + 'static> Animatable for T {}
64
65/// A value that advances through time.
66///
67/// Implemented by `Tween<T>`, `Spring`, `SpringN<T>`,
68/// `Timeline`, and any user-defined animation type.
69/// The `AnimationDriver` (see `animato-driver`) calls this each frame.
70///
71/// # Contract
72///
73/// - Returns `true` while the animation is still running.
74/// - Returns `false` when the animation has completed (or is settled, for springs).
75/// - Calling `update` after returning `false` is a no-op and returns `false`.
76/// - `dt < 0.0` is treated as `0.0` — time does not run backward.
77///
78/// # Example
79///
80/// ```rust
81/// use animato_core::Update;
82///
83/// struct Counter { count: u32 }
84///
85/// impl Update for Counter {
86///     fn update(&mut self, _dt: f32) -> bool {
87///         self.count += 1;
88///         self.count < 10
89///     }
90/// }
91///
92/// let mut c = Counter { count: 0 };
93/// while c.update(0.016) {}
94/// assert_eq!(c.count, 10);
95/// ```
96pub trait Update {
97    /// Advance the animation by `dt` seconds.
98    ///
99    /// Returns `true` while still running, `false` when complete.
100    fn update(&mut self, dt: f32) -> bool;
101}
102
103/// Object-safe animation interface used by composition containers.
104///
105/// [`Update`] is intentionally tiny: it only advances an animation. `Playable`
106/// adds the small amount of control a timeline needs to reset and seek children
107/// without knowing their concrete type.
108///
109/// Implementations should interpret [`seek_to`](Self::seek_to) as normalized
110/// progress through their finite duration, clamped to `[0.0, 1.0]`.
111///
112/// # Example
113///
114/// ```rust
115/// use animato_core::{Playable, Update};
116///
117/// #[derive(Default)]
118/// struct Clip { elapsed: f32 }
119///
120/// impl Update for Clip {
121///     fn update(&mut self, dt: f32) -> bool {
122///         self.elapsed = (self.elapsed + dt.max(0.0)).min(1.0);
123///         !self.is_complete()
124///     }
125/// }
126///
127/// impl Playable for Clip {
128///     fn duration(&self) -> f32 { 1.0 }
129///     fn reset(&mut self) { self.elapsed = 0.0; }
130///     fn seek_to(&mut self, progress: f32) {
131///         self.elapsed = progress.clamp(0.0, 1.0);
132///     }
133///     fn is_complete(&self) -> bool { self.elapsed >= 1.0 }
134///     fn as_any(&self) -> &dyn core::any::Any { self }
135///     fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self }
136/// }
137/// ```
138pub trait Playable: Update + Any {
139    /// Total finite duration in seconds.
140    fn duration(&self) -> f32;
141
142    /// Reset the animation to its initial state.
143    fn reset(&mut self);
144
145    /// Seek to normalized progress through the animation.
146    fn seek_to(&mut self, progress: f32);
147
148    /// `true` when the animation has reached its terminal state.
149    fn is_complete(&self) -> bool;
150
151    /// Return a type-erased shared reference for downcasting.
152    fn as_any(&self) -> &dyn Any;
153
154    /// Return a type-erased mutable reference for downcasting.
155    fn as_any_mut(&mut self) -> &mut dyn Any;
156}
157
158// ──────────────────────────────────────────────────────────────────────────────
159// Blanket `Interpolate` implementations
160// ──────────────────────────────────────────────────────────────────────────────
161
162impl Interpolate for f32 {
163    /// Direct float lerp — `self + (other - self) * t`.
164    #[inline]
165    fn lerp(&self, other: &Self, t: f32) -> Self {
166        self + (other - self) * t
167    }
168}
169
170impl Interpolate for f64 {
171    /// Full-precision f64 lerp — `t` is cast to f64.
172    #[inline]
173    fn lerp(&self, other: &Self, t: f32) -> Self {
174        let t64 = t as f64;
175        self + (other - self) * t64
176    }
177}
178
179impl Interpolate for [f32; 2] {
180    /// Per-component lerp for 2D vectors.
181    #[inline]
182    fn lerp(&self, other: &Self, t: f32) -> Self {
183        [
184            self[0] + (other[0] - self[0]) * t,
185            self[1] + (other[1] - self[1]) * t,
186        ]
187    }
188}
189
190impl Interpolate for [f32; 3] {
191    /// Per-component lerp for 3D vectors.
192    #[inline]
193    fn lerp(&self, other: &Self, t: f32) -> Self {
194        [
195            self[0] + (other[0] - self[0]) * t,
196            self[1] + (other[1] - self[1]) * t,
197            self[2] + (other[2] - self[2]) * t,
198        ]
199    }
200}
201
202impl Interpolate for [f32; 4] {
203    /// Per-component lerp for 4D vectors (e.g. RGBA colors in linear space).
204    #[inline]
205    fn lerp(&self, other: &Self, t: f32) -> Self {
206        [
207            self[0] + (other[0] - self[0]) * t,
208            self[1] + (other[1] - self[1]) * t,
209            self[2] + (other[2] - self[2]) * t,
210            self[3] + (other[3] - self[3]) * t,
211        ]
212    }
213}
214
215impl Interpolate for i32 {
216    /// Lerps as `f32` and rounds to the nearest integer.
217    #[inline]
218    fn lerp(&self, other: &Self, t: f32) -> Self {
219        let a = *self as f32;
220        let b = *other as f32;
221        (round(a + (b - a) * t)) as i32
222    }
223}
224
225impl Interpolate for u8 {
226    /// Lerps as `f32`, rounds, and clamps to `[0, 255]`.
227    #[inline]
228    fn lerp(&self, other: &Self, t: f32) -> Self {
229        let a = *self as f32;
230        let b = *other as f32;
231        round(a + (b - a) * t).clamp(0.0, 255.0) as u8
232    }
233}
234
235// ──────────────────────────────────────────────────────────────────────────────
236// Tests
237// ──────────────────────────────────────────────────────────────────────────────
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    // --- f32 ---
244    #[test]
245    fn f32_lerp_start() {
246        assert_eq!(0.0_f32.lerp(&100.0, 0.0), 0.0);
247    }
248
249    #[test]
250    fn f32_lerp_end() {
251        assert_eq!(0.0_f32.lerp(&100.0, 1.0), 100.0);
252    }
253
254    #[test]
255    fn f32_lerp_mid() {
256        assert_eq!(0.0_f32.lerp(&100.0, 0.5), 50.0);
257    }
258
259    // --- f64 ---
260    #[test]
261    fn f64_lerp_precision() {
262        let result = 0.0_f64.lerp(&1.0, 0.5);
263        assert!((result - 0.5).abs() < 1e-10);
264    }
265
266    // --- [f32; 2] ---
267    #[test]
268    fn vec2_lerp() {
269        let a = [0.0_f32, 0.0];
270        let b = [10.0_f32, 20.0];
271        let mid = a.lerp(&b, 0.5);
272        assert_eq!(mid, [5.0, 10.0]);
273    }
274
275    // --- [f32; 3] ---
276    #[test]
277    fn vec3_lerp_endpoints() {
278        let a = [1.0_f32, 2.0, 3.0];
279        let b = [4.0_f32, 5.0, 6.0];
280        assert_eq!(a.lerp(&b, 0.0), a);
281        assert_eq!(a.lerp(&b, 1.0), b);
282    }
283
284    // --- [f32; 4] component independence ---
285    #[test]
286    fn vec4_components_independent() {
287        let a = [0.0_f32; 4];
288        let b = [1.0_f32, 2.0, 3.0, 4.0];
289        let mid = a.lerp(&b, 0.5);
290        assert_eq!(mid, [0.5, 1.0, 1.5, 2.0]);
291    }
292
293    // --- i32 rounding ---
294    #[test]
295    fn i32_rounds_correctly() {
296        assert_eq!(0_i32.lerp(&10, 0.55), 6); // 5.5 → rounds to 6
297        assert_eq!(0_i32.lerp(&10, 0.44), 4); // 4.4 → rounds to 4
298    }
299
300    // --- u8 clamping ---
301    #[test]
302    fn u8_clamps_at_255() {
303        assert_eq!(200_u8.lerp(&255, 2.0), 255); // extrapolated, clamped
304    }
305
306    #[test]
307    fn u8_clamps_at_0() {
308        assert_eq!(50_u8.lerp(&0, 2.0), 0); // extrapolated below 0, clamped
309    }
310
311    // --- Update trait contract ---
312    #[test]
313    fn update_returns_false_when_done() {
314        struct OneShot {
315            done: bool,
316        }
317        impl Update for OneShot {
318            fn update(&mut self, _dt: f32) -> bool {
319                if self.done {
320                    return false;
321                }
322                self.done = true;
323                false
324            }
325        }
326        let mut s = OneShot { done: false };
327        assert!(!s.update(0.016));
328        assert!(!s.update(0.016)); // idempotent after done
329    }
330}