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