Skip to main content

animato_tween/
tween.rs

1//! Core [`Tween<T>`] type and [`TweenState`] enum.
2
3use crate::loop_mode::Loop;
4use animato_core::{Animatable, Easing, Playable, Update};
5
6/// The current execution state of a [`Tween`].
7#[derive(Clone, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum TweenState {
10    /// Waiting for the delay period to expire before starting.
11    Idle,
12    /// Actively animating.
13    Running,
14    /// Paused mid-animation — `update()` calls are no-ops.
15    Paused,
16    /// Finished. Further `update()` calls return `false` immediately.
17    Completed,
18}
19
20/// A single-value animation from `start` to `end` over `duration` seconds.
21///
22/// Build with [`Tween::new`] and the consuming builder chain:
23///
24/// ```rust
25/// use animato_tween::Tween;
26/// use animato_core::Easing;
27///
28/// let mut t = Tween::new(0.0_f32, 100.0)
29///     .duration(1.5)
30///     .easing(Easing::EaseOutCubic)
31///     .delay(0.2)
32///     .build();
33/// ```
34///
35/// # `no_std`
36///
37/// `Tween<T>` is stack-allocated — no heap allocation occurs in `update()` or `value()`.
38#[derive(Clone, Debug)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct Tween<T: Animatable> {
41    /// The value at `t = 0`.
42    pub start: T,
43    /// The value at `t = duration`.
44    pub end: T,
45    /// Total animation duration in seconds. Clamped to `≥ 0`.
46    pub duration: f32,
47    /// Easing curve applied to the normalised progress.
48    pub easing: Easing,
49    /// Delay in seconds before the animation begins.
50    pub delay: f32,
51    /// Time scale multiplier. `1.0` = normal, `2.0` = double speed.
52    pub time_scale: f32,
53    /// Looping behaviour.
54    pub looping: Loop,
55
56    // ── private state ────────────────────────────────────────────────────────
57    elapsed: f32,
58    delay_elapsed: f32,
59    state: TweenState,
60    loop_count: u32,
61    /// When PingPong is active and we're in the backward pass.
62    ping_pong_reverse: bool,
63}
64
65impl<T: Animatable> Tween<T> {
66    // ── Construction (called by TweenBuilder) ────────────────────────────────
67
68    /// Create via [`TweenBuilder`](crate::TweenBuilder) — use `Tween::new(start, end)`.
69    #[doc(hidden)]
70    pub(crate) fn from_builder(
71        start: T,
72        end: T,
73        duration: f32,
74        easing: Easing,
75        delay: f32,
76        time_scale: f32,
77        looping: Loop,
78    ) -> Self {
79        let initial_state = if delay > 0.0 {
80            TweenState::Idle
81        } else {
82            TweenState::Running
83        };
84        Self {
85            start,
86            end,
87            duration: duration.max(0.0),
88            easing,
89            delay: delay.max(0.0),
90            time_scale: time_scale.max(0.0),
91            looping,
92            elapsed: 0.0,
93            delay_elapsed: 0.0,
94            state: initial_state,
95            loop_count: 0,
96            ping_pong_reverse: false,
97        }
98    }
99
100    // ── Public API ───────────────────────────────────────────────────────────
101
102    /// The current interpolated value.
103    ///
104    /// This is the hot path — no allocation, just a lerp.
105    ///
106    /// ```rust
107    /// use animato_tween::Tween;
108    /// use animato_core::Easing;
109    ///
110    /// let t = Tween::new(0.0_f32, 100.0).duration(1.0).build();
111    /// assert_eq!(t.value(), 0.0); // hasn't started yet
112    /// ```
113    pub fn value(&self) -> T {
114        if self.duration == 0.0 {
115            return self.end.clone();
116        }
117        let raw_t = (self.elapsed / self.duration).clamp(0.0, 1.0);
118        let curved_t = self.easing.apply(raw_t);
119        if self.ping_pong_reverse {
120            self.end.lerp(&self.start, curved_t)
121        } else {
122            self.start.lerp(&self.end, curved_t)
123        }
124    }
125
126    /// Normalised progress in `[0.0, 1.0]` — raw, before easing.
127    pub fn progress(&self) -> f32 {
128        if self.duration == 0.0 {
129            return 1.0;
130        }
131        (self.elapsed / self.duration).clamp(0.0, 1.0)
132    }
133
134    /// Normalised progress after easing is applied.
135    pub fn eased_progress(&self) -> f32 {
136        self.easing.apply(self.progress())
137    }
138
139    /// `true` when the tween has finished all its loops.
140    pub fn is_complete(&self) -> bool {
141        self.state == TweenState::Completed
142    }
143
144    /// Current execution state.
145    pub fn state(&self) -> &TweenState {
146        &self.state
147    }
148
149    /// Reset to the very beginning, including delay and loop counter.
150    pub fn reset(&mut self) {
151        self.elapsed = 0.0;
152        self.delay_elapsed = 0.0;
153        self.loop_count = 0;
154        self.ping_pong_reverse = false;
155        self.state = if self.delay > 0.0 {
156            TweenState::Idle
157        } else {
158            TweenState::Running
159        };
160    }
161
162    /// Jump to a normalised time `t ∈ [0, 1]` within the current pass.
163    ///
164    /// Does not affect loop count or ping-pong direction.
165    pub fn seek(&mut self, t: f32) {
166        self.elapsed = (t.clamp(0.0, 1.0) * self.duration).max(0.0);
167        if self.state == TweenState::Completed {
168            self.state = TweenState::Running;
169        }
170    }
171
172    /// Swap `start` and `end` in place.
173    ///
174    /// The animation immediately plays toward the new `end`.
175    ///
176    /// When called mid-animation, the current visual position is preserved —
177    /// `elapsed` is mirrored so the object appears to continue from where it is.
178    ///
179    /// # Example
180    ///
181    /// ```rust
182    /// use animato_tween::Tween;
183    /// use animato_core::{Easing, Update};
184    ///
185    /// let mut t = Tween::new(0.0_f32, 100.0)
186    ///     .duration(1.0)
187    ///     .easing(Easing::Linear)
188    ///     .build();
189    /// // Advance 30% of the way
190    /// t.update(0.3);
191    /// assert!((t.value() - 30.0).abs() < 1.0);
192    /// // Reverse — now at 70% of the backward journey (100→0)
193    /// t.reverse();
194    /// assert!((t.value() - 30.0).abs() < 1.0); // same visual position
195    /// ```
196    pub fn reverse(&mut self) {
197        core::mem::swap(&mut self.start, &mut self.end);
198        // Mirror elapsed: preserves the current visual position after swap.
199        // e.g. 30% forward → 70% backward, same screen position.
200        self.elapsed = (self.duration - self.elapsed).clamp(0.0, self.duration);
201        if self.state == TweenState::Completed {
202            self.state = TweenState::Running;
203        }
204    }
205
206    /// Pause — `update()` calls become no-ops until [`resume`](Self::resume).
207    pub fn pause(&mut self) {
208        if self.state == TweenState::Running {
209            self.state = TweenState::Paused;
210        }
211    }
212
213    /// Resume from a paused state.
214    pub fn resume(&mut self) {
215        if self.state == TweenState::Paused {
216            self.state = TweenState::Running;
217        }
218    }
219
220    #[inline]
221    fn playback_duration(&self) -> f32 {
222        match self.looping {
223            Loop::Once => self.delay + self.duration,
224            Loop::Times(n) => self.delay + self.duration * n.max(1) as f32,
225            Loop::Forever | Loop::PingPong => f32::INFINITY,
226        }
227    }
228}
229
230impl<T: Animatable> Update for Tween<T> {
231    /// Advance the tween by `dt` seconds.
232    ///
233    /// Returns `true` while still running, `false` when complete.
234    /// Negative `dt` is treated as `0.0`.
235    fn update(&mut self, dt: f32) -> bool {
236        let dt = dt.max(0.0);
237
238        match self.state {
239            TweenState::Completed => return false,
240            TweenState::Paused => return true,
241            TweenState::Idle => {
242                // Drain delay bucket
243                self.delay_elapsed += dt;
244                if self.delay_elapsed < self.delay {
245                    return true;
246                }
247                // Carry the overflow into running time
248                let overflow = self.delay_elapsed - self.delay;
249                self.state = TweenState::Running;
250                self.elapsed += overflow * self.time_scale;
251            }
252            TweenState::Running => {
253                self.elapsed += dt * self.time_scale;
254            }
255        }
256
257        // Zero-duration tween completes immediately
258        if self.duration == 0.0 {
259            self.state = TweenState::Completed;
260            return false;
261        }
262
263        // Handle loop overflow
264        while self.elapsed >= self.duration {
265            match &self.looping {
266                Loop::Once => {
267                    self.elapsed = self.duration;
268                    self.state = TweenState::Completed;
269                    return false;
270                }
271                Loop::Times(n) => {
272                    self.loop_count += 1;
273                    if self.loop_count >= *n {
274                        self.elapsed = self.duration;
275                        self.state = TweenState::Completed;
276                        return false;
277                    }
278                    self.elapsed -= self.duration;
279                }
280                Loop::Forever => {
281                    self.elapsed -= self.duration;
282                }
283                Loop::PingPong => {
284                    self.elapsed -= self.duration;
285                    self.ping_pong_reverse = !self.ping_pong_reverse;
286                }
287            }
288        }
289
290        true
291    }
292}
293
294impl<T: Animatable> Playable for Tween<T> {
295    fn duration(&self) -> f32 {
296        self.playback_duration()
297    }
298
299    fn reset(&mut self) {
300        Tween::reset(self);
301    }
302
303    fn seek_to(&mut self, progress: f32) {
304        let progress = progress.clamp(0.0, 1.0);
305        let total = self.playback_duration();
306        let finite_total = if total.is_finite() {
307            total
308        } else {
309            self.delay + self.duration
310        };
311
312        Tween::reset(self);
313
314        if finite_total == 0.0 {
315            self.elapsed = self.duration;
316            self.state = TweenState::Completed;
317            return;
318        }
319
320        let secs = finite_total * progress;
321        if secs < self.delay {
322            self.delay_elapsed = secs;
323            self.state = if self.delay > 0.0 {
324                TweenState::Idle
325            } else {
326                TweenState::Running
327            };
328            return;
329        }
330
331        let anim_secs = (secs - self.delay).max(0.0);
332        if self.duration == 0.0 {
333            self.elapsed = 0.0;
334            self.state = TweenState::Completed;
335            return;
336        }
337
338        match self.looping {
339            Loop::Once => {
340                self.elapsed = anim_secs.min(self.duration);
341                self.state = if progress >= 1.0 {
342                    TweenState::Completed
343                } else {
344                    TweenState::Running
345                };
346            }
347            Loop::Times(n) => {
348                let plays = n.max(1);
349                let total_anim = self.duration * plays as f32;
350                if anim_secs >= total_anim || progress >= 1.0 {
351                    self.loop_count = plays;
352                    self.elapsed = self.duration;
353                    self.state = TweenState::Completed;
354                } else {
355                    self.loop_count = (anim_secs / self.duration) as u32;
356                    self.elapsed = anim_secs - self.duration * self.loop_count as f32;
357                    self.state = TweenState::Running;
358                }
359            }
360            Loop::Forever => {
361                self.elapsed = anim_secs % self.duration;
362                self.state = TweenState::Running;
363            }
364            Loop::PingPong => {
365                let cycle = anim_secs % (self.duration * 2.0);
366                self.ping_pong_reverse = cycle >= self.duration;
367                self.elapsed = if self.ping_pong_reverse {
368                    cycle - self.duration
369                } else {
370                    cycle
371                };
372                self.state = TweenState::Running;
373            }
374        }
375    }
376
377    fn is_complete(&self) -> bool {
378        Tween::is_complete(self)
379    }
380
381    fn as_any(&self) -> &dyn core::any::Any {
382        self
383    }
384
385    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
386        self
387    }
388}
389
390// ──────────────────────────────────────────────────────────────────────────────
391// Tests
392// ──────────────────────────────────────────────────────────────────────────────
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::loop_mode::Loop;
398    use animato_core::Easing;
399
400    fn make(start: f32, end: f32, duration: f32) -> Tween<f32> {
401        Tween::new(start, end).duration(duration).build()
402    }
403
404    #[test]
405    fn value_at_start_equals_start() {
406        let t = make(10.0, 90.0, 2.0);
407        assert_eq!(t.value(), 10.0);
408    }
409
410    #[test]
411    fn value_at_end_equals_end() {
412        let mut t = make(10.0, 90.0, 1.0);
413        t.update(1.0);
414        assert_eq!(t.value(), 90.0);
415    }
416
417    #[test]
418    fn is_complete_after_full_duration() {
419        let mut t = make(0.0, 1.0, 1.0);
420        t.update(1.0);
421        assert!(t.is_complete());
422    }
423
424    #[test]
425    fn large_dt_completes_cleanly() {
426        let mut t = make(0.0, 1.0, 1.0);
427        t.update(100.0);
428        assert!(t.is_complete());
429        assert_eq!(t.value(), 1.0);
430    }
431
432    #[test]
433    fn no_update_after_complete() {
434        let mut t = make(0.0, 1.0, 0.5);
435        t.update(1.0);
436        assert!(!t.update(1.0)); // still returns false, no panic
437    }
438
439    #[test]
440    fn delay_holds_at_start() {
441        let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
442        t.update(0.25); // still in delay
443        assert_eq!(t.value(), 0.0);
444        assert_eq!(t.state(), &TweenState::Idle);
445    }
446
447    #[test]
448    fn delay_transitions_to_running() {
449        let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
450        t.update(0.5); // exactly at delay end → now Running
451        assert_eq!(t.state(), &TweenState::Running);
452    }
453
454    #[test]
455    fn seek_jumps_to_midpoint() {
456        let mut t = make(0.0, 100.0, 1.0);
457        t.seek(0.5);
458        // Linear easing: midpoint = 50.0
459        let t2 = Tween::new(0.0_f32, 100.0)
460            .duration(1.0)
461            .easing(Easing::Linear)
462            .build();
463        let mut t2 = t2;
464        t2.seek(0.5);
465        assert!((t2.value() - 50.0).abs() < 0.01);
466    }
467
468    #[test]
469    fn reverse_swaps_direction() {
470        let mut t = Tween::new(0.0_f32, 100.0)
471            .duration(1.0)
472            .easing(Easing::Linear)
473            .build();
474        // Advance 40% forward
475        t.update(0.4);
476        let before = t.value(); // ~40.0
477        // Reverse: visual position preserved, now animating toward 0
478        t.reverse();
479        // Immediately after reverse, same visual position
480        assert!(
481            (t.value() - before).abs() < 1.0,
482            "visual position should be preserved: before={} after={}",
483            before,
484            t.value()
485        );
486        // One more step: value should decrease
487        t.update(0.1);
488        assert!(t.value() < before, "value should decrease after reverse");
489    }
490
491    #[test]
492    fn pause_stops_progress() {
493        let mut t = make(0.0, 1.0, 2.0);
494        t.update(0.5);
495        let v_before = t.value();
496        t.pause();
497        t.update(0.5); // should not advance
498        assert_eq!(t.value(), v_before);
499    }
500
501    #[test]
502    fn resume_continues_progress() {
503        let mut t = make(0.0, 1.0, 2.0);
504        t.update(0.5);
505        t.pause();
506        t.update(0.5); // no-op while paused
507        let v_paused = t.value();
508        t.resume();
509        t.update(0.5); // should advance
510        assert!(
511            t.value() > v_paused,
512            "resumed tween must advance past v_paused={}",
513            v_paused
514        );
515    }
516
517    #[test]
518    fn loop_times_completes_after_n() {
519        let mut t = Tween::new(0.0_f32, 1.0)
520            .duration(1.0)
521            .looping(Loop::Times(3))
522            .build();
523        // 3 × 1.0s + small epsilon to push past the 3rd boundary
524        t.update(3.0 + f32::EPSILON);
525        assert!(t.is_complete());
526    }
527
528    #[test]
529    fn loop_forever_never_completes() {
530        let mut t = Tween::new(0.0_f32, 1.0)
531            .duration(1.0)
532            .looping(Loop::Forever)
533            .build();
534        for _ in 0..1000 {
535            t.update(0.1);
536        }
537        assert!(!t.is_complete());
538    }
539
540    #[test]
541    fn pingpong_reverses_direction() {
542        let mut t = Tween::new(0.0_f32, 100.0)
543            .duration(1.0)
544            .easing(Easing::Linear)
545            .looping(Loop::PingPong)
546            .build();
547        // Forward pass → value should be 100 at t=1.0
548        t.update(1.0);
549        // Now in reverse: at halfway through backward pass value should be ~50
550        t.update(0.5);
551        let v = t.value();
552        assert!(v > 40.0 && v < 60.0, "pingpong mid-reverse = {}", v);
553    }
554
555    #[test]
556    fn reset_returns_to_idle_with_delay() {
557        let mut t = Tween::new(0.0_f32, 1.0).duration(1.0).delay(0.5).build();
558        t.update(2.0); // complete
559        t.reset();
560        assert_eq!(t.state(), &TweenState::Idle);
561        assert_eq!(t.value(), 0.0);
562    }
563
564    #[test]
565    fn zero_duration_completes_immediately() {
566        let mut t = make(0.0, 100.0, 0.0);
567        t.update(0.0);
568        assert!(t.is_complete());
569        assert_eq!(t.value(), 100.0);
570    }
571
572    #[test]
573    fn negative_dt_is_noop() {
574        let mut t = make(0.0, 100.0, 1.0);
575        t.update(-5.0);
576        assert_eq!(t.value(), 0.0);
577    }
578}