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