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::PingPongTimes(n) => self.delay + self.duration * n.max(1) as f32,
276            Loop::Forever | Loop::PingPong => f32::INFINITY,
277        }
278    }
279
280    #[inline]
281    fn complete_ping_pong_times(&mut self, passes: u32) -> bool {
282        self.loop_count = passes;
283        self.elapsed = self.duration;
284        self.ping_pong_reverse = passes.is_multiple_of(2);
285        self.state = TweenState::Completed;
286        false
287    }
288}
289
290impl<T: Animatable> Update for Tween<T> {
291    /// Advance the tween by `dt` seconds.
292    ///
293    /// Returns `true` while still running, `false` when complete.
294    /// Negative `dt` is treated as `0.0`.
295    fn update(&mut self, dt: f32) -> bool {
296        let dt = dt.max(0.0);
297
298        match self.state {
299            TweenState::Completed => return false,
300            TweenState::Paused => return true,
301            TweenState::Idle => {
302                // Drain delay bucket
303                self.delay_elapsed += dt;
304                if self.delay_elapsed < self.delay {
305                    return true;
306                }
307                // Carry the overflow into running time
308                let overflow = self.delay_elapsed - self.delay;
309                self.state = TweenState::Running;
310                self.elapsed += overflow * self.time_scale;
311            }
312            TweenState::Running => {
313                self.elapsed += dt * self.time_scale;
314            }
315        }
316
317        // Zero-duration tween completes immediately
318        if self.duration == 0.0 {
319            self.state = TweenState::Completed;
320            return false;
321        }
322
323        // Handle loop overflow
324        while self.elapsed >= self.duration {
325            match &self.looping {
326                Loop::Once => {
327                    self.elapsed = self.duration;
328                    self.state = TweenState::Completed;
329                    return false;
330                }
331                Loop::Times(n) => {
332                    self.loop_count += 1;
333                    if self.loop_count >= *n {
334                        self.elapsed = self.duration;
335                        self.state = TweenState::Completed;
336                        return false;
337                    }
338                    self.elapsed -= self.duration;
339                }
340                Loop::Forever => {
341                    self.elapsed -= self.duration;
342                }
343                Loop::PingPong => {
344                    self.elapsed -= self.duration;
345                    self.ping_pong_reverse = !self.ping_pong_reverse;
346                }
347                Loop::PingPongTimes(n) => {
348                    let passes = (*n).max(1);
349                    self.loop_count += 1;
350                    if self.loop_count >= passes {
351                        return self.complete_ping_pong_times(passes);
352                    }
353                    self.elapsed -= self.duration;
354                    self.ping_pong_reverse = !self.ping_pong_reverse;
355                }
356            }
357        }
358
359        true
360    }
361}
362
363impl<T: Animatable> Playable for Tween<T> {
364    fn duration(&self) -> f32 {
365        self.playback_duration()
366    }
367
368    fn reset(&mut self) {
369        Tween::reset(self);
370    }
371
372    fn seek_to(&mut self, progress: f32) {
373        let progress = progress.clamp(0.0, 1.0);
374        let total = self.playback_duration();
375        let finite_total = if total.is_finite() {
376            total
377        } else {
378            self.delay + self.duration
379        };
380
381        Tween::reset(self);
382
383        if finite_total == 0.0 {
384            self.elapsed = self.duration;
385            self.state = TweenState::Completed;
386            return;
387        }
388
389        let secs = finite_total * progress;
390        if secs < self.delay {
391            self.delay_elapsed = secs;
392            self.state = if self.delay > 0.0 {
393                TweenState::Idle
394            } else {
395                TweenState::Running
396            };
397            return;
398        }
399
400        let anim_secs = (secs - self.delay).max(0.0);
401        if self.duration == 0.0 {
402            self.elapsed = 0.0;
403            self.state = TweenState::Completed;
404            return;
405        }
406
407        match self.looping {
408            Loop::Once => {
409                self.elapsed = anim_secs.min(self.duration);
410                self.state = if progress >= 1.0 {
411                    TweenState::Completed
412                } else {
413                    TweenState::Running
414                };
415            }
416            Loop::Times(n) => {
417                let plays = n.max(1);
418                let total_anim = self.duration * plays as f32;
419                if anim_secs >= total_anim || progress >= 1.0 {
420                    self.loop_count = plays;
421                    self.elapsed = self.duration;
422                    self.state = TweenState::Completed;
423                } else {
424                    self.loop_count = (anim_secs / self.duration) as u32;
425                    self.elapsed = anim_secs - self.duration * self.loop_count as f32;
426                    self.state = TweenState::Running;
427                }
428            }
429            Loop::Forever => {
430                self.elapsed = anim_secs % self.duration;
431                self.state = TweenState::Running;
432            }
433            Loop::PingPong => {
434                let cycle = anim_secs % (self.duration * 2.0);
435                self.ping_pong_reverse = cycle >= self.duration;
436                self.elapsed = if self.ping_pong_reverse {
437                    cycle - self.duration
438                } else {
439                    cycle
440                };
441                self.state = TweenState::Running;
442            }
443            Loop::PingPongTimes(n) => {
444                let passes = n.max(1);
445                let total_anim = self.duration * passes as f32;
446                if anim_secs >= total_anim || progress >= 1.0 {
447                    self.complete_ping_pong_times(passes);
448                } else {
449                    self.loop_count = (anim_secs / self.duration) as u32;
450                    self.ping_pong_reverse = self.loop_count % 2 == 1;
451                    self.elapsed = anim_secs - self.duration * self.loop_count as f32;
452                    self.state = TweenState::Running;
453                }
454            }
455        }
456    }
457
458    fn is_complete(&self) -> bool {
459        Tween::is_complete(self)
460    }
461
462    fn as_any(&self) -> &dyn core::any::Any {
463        self
464    }
465
466    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
467        self
468    }
469}
470
471// ──────────────────────────────────────────────────────────────────────────────
472// Tests
473// ──────────────────────────────────────────────────────────────────────────────
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::loop_mode::Loop;
479    use animato_core::Easing;
480
481    fn make(start: f32, end: f32, duration: f32) -> Tween<f32> {
482        Tween::new(start, end).duration(duration).build()
483    }
484
485    #[test]
486    fn value_at_start_equals_start() {
487        let t = make(10.0, 90.0, 2.0);
488        assert_eq!(t.value(), 10.0);
489    }
490
491    #[test]
492    fn value_at_end_equals_end() {
493        let mut t = make(10.0, 90.0, 1.0);
494        t.update(1.0);
495        assert_eq!(t.value(), 90.0);
496    }
497
498    #[test]
499    fn is_complete_after_full_duration() {
500        let mut t = make(0.0, 1.0, 1.0);
501        t.update(1.0);
502        assert!(t.is_complete());
503    }
504
505    #[test]
506    fn large_dt_completes_cleanly() {
507        let mut t = make(0.0, 1.0, 1.0);
508        t.update(100.0);
509        assert!(t.is_complete());
510        assert_eq!(t.value(), 1.0);
511    }
512
513    #[test]
514    fn no_update_after_complete() {
515        let mut t = make(0.0, 1.0, 0.5);
516        t.update(1.0);
517        assert!(!t.update(1.0)); // still returns false, no panic
518    }
519
520    #[test]
521    fn delay_holds_at_start() {
522        let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
523        t.update(0.25); // still in delay
524        assert_eq!(t.value(), 0.0);
525        assert_eq!(t.state(), &TweenState::Idle);
526    }
527
528    #[test]
529    fn delay_transitions_to_running() {
530        let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
531        t.update(0.5); // exactly at delay end → now Running
532        assert_eq!(t.state(), &TweenState::Running);
533    }
534
535    #[test]
536    fn seek_jumps_to_midpoint() {
537        let mut t = make(0.0, 100.0, 1.0);
538        t.seek(0.5);
539        // Linear easing: midpoint = 50.0
540        let t2 = Tween::new(0.0_f32, 100.0)
541            .duration(1.0)
542            .easing(Easing::Linear)
543            .build();
544        let mut t2 = t2;
545        t2.seek(0.5);
546        assert!((t2.value() - 50.0).abs() < 0.01);
547    }
548
549    #[test]
550    fn reverse_swaps_direction() {
551        let mut t = Tween::new(0.0_f32, 100.0)
552            .duration(1.0)
553            .easing(Easing::Linear)
554            .build();
555        // Advance 40% forward
556        t.update(0.4);
557        let before = t.value(); // ~40.0
558        // Reverse: visual position preserved, now animating toward 0
559        t.reverse();
560        // Immediately after reverse, same visual position
561        assert!(
562            (t.value() - before).abs() < 1.0,
563            "visual position should be preserved: before={} after={}",
564            before,
565            t.value()
566        );
567        // One more step: value should decrease
568        t.update(0.1);
569        assert!(t.value() < before, "value should decrease after reverse");
570    }
571
572    #[test]
573    fn pause_stops_progress() {
574        let mut t = make(0.0, 1.0, 2.0);
575        t.update(0.5);
576        let v_before = t.value();
577        t.pause();
578        t.update(0.5); // should not advance
579        assert_eq!(t.value(), v_before);
580    }
581
582    #[test]
583    fn resume_continues_progress() {
584        let mut t = make(0.0, 1.0, 2.0);
585        t.update(0.5);
586        t.pause();
587        t.update(0.5); // no-op while paused
588        let v_paused = t.value();
589        t.resume();
590        t.update(0.5); // should advance
591        assert!(
592            t.value() > v_paused,
593            "resumed tween must advance past v_paused={}",
594            v_paused
595        );
596    }
597
598    #[test]
599    fn loop_times_completes_after_n() {
600        let mut t = Tween::new(0.0_f32, 1.0)
601            .duration(1.0)
602            .looping(Loop::Times(3))
603            .build();
604        // 3 × 1.0s + small epsilon to push past the 3rd boundary
605        t.update(3.0 + f32::EPSILON);
606        assert!(t.is_complete());
607    }
608
609    #[test]
610    fn loop_forever_never_completes() {
611        let mut t = Tween::new(0.0_f32, 1.0)
612            .duration(1.0)
613            .looping(Loop::Forever)
614            .build();
615        for _ in 0..1000 {
616            t.update(0.1);
617        }
618        assert!(!t.is_complete());
619    }
620
621    #[test]
622    fn pingpong_reverses_direction() {
623        let mut t = Tween::new(0.0_f32, 100.0)
624            .duration(1.0)
625            .easing(Easing::Linear)
626            .looping(Loop::PingPong)
627            .build();
628        // Forward pass → value should be 100 at t=1.0
629        t.update(1.0);
630        // Now in reverse: at halfway through backward pass value should be ~50
631        t.update(0.5);
632        let v = t.value();
633        assert!(v > 40.0 && v < 60.0, "pingpong mid-reverse = {}", v);
634    }
635
636    #[test]
637    fn ping_pong_times_even_passes_complete_at_start() {
638        let mut t = Tween::new(0.0_f32, 100.0)
639            .duration(1.0)
640            .easing(Easing::Linear)
641            .looping(Loop::PingPongTimes(2))
642            .build();
643
644        assert!(t.update(1.5));
645        assert_eq!(t.value(), 50.0);
646        assert!(!t.update(0.5));
647        assert!(t.is_complete());
648        assert_eq!(t.value(), 0.0);
649    }
650
651    #[test]
652    fn ping_pong_times_odd_passes_complete_at_end() {
653        let mut t = Tween::new(0.0_f32, 100.0)
654            .duration(1.0)
655            .easing(Easing::Linear)
656            .looping(Loop::PingPongTimes(3))
657            .build();
658
659        assert!(t.update(2.5));
660        assert_eq!(t.value(), 50.0);
661        assert!(!t.update(0.5));
662        assert!(t.is_complete());
663        assert_eq!(t.value(), 100.0);
664    }
665
666    #[test]
667    fn reset_returns_to_idle_with_delay() {
668        let mut t = Tween::new(0.0_f32, 1.0).duration(1.0).delay(0.5).build();
669        t.update(2.0); // complete
670        t.reset();
671        assert_eq!(t.state(), &TweenState::Idle);
672        assert_eq!(t.value(), 0.0);
673    }
674
675    #[test]
676    fn zero_duration_completes_immediately() {
677        let mut t = make(0.0, 100.0, 0.0);
678        t.update(0.0);
679        assert!(t.is_complete());
680        assert_eq!(t.value(), 100.0);
681    }
682
683    #[test]
684    fn negative_dt_is_noop() {
685        let mut t = make(0.0, 100.0, 1.0);
686        t.update(-5.0);
687        assert_eq!(t.value(), 0.0);
688    }
689
690    #[test]
691    fn accessors_snapshot_and_eased_progress_are_current() {
692        let mut t = Tween::new(0.0_f32, 100.0)
693            .duration(2.0)
694            .delay(0.5)
695            .easing(Easing::EaseInQuad)
696            .build();
697
698        t.update(0.25);
699        assert_eq!(t.progress(), 0.0);
700        assert_eq!(t.eased_progress(), 0.0);
701        assert_eq!(t.elapsed(), 0.0);
702        assert_eq!(t.delay_elapsed(), 0.25);
703        assert_eq!(t.loop_count(), 0);
704        assert!(!t.is_ping_pong_reversed());
705
706        let snapshot = t.snapshot();
707        assert_eq!(snapshot.delay_elapsed, 0.25);
708        assert_eq!(snapshot.state, TweenState::Idle);
709    }
710
711    #[test]
712    fn seek_and_reverse_restart_completed_tween() {
713        let mut t = make(0.0, 100.0, 1.0);
714
715        t.update(1.0);
716        assert!(t.is_complete());
717        t.seek(0.25);
718        assert_eq!(t.state(), &TweenState::Running);
719        assert_eq!(t.value(), 25.0);
720
721        t.update(1.0);
722        t.reverse();
723        assert_eq!(t.state(), &TweenState::Running);
724        assert_eq!(t.value(), 100.0);
725    }
726
727    #[test]
728    fn playables_seek_cover_delay_looping_and_downcast() {
729        let mut delayed = Tween::new(0.0_f32, 100.0).duration(1.0).delay(1.0).build();
730        Playable::seek_to(&mut delayed, 0.25);
731        assert_eq!(delayed.state(), &TweenState::Idle);
732        assert_eq!(delayed.delay_elapsed(), 0.5);
733
734        let mut times = Tween::new(0.0_f32, 10.0)
735            .duration(1.0)
736            .looping(Loop::Times(3))
737            .build();
738        Playable::seek_to(&mut times, 0.5);
739        assert_eq!(times.loop_count(), 1);
740        assert_eq!(times.state(), &TweenState::Running);
741        Playable::seek_to(&mut times, 1.0);
742        assert!(Playable::is_complete(&times));
743
744        let mut forever = Tween::new(0.0_f32, 10.0)
745            .duration(1.0)
746            .looping(Loop::Forever)
747            .build();
748        Playable::seek_to(&mut forever, 0.75);
749        assert_eq!(forever.value(), 7.5);
750
751        let mut ping_pong = Tween::new(0.0_f32, 10.0)
752            .duration(1.0)
753            .looping(Loop::PingPong)
754            .build();
755        Playable::seek_to(&mut ping_pong, 1.0);
756        assert!(ping_pong.is_ping_pong_reversed());
757        assert_eq!(ping_pong.value(), 10.0);
758
759        let mut ping_pong_times = Tween::new(0.0_f32, 10.0)
760            .duration(1.0)
761            .looping(Loop::PingPongTimes(2))
762            .build();
763        Playable::seek_to(&mut ping_pong_times, 1.0);
764        assert!(Playable::is_complete(&ping_pong_times));
765        assert_eq!(ping_pong_times.value(), 0.0);
766
767        assert_eq!(Playable::duration(&times), 3.0);
768        assert_eq!(Playable::duration(&ping_pong_times), 2.0);
769        assert!(Playable::as_any(&times).is::<Tween<f32>>());
770        assert!(Playable::as_any_mut(&mut times).is::<Tween<f32>>());
771        Playable::reset(&mut times);
772        assert_eq!(times.state(), &TweenState::Running);
773    }
774
775    #[test]
776    fn playable_seek_handles_zero_duration_and_delay_boundary() {
777        let mut zero = make(0.0, 1.0, 0.0);
778        Playable::seek_to(&mut zero, 0.5);
779        assert_eq!(zero.state(), &TweenState::Completed);
780        assert_eq!(zero.value(), 1.0);
781
782        let mut delayed = Tween::new(0.0_f32, 1.0).duration(1.0).delay(1.0).build();
783        Playable::seek_to(&mut delayed, 0.5);
784        assert_eq!(delayed.state(), &TweenState::Running);
785        assert_eq!(delayed.value(), 0.0);
786    }
787}