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}