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