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::easing::Easing;
9use crate::math::round;
10use core::any::Any;
11
12/// A value that supports linear interpolation between two instances.
13///
14/// Implement this trait for any type you want to animate with Animato.
15/// The library ships blanket impls for `f32`, `f64`, `[f32; 2]`,
16/// `[f32; 3]`, `[f32; 4]`, `i32`, and `u8`.
17///
18/// # Contract
19///
20/// - `self.lerp(other, 0.0)` must equal `*self`
21/// - `self.lerp(other, 1.0)` must equal `*other`
22/// - `t` outside `[0.0, 1.0]` is allowed — implementations may extrapolate or clamp
23///
24/// # Example
25///
26/// ```rust
27/// use animato_core::Interpolate;
28///
29/// let a = 0.0_f32;
30/// let b = 100.0_f32;
31/// assert_eq!(a.lerp(&b, 0.0), 0.0);
32/// assert_eq!(a.lerp(&b, 1.0), 100.0);
33/// assert_eq!(a.lerp(&b, 0.5), 50.0);
34/// ```
35pub trait Interpolate: Sized {
36    /// Linearly interpolate from `self` to `other` by factor `t`.
37    ///
38    /// `t = 0.0` returns `self`, `t = 1.0` returns `other`.
39    fn lerp(&self, other: &Self, t: f32) -> Self;
40}
41
42/// Marker trait for types that can be used as animation targets.
43///
44/// Any type implementing `Interpolate + Clone + 'static` automatically
45/// satisfies `Animatable` through a blanket impl. You never implement
46/// this trait manually.
47///
48/// # Example
49///
50/// ```rust
51/// use animato_core::Animatable;
52///
53/// fn animate<T: Animatable>(start: T, end: T) {
54///     let mid = start.lerp(&end, 0.5);
55///     let _ = mid;
56/// }
57///
58/// animate(0.0_f32, 1.0_f32);
59/// animate([0.0_f32; 3], [1.0_f32; 3]);
60/// ```
61pub trait Animatable: Interpolate + Clone + 'static {}
62
63/// Blanket impl — every `Interpolate + Clone + 'static` is automatically `Animatable`.
64impl<T: Interpolate + Clone + 'static> Animatable for T {}
65
66/// A value that advances through time.
67///
68/// Implemented by `Tween<T>`, `Spring`, `SpringN<T>`,
69/// `Timeline`, and any user-defined animation type.
70/// The `AnimationDriver` (see `animato-driver`) calls this each frame.
71///
72/// # Contract
73///
74/// - Returns `true` while the animation is still running.
75/// - Returns `false` when the animation has completed (or is settled, for springs).
76/// - Calling `update` after returning `false` is a no-op and returns `false`.
77/// - `dt < 0.0` is treated as `0.0` — time does not run backward.
78///
79/// # Example
80///
81/// ```rust
82/// use animato_core::Update;
83///
84/// struct Counter { count: u32 }
85///
86/// impl Update for Counter {
87///     fn update(&mut self, _dt: f32) -> bool {
88///         self.count += 1;
89///         self.count < 10
90///     }
91/// }
92///
93/// let mut c = Counter { count: 0 };
94/// while c.update(0.016) {}
95/// assert_eq!(c.count, 10);
96/// ```
97pub trait Update {
98    /// Advance the animation by `dt` seconds.
99    ///
100    /// Returns `true` while still running, `false` when complete.
101    fn update(&mut self, dt: f32) -> bool;
102}
103
104/// High-level animation category reported to DevTools and inspectors.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub enum AnimationKind {
108    /// A tween from one value to another.
109    Tween,
110    /// A damped spring animation.
111    Spring,
112    /// A keyframe track.
113    Keyframe,
114    /// A composed timeline.
115    Timeline,
116    /// A grouped animation.
117    Group,
118    /// A custom user animation.
119    Custom,
120}
121
122/// Coarse playback state reported by inspectable animations.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125pub enum PlaybackState {
126    /// Waiting to start or reset to the beginning.
127    Idle,
128    /// Actively advancing.
129    Playing,
130    /// Paused mid-playback.
131    Paused,
132    /// Finished or settled.
133    Complete,
134}
135
136/// Renderer-agnostic runtime state for a single animation.
137#[derive(Clone, Debug, PartialEq)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct AnimationIntrospection {
140    /// High-level animation category.
141    pub kind: AnimationKind,
142    /// Normalized progress in `[0.0, 1.0]` when available.
143    pub progress: f32,
144    /// Elapsed seconds reported by the animation.
145    pub elapsed: f32,
146    /// Finite duration in seconds, or `None` for unbounded animations such as springs.
147    pub duration: Option<f32>,
148    /// Coarse playback state.
149    pub state: PlaybackState,
150    /// Active easing curve when the animation has one.
151    pub easing: Option<Easing>,
152}
153
154impl AnimationIntrospection {
155    /// Create a sanitized introspection record.
156    pub fn new(
157        kind: AnimationKind,
158        progress: f32,
159        elapsed: f32,
160        duration: Option<f32>,
161        state: PlaybackState,
162        easing: Option<Easing>,
163    ) -> Self {
164        Self {
165            kind,
166            progress: progress.clamp(0.0, 1.0),
167            elapsed: elapsed.max(0.0),
168            duration: duration
169                .filter(|value| value.is_finite())
170                .map(|value| value.max(0.0)),
171            state,
172            easing,
173        }
174    }
175}
176
177/// Animation interface for DevTools and runtime inspectors.
178///
179/// This extends [`Update`] with read-only runtime state. It is implemented by
180/// Animato's built-in animation types and can be implemented by custom
181/// animations that want to participate in [`AnimationDriver`](https://docs.rs/animato-driver)
182/// snapshots.
183pub trait Inspectable: Update {
184    /// Return a compact, allocation-free snapshot of the current animation state.
185    fn introspect(&self) -> AnimationIntrospection;
186}
187
188/// Object-safe animation interface used by composition containers.
189///
190/// [`Update`] is intentionally tiny: it only advances an animation. `Playable`
191/// adds the small amount of control a timeline needs to reset and seek children
192/// without knowing their concrete type.
193///
194/// Implementations should interpret [`seek_to`](Self::seek_to) as normalized
195/// progress through their finite duration, clamped to `[0.0, 1.0]`.
196///
197/// # Example
198///
199/// ```rust
200/// use animato_core::{Playable, Update};
201///
202/// #[derive(Default)]
203/// struct Clip { elapsed: f32 }
204///
205/// impl Update for Clip {
206///     fn update(&mut self, dt: f32) -> bool {
207///         self.elapsed = (self.elapsed + dt.max(0.0)).min(1.0);
208///         !self.is_complete()
209///     }
210/// }
211///
212/// impl Playable for Clip {
213///     fn duration(&self) -> f32 { 1.0 }
214///     fn reset(&mut self) { self.elapsed = 0.0; }
215///     fn seek_to(&mut self, progress: f32) {
216///         self.elapsed = progress.clamp(0.0, 1.0);
217///     }
218///     fn is_complete(&self) -> bool { self.elapsed >= 1.0 }
219///     fn as_any(&self) -> &dyn core::any::Any { self }
220///     fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self }
221/// }
222/// ```
223pub trait Playable: Update + Any {
224    /// Total finite duration in seconds.
225    fn duration(&self) -> f32;
226
227    /// Reset the animation to its initial state.
228    fn reset(&mut self);
229
230    /// Seek to normalized progress through the animation.
231    fn seek_to(&mut self, progress: f32);
232
233    /// `true` when the animation has reached its terminal state.
234    fn is_complete(&self) -> bool;
235
236    /// Return a type-erased shared reference for downcasting.
237    fn as_any(&self) -> &dyn Any;
238
239    /// Return a type-erased mutable reference for downcasting.
240    fn as_any_mut(&mut self) -> &mut dyn Any;
241}
242
243// ──────────────────────────────────────────────────────────────────────────────
244// Blanket `Interpolate` implementations
245// ──────────────────────────────────────────────────────────────────────────────
246
247impl Interpolate for f32 {
248    /// Direct float lerp — `self + (other - self) * t`.
249    #[inline]
250    fn lerp(&self, other: &Self, t: f32) -> Self {
251        self + (other - self) * t
252    }
253}
254
255impl Interpolate for f64 {
256    /// Full-precision f64 lerp — `t` is cast to f64.
257    #[inline]
258    fn lerp(&self, other: &Self, t: f32) -> Self {
259        let t64 = t as f64;
260        self + (other - self) * t64
261    }
262}
263
264impl Interpolate for [f32; 2] {
265    /// Per-component lerp for 2D vectors.
266    #[inline]
267    fn lerp(&self, other: &Self, t: f32) -> Self {
268        [
269            self[0] + (other[0] - self[0]) * t,
270            self[1] + (other[1] - self[1]) * t,
271        ]
272    }
273}
274
275impl Interpolate for [f32; 3] {
276    /// Per-component lerp for 3D vectors.
277    #[inline]
278    fn lerp(&self, other: &Self, t: f32) -> Self {
279        [
280            self[0] + (other[0] - self[0]) * t,
281            self[1] + (other[1] - self[1]) * t,
282            self[2] + (other[2] - self[2]) * t,
283        ]
284    }
285}
286
287impl Interpolate for [f32; 4] {
288    /// Per-component lerp for 4D vectors (e.g. RGBA colors in linear space).
289    #[inline]
290    fn lerp(&self, other: &Self, t: f32) -> Self {
291        [
292            self[0] + (other[0] - self[0]) * t,
293            self[1] + (other[1] - self[1]) * t,
294            self[2] + (other[2] - self[2]) * t,
295            self[3] + (other[3] - self[3]) * t,
296        ]
297    }
298}
299
300impl Interpolate for i32 {
301    /// Lerps as `f32` and rounds to the nearest integer.
302    #[inline]
303    fn lerp(&self, other: &Self, t: f32) -> Self {
304        let a = *self as f32;
305        let b = *other as f32;
306        (round(a + (b - a) * t)) as i32
307    }
308}
309
310impl Interpolate for u8 {
311    /// Lerps as `f32`, rounds, and clamps to `[0, 255]`.
312    #[inline]
313    fn lerp(&self, other: &Self, t: f32) -> Self {
314        let a = *self as f32;
315        let b = *other as f32;
316        round(a + (b - a) * t).clamp(0.0, 255.0) as u8
317    }
318}
319
320// ──────────────────────────────────────────────────────────────────────────────
321// Tests
322// ──────────────────────────────────────────────────────────────────────────────
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    // --- f32 ---
329    #[test]
330    fn f32_lerp_start() {
331        assert_eq!(0.0_f32.lerp(&100.0, 0.0), 0.0);
332    }
333
334    #[test]
335    fn f32_lerp_end() {
336        assert_eq!(0.0_f32.lerp(&100.0, 1.0), 100.0);
337    }
338
339    #[test]
340    fn f32_lerp_mid() {
341        assert_eq!(0.0_f32.lerp(&100.0, 0.5), 50.0);
342    }
343
344    // --- f64 ---
345    #[test]
346    fn f64_lerp_precision() {
347        let result = 0.0_f64.lerp(&1.0, 0.5);
348        assert!((result - 0.5).abs() < 1e-10);
349    }
350
351    // --- [f32; 2] ---
352    #[test]
353    fn vec2_lerp() {
354        let a = [0.0_f32, 0.0];
355        let b = [10.0_f32, 20.0];
356        let mid = a.lerp(&b, 0.5);
357        assert_eq!(mid, [5.0, 10.0]);
358    }
359
360    // --- [f32; 3] ---
361    #[test]
362    fn vec3_lerp_endpoints() {
363        let a = [1.0_f32, 2.0, 3.0];
364        let b = [4.0_f32, 5.0, 6.0];
365        assert_eq!(a.lerp(&b, 0.0), a);
366        assert_eq!(a.lerp(&b, 1.0), b);
367    }
368
369    // --- [f32; 4] component independence ---
370    #[test]
371    fn vec4_components_independent() {
372        let a = [0.0_f32; 4];
373        let b = [1.0_f32, 2.0, 3.0, 4.0];
374        let mid = a.lerp(&b, 0.5);
375        assert_eq!(mid, [0.5, 1.0, 1.5, 2.0]);
376    }
377
378    // --- i32 rounding ---
379    #[test]
380    fn i32_rounds_correctly() {
381        assert_eq!(0_i32.lerp(&10, 0.55), 6); // 5.5 → rounds to 6
382        assert_eq!(0_i32.lerp(&10, 0.44), 4); // 4.4 → rounds to 4
383    }
384
385    // --- u8 clamping ---
386    #[test]
387    fn u8_clamps_at_255() {
388        assert_eq!(200_u8.lerp(&255, 2.0), 255); // extrapolated, clamped
389    }
390
391    #[test]
392    fn u8_clamps_at_0() {
393        assert_eq!(50_u8.lerp(&0, 2.0), 0); // extrapolated below 0, clamped
394    }
395
396    // --- Update trait contract ---
397    #[test]
398    fn update_returns_false_when_done() {
399        struct OneShot {
400            done: bool,
401        }
402        impl Update for OneShot {
403            fn update(&mut self, _dt: f32) -> bool {
404                if self.done {
405                    return false;
406                }
407                self.done = true;
408                false
409            }
410        }
411        let mut s = OneShot { done: false };
412        assert!(!s.update(0.016));
413        assert!(!s.update(0.016)); // idempotent after done
414    }
415}