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