Skip to main content

proof_engine/tween/
mod.rs

1//! Tween / animation system.
2//!
3//! Provides typed interpolation between values, 30+ easing functions,
4//! multi-track keyframe timelines, and composable animation sequences.
5//! Every interpolation can be driven by a `MathFunction` instead of a simple t ∈ [0,1].
6//!
7//! # Quick start
8//!
9//! ```rust,no_run
10//! use proof_engine::tween::{Tween, Easing};
11//! use glam::Vec3;
12//!
13//! let tween = Tween::new(Vec3::ZERO, Vec3::ONE, 2.0, Easing::EaseInOutCubic);
14//! let pos = tween.sample(1.0); // halfway through → roughly Vec3(0.5, 0.5, 0.5)
15//! ```
16
17pub mod easing;
18pub mod sequence;
19pub mod keyframe;
20
21pub use easing::Easing;
22pub use sequence::{TweenSequence, SequenceBuilder};
23pub use keyframe::{KeyframeTrack, Keyframe};
24
25use glam::{Vec2, Vec3, Vec4};
26
27// ── Lerp trait ─────────────────────────────────────────────────────────────────
28
29/// Values that can be linearly interpolated.
30pub trait Lerp: Clone {
31    fn lerp(a: &Self, b: &Self, t: f32) -> Self;
32    fn zero() -> Self;
33}
34
35impl Lerp for f32 {
36    fn lerp(a: &Self, b: &Self, t: f32) -> Self { a + (b - a) * t }
37    fn zero() -> Self { 0.0 }
38}
39
40impl Lerp for Vec2 {
41    fn lerp(a: &Self, b: &Self, t: f32) -> Self { *a + (*b - *a) * t }
42    fn zero() -> Self { Vec2::ZERO }
43}
44
45impl Lerp for Vec3 {
46    fn lerp(a: &Self, b: &Self, t: f32) -> Self { *a + (*b - *a) * t }
47    fn zero() -> Self { Vec3::ZERO }
48}
49
50impl Lerp for Vec4 {
51    fn lerp(a: &Self, b: &Self, t: f32) -> Self { *a + (*b - *a) * t }
52    fn zero() -> Self { Vec4::ZERO }
53}
54
55// ── Tween<T> ──────────────────────────────────────────────────────────────────
56
57/// A single interpolation from `from` to `to` over `duration` seconds.
58#[derive(Clone, Debug)]
59pub struct Tween<T: Lerp + std::fmt::Debug> {
60    pub from:     T,
61    pub to:       T,
62    pub duration: f32,
63    pub easing:   Easing,
64    pub delay:    f32,
65    /// Automatically reverse and repeat (`yoyo` mode). Negative for infinite.
66    pub repeat:   i32,
67    /// Whether to yoyo (ping-pong) on repeat.
68    pub yoyo:     bool,
69}
70
71impl<T: Lerp + std::fmt::Debug> Tween<T> {
72    pub fn new(from: T, to: T, duration: f32, easing: Easing) -> Self {
73        Self { from, to, duration, easing, delay: 0.0, repeat: 0, yoyo: false }
74    }
75
76    pub fn with_delay(mut self, delay: f32) -> Self {
77        self.delay = delay;
78        self
79    }
80
81    pub fn with_repeat(mut self, repeat: i32, yoyo: bool) -> Self {
82        self.repeat = repeat;
83        self.yoyo = yoyo;
84        self
85    }
86
87    /// Sample the tween at `time` seconds from the start.
88    ///
89    /// Returns the interpolated value. After duration + delay, clamps to `to`
90    /// unless repeat is set.
91    pub fn sample(&self, time: f32) -> T {
92        let t = ((time - self.delay) / self.duration.max(f32::EPSILON)).clamp(0.0, 1.0);
93        let raw_t = self.easing.apply(t);
94        T::lerp(&self.from, &self.to, raw_t)
95    }
96
97    /// Sample with repeat/yoyo handling.
98    pub fn sample_looped(&self, time: f32) -> T {
99        let local = (time - self.delay).max(0.0);
100        let period = self.duration.max(f32::EPSILON);
101        let cycle = (local / period) as i32;
102
103        // Check if we've exceeded repeat count
104        if self.repeat >= 0 && cycle > self.repeat {
105            return if self.yoyo && self.repeat % 2 == 1 {
106                T::lerp(&self.to, &self.from, self.easing.apply(1.0))
107            } else {
108                T::lerp(&self.from, &self.to, self.easing.apply(1.0))
109            };
110        }
111
112        let frac = (local / period).fract();
113        let (a, b) = if self.yoyo && cycle % 2 == 1 {
114            (&self.to, &self.from)
115        } else {
116            (&self.from, &self.to)
117        };
118        T::lerp(a, b, self.easing.apply(frac))
119    }
120
121    /// Returns true if the tween has finished (after duration + delay, accounting for repeat).
122    pub fn is_complete(&self, time: f32) -> bool {
123        let local = (time - self.delay).max(0.0);
124        if self.repeat < 0 { return false; }
125        local >= self.duration * (self.repeat as f32 + 1.0)
126    }
127
128    /// Total duration including all repeats and delay.
129    pub fn total_duration(&self) -> f32 {
130        if self.repeat < 0 { f32::INFINITY }
131        else { self.delay + self.duration * (self.repeat as f32 + 1.0) }
132    }
133}
134
135// ── Specialized constructors ───────────────────────────────────────────────────
136
137/// Convenience methods for common tween patterns.
138pub struct Tweens;
139
140impl Tweens {
141    pub fn fade_in(duration: f32) -> Tween<f32> {
142        Tween::new(0.0, 1.0, duration, Easing::EaseInQuad)
143    }
144
145    pub fn fade_out(duration: f32) -> Tween<f32> {
146        Tween::new(1.0, 0.0, duration, Easing::EaseOutQuad)
147    }
148
149    pub fn bounce_in(from: Vec3, to: Vec3, duration: f32) -> Tween<Vec3> {
150        Tween::new(from, to, duration, Easing::EaseOutBounce)
151    }
152
153    pub fn elastic_pop(from: f32, to: f32, duration: f32) -> Tween<f32> {
154        Tween::new(from, to, duration, Easing::EaseOutElastic)
155    }
156
157    pub fn camera_slide(from: Vec3, to: Vec3, duration: f32) -> Tween<Vec3> {
158        Tween::new(from, to, duration, Easing::EaseInOutCubic)
159    }
160
161    pub fn damage_number_rise(origin: Vec3, height: f32, duration: f32) -> Tween<Vec3> {
162        Tween::new(origin, origin + Vec3::Y * height, duration, Easing::EaseOutCubic)
163    }
164
165    pub fn color_flash(base: Vec4, flash: Vec4, duration: f32) -> Tween<Vec4> {
166        Tween::new(flash, base, duration, Easing::EaseOutExpo)
167    }
168
169    pub fn shake_decay(intensity: f32, duration: f32) -> Tween<f32> {
170        Tween::new(intensity, 0.0, duration, Easing::EaseOutExpo)
171    }
172
173    pub fn health_bar(from: f32, to: f32, duration: f32) -> Tween<f32> {
174        Tween::new(from, to, duration, Easing::EaseOutBack)
175    }
176
177    pub fn pulse(amplitude: f32, rate: f32) -> Tween<f32> {
178        Tween::new(1.0 - amplitude, 1.0 + amplitude, 1.0 / rate, Easing::EaseInOutSine)
179            .with_repeat(-1, true)
180    }
181}
182
183// ── TweenState ─────────────────────────────────────────────────────────────────
184
185/// A running tween with its own clock.
186pub struct TweenState<T: Lerp + std::fmt::Debug> {
187    pub tween: Tween<T>,
188    elapsed:   f32,
189    pub done:  bool,
190}
191
192impl<T: Lerp + std::fmt::Debug> TweenState<T> {
193    pub fn new(tween: Tween<T>) -> Self {
194        Self { done: false, tween, elapsed: 0.0 }
195    }
196
197    /// Advance time and return the current value.
198    pub fn tick(&mut self, dt: f32) -> T {
199        self.elapsed += dt;
200        self.done = self.tween.is_complete(self.elapsed);
201        if self.tween.repeat < 0 || !self.done {
202            self.tween.sample_looped(self.elapsed)
203        } else {
204            self.tween.sample(self.tween.total_duration())
205        }
206    }
207
208    pub fn reset(&mut self) {
209        self.elapsed = 0.0;
210        self.done = false;
211    }
212
213    pub fn value(&self) -> T {
214        self.tween.sample_looped(self.elapsed)
215    }
216
217    pub fn progress(&self) -> f32 {
218        (self.elapsed / self.tween.duration.max(f32::EPSILON)).clamp(0.0, 1.0)
219    }
220}
221
222// ── AnimationGroup ─────────────────────────────────────────────────────────────
223
224/// Runs multiple f32 tweens in parallel, identified by string key.
225pub struct AnimationGroup {
226    tweens: std::collections::HashMap<String, TweenState<f32>>,
227}
228
229impl AnimationGroup {
230    pub fn new() -> Self { Self { tweens: std::collections::HashMap::new() } }
231
232    pub fn add(&mut self, key: impl Into<String>, tween: Tween<f32>) {
233        self.tweens.insert(key.into(), TweenState::new(tween));
234    }
235
236    pub fn remove(&mut self, key: &str) {
237        self.tweens.remove(key);
238    }
239
240    pub fn tick(&mut self, dt: f32) {
241        self.tweens.values_mut().for_each(|t| { t.tick(dt); });
242        self.tweens.retain(|_, t| !t.done);
243    }
244
245    pub fn get(&self, key: &str) -> f32 {
246        self.tweens.get(key).map(|t| t.value()).unwrap_or(0.0)
247    }
248
249    pub fn is_running(&self, key: &str) -> bool {
250        self.tweens.contains_key(key)
251    }
252
253    pub fn all_done(&self) -> bool {
254        self.tweens.is_empty()
255    }
256}
257
258impl Default for AnimationGroup {
259    fn default() -> Self { Self::new() }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn tween_endpoints() {
268        let tw = Tween::new(0.0f32, 10.0f32, 2.0, Easing::Linear);
269        assert!((tw.sample(0.0) - 0.0).abs() < 1e-4);
270        assert!((tw.sample(2.0) - 10.0).abs() < 1e-4);
271        assert!((tw.sample(1.0) - 5.0).abs() < 1e-4);
272    }
273
274    #[test]
275    fn tween_complete() {
276        let tw = Tween::new(0.0f32, 1.0f32, 1.0, Easing::Linear);
277        assert!(!tw.is_complete(0.5));
278        assert!(tw.is_complete(1.0));
279        assert!(tw.is_complete(2.0));
280    }
281
282    #[test]
283    fn tween_delay() {
284        let tw = Tween::new(0.0f32, 1.0f32, 1.0, Easing::Linear).with_delay(0.5);
285        assert!((tw.sample(0.0) - 0.0).abs() < 1e-4);
286        assert!((tw.sample(0.5) - 0.0).abs() < 1e-4);
287        assert!((tw.sample(1.5) - 1.0).abs() < 1e-4);
288    }
289
290    #[test]
291    fn tween_state_advances() {
292        let tw = Tween::new(0.0f32, 1.0f32, 0.5, Easing::Linear);
293        let mut state = TweenState::new(tw);
294        let v = state.tick(0.25);
295        assert!((v - 0.5).abs() < 1e-4, "expected 0.5 got {v}");
296        assert!(!state.done);
297        state.tick(0.25);
298        assert!(state.done);
299    }
300
301    #[test]
302    fn vec3_tween() {
303        let tw = Tween::new(Vec3::ZERO, Vec3::ONE, 1.0, Easing::Linear);
304        let mid = tw.sample(0.5);
305        assert!((mid.x - 0.5).abs() < 1e-4);
306        assert!((mid.y - 0.5).abs() < 1e-4);
307        assert!((mid.z - 0.5).abs() < 1e-4);
308    }
309}