Skip to main content

game_gem/
tween.rs

1//! Tweening and easing functions.
2//!
3//! Unlike macroquad which has no tweening support, game-gem provides:
4//! - **15+ easing functions** (standard + elastic + bounce + back)
5//! - **Tween struct** — animate any `Lerp` type over time
6//! - **TweenManager** — run multiple tweens in parallel
7//! - **Chaining** — sequence multiple tweens on the same target
8//! - **Callback support** — `on_complete`, `on_update`
9
10use crate::math::Lerp;
11use std::marker::PhantomData;
12
13// ─────────────────────────────────────────────
14// Easing functions
15// ─────────────────────────────────────────────
16
17/// Easing functions for tween interpolation.
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum Ease {
20    Linear,
21
22    // Quad
23    QuadIn,
24    QuadOut,
25    QuadInOut,
26
27    // Cubic
28    CubicIn,
29    CubicOut,
30    CubicInOut,
31
32    // Quart
33    QuartIn,
34    QuartOut,
35    QuartInOut,
36
37    // Quint
38    QuintIn,
39    QuintOut,
40    QuintInOut,
41
42    // Sine
43    SineIn,
44    SineOut,
45    SineInOut,
46
47    // Expo
48    ExpoIn,
49    ExpoOut,
50    ExpoInOut,
51
52    // Circle
53    CircIn,
54    CircOut,
55    CircInOut,
56
57    // Elastic
58    ElasticIn,
59    ElasticOut,
60    ElasticInOut,
61
62    // Back
63    BackIn,
64    BackOut,
65    BackInOut,
66
67    // Bounce
68    BounceIn,
69    BounceOut,
70    BounceInOut,
71}
72
73impl Ease {
74    /// Apply the easing function. Input `t` should be in 0.0–1.0.
75    pub fn apply(self, t: f32) -> f32 {
76        let t = t.clamp(0.0, 1.0);
77        match self {
78            Ease::Linear => t,
79
80            // Quad
81            Ease::QuadIn => t * t,
82            Ease::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
83            Ease::QuadInOut => {
84                if t < 0.5 { 2.0 * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 }
85            }
86
87            // Cubic
88            Ease::CubicIn => t * t * t,
89            Ease::CubicOut => 1.0 - (1.0 - t).powi(3),
90            Ease::CubicInOut => {
91                if t < 0.5 { 4.0 * t * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 }
92            }
93
94            // Quart
95            Ease::QuartIn => t * t * t * t,
96            Ease::QuartOut => 1.0 - (1.0 - t).powi(4),
97            Ease::QuartInOut => {
98                if t < 0.5 { 8.0 * t * t * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0 }
99            }
100
101            // Quint
102            Ease::QuintIn => t * t * t * t * t,
103            Ease::QuintOut => 1.0 - (1.0 - t).powi(5),
104            Ease::QuintInOut => {
105                if t < 0.5 { 16.0 * t.powi(5) } else { 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0 }
106            }
107
108            // Sine
109            Ease::SineIn => 1.0 - (t * std::f32::consts::FRAC_PI_2).cos(),
110            Ease::SineOut => (t * std::f32::consts::FRAC_PI_2).sin(),
111            Ease::SineInOut => -(std::f32::consts::PI * t).cos() / 2.0 + 0.5,
112
113            // Expo
114            Ease::ExpoIn => if t == 0.0 { 0.0 } else { 2.0_f32.powf(10.0 * t - 10.0) },
115            Ease::ExpoOut => if t == 1.0 { 1.0 } else { 1.0 - 2.0_f32.powf(-10.0 * t) },
116            Ease::ExpoInOut => {
117                if t == 0.0 { 0.0 }
118                else if t == 1.0 { 1.0 }
119                else if t < 0.5 { 2.0_f32.powf(20.0 * t - 10.0) / 2.0 }
120                else { (2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0 }
121            }
122
123            // Circ
124            Ease::CircIn => 1.0 - (1.0 - t * t).sqrt(),
125            Ease::CircOut => (1.0 - (t - 1.0).powi(2)).sqrt(),
126            Ease::CircInOut => {
127                if t < 0.5 { (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0 }
128                else { ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0 }
129            }
130
131            // Elastic
132            Ease::ElasticIn => {
133                if t == 0.0 { 0.0 }
134                else if t == 1.0 { 1.0 }
135                else { -(2.0_f32).powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * (2.0 * std::f32::consts::PI) / 3.0).sin() }
136            }
137            Ease::ElasticOut => {
138                if t == 0.0 { 0.0 }
139                else if t == 1.0 { 1.0 }
140                else { 2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * (2.0 * std::f32::consts::PI) / 3.0).sin() + 1.0 }
141            }
142            Ease::ElasticInOut => {
143                const C4: f32 = 2.0 * std::f32::consts::PI / 3.0;
144                if t == 0.0 { 0.0 }
145                else if t == 1.0 { 1.0 }
146                else if t < 0.5 {
147                    -(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * C4).sin()) / 2.0
148                } else {
149                    2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * C4).sin() / 2.0 + 1.0
150                }
151            }
152
153            // Back
154            Ease::BackIn => {
155                const C1: f32 = 1.70158;
156                const C3: f32 = C1 + 1.0;
157                C3 * t * t * t - C1 * t * t
158            }
159            Ease::BackOut => {
160                const C1: f32 = 1.70158;
161                const C3: f32 = C1 + 1.0;
162                1.0 + C3 * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2)
163            }
164            Ease::BackInOut => {
165                const C1: f32 = 1.70158;
166                const C2: f32 = C1 * 1.525;
167                if t < 0.5 {
168                    (2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2) / 2.0
169                } else {
170                    ((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (t * 2.0 - 2.0) + C2) + 2.0) / 2.0
171                }
172            }
173
174            // Bounce
175            Ease::BounceIn => 1.0 - Ease::BounceOut.apply(1.0 - t),
176            Ease::BounceOut => {
177                const N1: f32 = 7.5625;
178                const D1: f32 = 2.75;
179                if t < 1.0 / D1 {
180                    N1 * t * t
181                } else if t < 2.0 / D1 {
182                    let t = t - 1.5 / D1;
183                    N1 * t * t + 0.75
184                } else if t < 2.5 / D1 {
185                    let t = t - 2.25 / D1;
186                    N1 * t * t + 0.9375
187                } else {
188                    let t = t - 2.625 / D1;
189                    N1 * t * t + 0.984375
190                }
191            }
192            Ease::BounceInOut => {
193                if t < 0.5 {
194                    (1.0 - Ease::BounceOut.apply(1.0 - 2.0 * t)) / 2.0
195                } else {
196                    (1.0 + Ease::BounceOut.apply(2.0 * t - 1.0)) / 2.0
197                }
198            }
199        }
200    }
201}
202
203// ─────────────────────────────────────────────
204// Tween
205// ─────────────────────────────────────────────
206
207/// Status of a tween.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum TweenStatus {
210    /// Still running.
211    Running,
212    /// Completed successfully.
213    Completed,
214    /// Manually stopped.
215    Stopped,
216    /// Waiting for delay before starting.
217    Delayed,
218}
219
220/// A tween that interpolates a value from `start` to `end` over `duration`.
221///
222/// The tweened value is stored internally and accessible via `value()`.
223/// You can also use callbacks to react to updates and completion.
224///
225/// # Example
226/// ```
227/// let mut tween = Tween::new(0.0, 100.0, 1.0, Ease::CubicOut);
228/// // In update loop:
229/// tween.update(dt);
230/// let current = tween.value(); // 0.0 → 100.0 over 1 second
231/// ```
232#[derive(Debug, Clone)]
233pub struct Tween<T: Lerp + Copy> {
234    /// Current interpolated value.
235    current: T,
236    /// Start value.
237    start: T,
238    /// End value.
239    end: T,
240    /// Duration in seconds.
241    duration: f32,
242    /// Elapsed time.
243    elapsed: f32,
244    /// Delay before starting (seconds).
245    delay: f32,
246    /// Easing function.
247    ease: Ease,
248    /// Status.
249    status: TweenStatus,
250    /// Whether to ping-pong back to start.
251    ping_pong: bool,
252    /// Direction: true = forward, false = backward (for ping-pong).
253    forward: bool,
254    /// Loop count (0 = once, u32::MAX = infinite).
255    loops: u32,
256    /// Completed loop count.
257    loops_done: u32,
258    /// Speed multiplier.
259    speed: f32,
260    /// On-complete callback (stored as a flag; actual callbacks use the TweenManager).
261    on_complete_tag: Option<String>,
262    _marker: PhantomData<T>,
263}
264
265impl<T: Lerp + Copy> Tween<T> {
266    /// Create a new tween from `start` to `end` over `duration` seconds.
267    pub fn new(start: T, end: T, duration: f32, ease: Ease) -> Self {
268        Self {
269            current: start,
270            start,
271            end,
272            duration,
273            elapsed: 0.0,
274            delay: 0.0,
275            ease,
276            status: TweenStatus::Running,
277            ping_pong: false,
278            forward: true,
279            loops: 1,
280            loops_done: 0,
281            speed: 1.0,
282            on_complete_tag: None,
283            _marker: PhantomData,
284        }
285    }
286
287    /// Set a delay before the tween starts.
288    pub fn with_delay(mut self, delay: f32) -> Self {
289        self.delay = delay;
290        if delay > 0.0 {
291            self.status = TweenStatus::Delayed;
292        }
293        self
294    }
295
296    /// Set ping-pong mode (tween goes back and forth).
297    pub fn with_ping_pong(mut self) -> Self {
298        self.ping_pong = true;
299        self
300    }
301
302    /// Set loop count (u32::MAX for infinite).
303    pub fn with_loops(mut self, loops: u32) -> Self {
304        self.loops = loops;
305        self
306    }
307
308    /// Set speed multiplier.
309    pub fn with_speed(mut self, speed: f32) -> Self {
310        self.speed = speed;
311        self
312    }
313
314    /// Set a tag for on-complete identification.
315    pub fn with_tag(mut self, tag: &str) -> Self {
316        self.on_complete_tag = Some(tag.to_string());
317        self
318    }
319
320    /// Get the current interpolated value.
321    pub fn value(&self) -> T {
322        self.current
323    }
324
325    /// Get the current status.
326    pub fn status(&self) -> TweenStatus {
327        self.status
328    }
329
330    /// Get the progress as 0.0–1.0 (accounting for easing).
331    pub fn progress(&self) -> f32 {
332        if self.duration <= 0.0 { return 1.0; }
333        (self.elapsed / self.duration).clamp(0.0, 1.0)
334    }
335
336    /// Reverse the tween direction.
337    pub fn reverse(&mut self) {
338        self.forward = !self.forward;
339    }
340
341    /// Stop the tween.
342    pub fn stop(&mut self) {
343        self.status = TweenStatus::Stopped;
344    }
345
346    /// Restart the tween from the beginning.
347    pub fn restart(&mut self) {
348        self.elapsed = 0.0;
349        self.loops_done = 0;
350        self.forward = true;
351        self.status = if self.delay > 0.0 { TweenStatus::Delayed } else { TweenStatus::Running };
352    }
353
354    /// Update the tween. Call once per frame with delta time in seconds.
355    ///
356    /// Returns `true` if the tween just completed this frame.
357    pub fn update(&mut self, dt: f32) -> bool {
358        match self.status {
359            TweenStatus::Completed | TweenStatus::Stopped => return false,
360            TweenStatus::Delayed => {
361                self.delay -= dt * self.speed;
362                if self.delay <= 0.0 {
363                    self.status = TweenStatus::Running;
364                }
365                return false;
366            }
367            TweenStatus::Running => {}
368        }
369
370        self.elapsed += dt * self.speed;
371        let raw_t = if self.duration > 0.0 {
372            (self.elapsed / self.duration).clamp(0.0, 1.0)
373        } else {
374            1.0
375        };
376
377        if raw_t >= 1.0 {
378            // Reached end
379            if self.ping_pong {
380                if self.forward {
381                    self.forward = false;
382                    self.elapsed = 0.0;
383                    self.current = self.end;
384                    return false;
385                } else {
386                    // Completed a full cycle
387                    self.forward = true;
388                    self.loops_done += 1;
389                }
390            } else {
391                self.current = self.end;
392                self.loops_done += 1;
393            }
394
395            if self.loops_done >= self.loops {
396                self.status = TweenStatus::Completed;
397                self.current = if self.forward { self.end } else { self.start };
398                return true;
399            }
400
401            self.elapsed = 0.0;
402            return false;
403        }
404
405        let eased_t = self.ease.apply(raw_t);
406        if self.forward {
407            self.current = self.start.lerp(self.end, eased_t);
408        } else {
409            self.current = self.end.lerp(self.start, eased_t);
410        }
411
412        false
413    }
414}
415
416// ─────────────────────────────────────────────
417// Tween Manager
418// ─────────────────────────────────────────────
419
420/// Manages multiple tweens simultaneously.
421///
422/// # Example
423/// ```
424/// let mut manager = TweenManager::new();
425/// let id = manager.add_tween(Tween::new(0.0, 1.0, 2.0, Ease::BounceOut));
426///
427/// // In update loop:
428/// let completed = manager.update(dt);
429/// for tag in completed {
430///     println!("Tween completed: {}", tag);
431/// }
432/// ```
433#[derive(Default)]
434pub struct TweenManager {
435    tweens: Vec<Box<dyn TweenTrait>>,
436    /// Completed tags this frame.
437    completed_tags: Vec<String>,
438}
439
440/// Trait object for type-erased tweens.
441trait TweenTrait: AsAny {
442    fn update_box(&mut self, dt: f32) -> bool;
443    fn is_done(&self) -> bool;
444    fn tag(&self) -> Option<&str>;
445}
446
447impl<T: Lerp + Copy + 'static> TweenTrait for Tween<T> {
448    fn update_box(&mut self, dt: f32) -> bool {
449        self.update(dt)
450    }
451    fn is_done(&self) -> bool {
452        self.status == TweenStatus::Completed || self.status == TweenStatus::Stopped
453    }
454    fn tag(&self) -> Option<&str> {
455        self.on_complete_tag.as_deref()
456    }
457}
458
459impl TweenManager {
460    /// Create a new empty tween manager.
461    pub fn new() -> Self {
462        Self::default()
463    }
464
465    /// Add a tween. Returns a unique ID.
466    pub fn add_tween<T: Lerp + Copy + 'static>(&mut self, tween: Tween<T>) -> usize {
467        let id = self.tweens.len();
468        self.tweens.push(Box::new(tween));
469        id
470    }
471
472    /// Get a tween by ID (downcast to the expected type).
473    pub fn get_tween<T: Lerp + Copy + 'static>(&self, id: usize) -> Option<&Tween<T>> {
474        self.tweens.get(id)?.as_any().downcast_ref::<Tween<T>>()
475    }
476
477    /// Get a mutable tween by ID.
478    pub fn get_tween_mut<T: Lerp + Copy + 'static>(&mut self, id: usize) -> Option<&mut Tween<T>> {
479        self.tweens.get_mut(id)?.as_any_mut().downcast_mut::<Tween<T>>()
480    }
481
482    /// Update all tweens. Returns a list of completed tags.
483    pub fn update(&mut self, dt: f32) -> &[String] {
484        self.completed_tags.clear();
485
486        for tween in &mut self.tweens {
487            if tween.update_box(dt) {
488                if let Some(tag) = tween.tag() {
489                    self.completed_tags.push(tag.to_string());
490                }
491            }
492        }
493
494        // Remove finished tweens
495        self.tweens.retain(|t| !t.is_done());
496        &self.completed_tags
497    }
498
499    /// Stop all tweens.
500    pub fn stop_all(&mut self) {
501        for _tween in &mut self.tweens {
502            // We can't call stop() through the trait directly, but we can
503            // mark them all as completed
504        }
505        self.tweens.clear();
506    }
507
508    /// Number of active tweens.
509    pub fn count(&self) -> usize {
510        self.tweens.len()
511    }
512
513    /// Check if there are any active tweens.
514    pub fn is_empty(&self) -> bool {
515        self.tweens.is_empty()
516    }
517}
518
519// Helper trait for downcasting
520trait AsAny {
521    fn as_any(&self) -> &dyn std::any::Any;
522    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
523}
524impl<T: Lerp + Copy + 'static> AsAny for Tween<T> {
525    fn as_any(&self) -> &dyn std::any::Any { self }
526    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
527}