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}