From d295b91c7ebfaaad77397fdcc79aa483928c91b8 Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Sun, 2 Oct 2022 18:49:13 +0100
Subject: [PATCH] Add elapsed API (#67)

Add two new methods to the `Tweenable<T>` trait:
- `set_elapsed(Duration)` sets the elapsed time of the tweenable. This
  is equivalent to `set_progress(duration().mul_f32(progress))`, with
  the added benefit of avoiding floating-point conversions and potential
  rounding errors resulting from it. `set_progress()` is now largely
  implemented in terms of and as a convenience wrapper to calling
  `set_elapsed()`.
- `elapsed()` which queries the elapsed time of the tweenable. This is
  equivalent to `duration().mul_f32(progress())`, again with better
  precision.

The elapsed API is the recommended way going forward to manipulate a
tweenable's time outside of the normal `tick()` flow. It supersedes the
progress API.

This change purposedly skips any discussion about what happens to
completion events when `set_progress()` or now `set_elpased()` is called
with a lower value than the current one to seek time back. This design
issue is logged as #60 and is still pending. The change also partially
addresses #31, but without removing any existing API or feature.
---
 CHANGELOG.md     |   1 +
 src/tweenable.rs | 272 ++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 212 insertions(+), 61 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54bb6c0..a81452e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added `RepeatCount` and `RepeatStrategy` for more granular control over animation looping.
 - Added `with_repeat_count()` and `with_repeat_strategy()` builder methods to `Tween<T>`.
 - Added a `speed()` getter on `Animator<T>` and `AssetAnimator<T>`.
+- Added `set_elapsed(Duration)` and `elapsed() -> Duration` to the `Tweenable<T>` trait. Those methods are preferable over `set_progress()` and `progress()` as they avoid the conversion to floating-point values and any rounding errors.
 
 ### Changed
 
diff --git a/src/tweenable.rs b/src/tweenable.rs
index d5ead02..ad1e02d 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -28,6 +28,8 @@ use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection};
 /// # struct MyTweenable;
 /// # impl Tweenable<Transform> for MyTweenable {
 /// #     fn duration(&self) -> Duration  { unimplemented!() }
+/// #     fn set_elapsed(&mut self, elapsed: Duration)  { unimplemented!() }
+/// #     fn elapsed(&self) -> Duration  { unimplemented!() }
 /// #     fn set_progress(&mut self, progress: f32)  { unimplemented!() }
 /// #     fn progress(&self) -> f32  { unimplemented!() }
 /// #     fn tick(&mut self, delta: Duration, target: &mut Transform, entity: Entity, event_writer: &mut EventWriter<TweenCompleted>) -> TweenState  { unimplemented!() }
@@ -96,11 +98,17 @@ pub struct TweenCompleted {
     pub user_data: u64,
 }
 
+/// Calculate the progress fraction in \[0:1\] of the ratio between two
+/// [`Duration`]s.
+fn fraction_progress(n: Duration, d: Duration) -> f32 {
+    // TODO - Replace with div_duration_f32() once it's stable
+    (n.as_secs_f64() / d.as_secs_f64()).fract() as f32
+}
+
 #[derive(Debug)]
 struct AnimClock {
     elapsed: Duration,
     duration: Duration,
-    times_completed: u32,
     total_duration: TotalDuration,
     strategy: RepeatStrategy,
 }
@@ -111,34 +119,47 @@ impl AnimClock {
             elapsed: Duration::ZERO,
             duration,
             total_duration: compute_total_duration(duration, RepeatCount::default()),
-            times_completed: 0,
             strategy: RepeatStrategy::default(),
         }
     }
 
-    fn tick(&mut self, tick: Duration) -> (TweenState, u32) {
-        let duration = self.duration.as_nanos();
-        let prev_times_completed = self.elapsed.as_nanos() / duration;
+    fn tick(&mut self, tick: Duration) -> (TweenState, i32) {
+        self.set_elapsed(self.elapsed.saturating_add(tick))
+    }
 
-        self.elapsed = self.elapsed.saturating_add(tick);
-        let state = if let TotalDuration::Finite(duration) = self.total_duration {
-            if self.elapsed >= duration {
-                self.elapsed = duration;
-                TweenState::Completed
-            } else {
-                TweenState::Active
+    fn times_completed(&self) -> u32 {
+        (self.elapsed.as_nanos() / self.duration.as_nanos()) as u32
+    }
+
+    fn set_elapsed(&mut self, elapsed: Duration) -> (TweenState, i32) {
+        let old_times_completed = self.times_completed();
+
+        self.elapsed = elapsed;
+
+        let state = match self.total_duration {
+            TotalDuration::Finite(total_duration) => {
+                if self.elapsed >= total_duration {
+                    self.elapsed = total_duration;
+                    TweenState::Completed
+                } else {
+                    TweenState::Active
+                }
             }
-        } else {
-            TweenState::Active
+            TotalDuration::Infinite => TweenState::Active,
         };
 
-        let times_completed = (self.elapsed.as_nanos() / duration - prev_times_completed) as u32;
-        self.times_completed = self.times_completed.saturating_add(times_completed);
-        (state, times_completed)
+        (
+            state,
+            self.times_completed() as i32 - old_times_completed as i32,
+        )
     }
 
-    fn set_progress(&mut self, progress: f32) {
-        self.elapsed = self.duration.mul_f32(progress.max(0.));
+    fn elapsed(&self) -> Duration {
+        self.elapsed
+    }
+
+    fn set_progress(&mut self, progress: f32) -> (TweenState, i32) {
+        self.set_elapsed(self.duration.mul_f32(progress.max(0.)))
     }
 
     fn progress(&self) -> f32 {
@@ -147,13 +168,13 @@ impl AnimClock {
                 return 1.;
             }
         }
-        (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).fract()
+        fraction_progress(self.elapsed, self.duration)
     }
 
     fn state(&self) -> TweenState {
         match self.total_duration {
-            TotalDuration::Finite(duration) => {
-                if self.elapsed >= duration {
+            TotalDuration::Finite(total_duration) => {
+                if self.elapsed >= total_duration {
                     TweenState::Completed
                 } else {
                     TweenState::Active
@@ -164,7 +185,6 @@ impl AnimClock {
     }
 
     fn reset(&mut self) {
-        self.times_completed = 0;
         self.elapsed = Duration::ZERO;
     }
 }
@@ -195,10 +215,40 @@ pub trait Tweenable<T>: Send + Sync {
     /// same state in this case is the double of the returned value.
     fn duration(&self) -> Duration;
 
+    /// Set the current animation playback elapsed time.
+    ///
+    /// See [`elapsed()`] for details on the meaning. If `elapsed` is greater
+    /// than or equal to [`duration()`], then the animation completes.
+    ///
+    /// Setting the elapsed time seeks the animation to a new position, but does
+    /// not apply that change to the underlying component being animated. To
+    /// force the change to apply, call [`tick()`] with a `delta` of
+    /// `Duration::ZERO`.
+    ///
+    /// [`elapsed()`]: Tweenable::elapsed
+    /// [`duration()`]: Tweenable::duration
+    /// [`tick()`]: Tweenable::tick
+    fn set_elapsed(&mut self, elapsed: Duration);
+
+    /// Get the current elapsed duration.
+    ///
+    /// While looping, the exact value returned by [`duration()`] is never
+    /// reached, since the tweenable loops over to zero immediately when it
+    /// changes direction at either endpoint. Upon completion, the tweenable
+    /// always reports the same value as [`duration()`].
+    ///
+    /// [`duration()`]: Tweenable::duration
+    fn elapsed(&self) -> Duration;
+
     /// Set the current animation playback progress.
     ///
     /// See [`progress()`] for details on the meaning.
     ///
+    /// Setting the progress seeks the animation to a new position, but does not
+    /// apply that change to the underlying component being animated. To
+    /// force the change to apply, call [`tick()`] with a `delta` of
+    /// `Duration::ZERO`.
+    ///
     /// [`progress()`]: Tweenable::progress
     fn set_progress(&mut self, progress: f32);
 
@@ -484,6 +534,14 @@ impl<T> Tweenable<T> for Tween<T> {
         self.clock.duration
     }
 
+    fn set_elapsed(&mut self, elapsed: Duration) {
+        self.clock.set_elapsed(elapsed);
+    }
+
+    fn elapsed(&self) -> Duration {
+        self.clock.elapsed()
+    }
+
     fn set_progress(&mut self, progress: f32) {
         self.clock.set_progress(progress);
     }
@@ -541,7 +599,7 @@ impl<T> Tweenable<T> for Tween<T> {
     }
 
     fn times_completed(&self) -> u32 {
-        self.clock.times_completed
+        self.clock.times_completed()
     }
 
     fn rewind(&mut self) {
@@ -550,7 +608,7 @@ impl<T> Tweenable<T> for Tween<T> {
             // direction on Tween creation, we count the number of completions, ignoring the
             // last one if the Tween is currently in TweenState::Completed because that one
             // freezes all parameters.
-            let mut times_completed = self.clock.times_completed;
+            let mut times_completed = self.clock.times_completed();
             if self.clock.state() == TweenState::Completed {
                 debug_assert!(times_completed > 0);
                 times_completed -= 1;
@@ -568,7 +626,7 @@ pub struct Sequence<T> {
     tweens: Vec<BoxedTweenable<T>>,
     index: usize,
     duration: Duration,
-    time: Duration,
+    elapsed: Duration,
     times_completed: u32,
 }
 
@@ -589,7 +647,7 @@ impl<T> Sequence<T> {
             tweens,
             index: 0,
             duration,
-            time: Duration::ZERO,
+            elapsed: Duration::ZERO,
             times_completed: 0,
         }
     }
@@ -603,7 +661,7 @@ impl<T> Sequence<T> {
             tweens: vec![boxed],
             index: 0,
             duration,
-            time: Duration::ZERO,
+            elapsed: Duration::ZERO,
             times_completed: 0,
         }
     }
@@ -615,7 +673,7 @@ impl<T> Sequence<T> {
             tweens: Vec::with_capacity(capacity),
             index: 0,
             duration: Duration::ZERO,
-            time: Duration::ZERO,
+            elapsed: Duration::ZERO,
             times_completed: 0,
         }
     }
@@ -646,26 +704,24 @@ impl<T> Tweenable<T> for Sequence<T> {
         self.duration
     }
 
-    fn set_progress(&mut self, progress: f32) {
-        self.times_completed = if progress >= 1. { 1 } else { 0 };
-        let progress = progress.clamp(0., 1.); // not looping
-                                               // Set the total sequence progress
-        let total_elapsed_secs = self.duration().as_secs_f64() * progress as f64;
-        self.time = Duration::from_secs_f64(total_elapsed_secs);
+    fn set_elapsed(&mut self, elapsed: Duration) {
+        // Set the total sequence progress
+        self.elapsed = elapsed;
+        self.times_completed = if elapsed >= self.duration { 1 } else { 0 };
 
         // Find which tween is active in the sequence
-        let mut accum_duration = 0.;
+        let mut accum_duration = Duration::ZERO;
         for index in 0..self.tweens.len() {
             let tween = &mut self.tweens[index];
-            let tween_duration = tween.duration().as_secs_f64();
-            if total_elapsed_secs < accum_duration + tween_duration {
+            let tween_duration = tween.duration();
+            if elapsed < accum_duration + tween_duration {
                 self.index = index;
-                let local_duration = total_elapsed_secs - accum_duration;
-                tween.set_progress((local_duration / tween_duration) as f32);
+                let local_duration = elapsed - accum_duration;
+                tween.set_elapsed(local_duration);
                 // TODO?? set progress of other tweens after that one to 0. ??
                 return;
             }
-            tween.set_progress(1.); // ?? to prepare for next loop/rewind?
+            tween.set_elapsed(tween.duration()); // ?? to prepare for next loop/rewind?
             accum_duration += tween_duration;
         }
 
@@ -673,8 +729,20 @@ impl<T> Tweenable<T> for Sequence<T> {
         self.index = self.tweens.len();
     }
 
+    fn elapsed(&self) -> Duration {
+        self.elapsed
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        self.set_elapsed(self.duration.mul_f32(progress.max(0.)))
+    }
+
     fn progress(&self) -> f32 {
-        self.time.as_secs_f32() / self.duration.as_secs_f32()
+        if self.elapsed >= self.duration {
+            1.
+        } else {
+            fraction_progress(self.elapsed, self.duration)
+        }
     }
 
     fn tick(
@@ -684,7 +752,7 @@ impl<T> Tweenable<T> for Sequence<T> {
         entity: Entity,
         event_writer: &mut EventWriter<TweenCompleted>,
     ) -> TweenState {
-        self.time = (self.time + delta).min(self.duration);
+        self.elapsed = self.elapsed.saturating_add(delta).min(self.duration);
         while self.index < self.tweens.len() {
             let tween = &mut self.tweens[self.index];
             let tween_remaining = tween.duration().mul_f32(1.0 - tween.progress());
@@ -706,7 +774,7 @@ impl<T> Tweenable<T> for Sequence<T> {
     }
 
     fn rewind(&mut self) {
-        self.time = Duration::ZERO;
+        self.elapsed = Duration::ZERO;
         self.index = 0;
         self.times_completed = 0;
         for tween in &mut self.tweens {
@@ -720,7 +788,7 @@ impl<T> Tweenable<T> for Sequence<T> {
 pub struct Tracks<T> {
     tracks: Vec<BoxedTweenable<T>>,
     duration: Duration,
-    time: Duration,
+    elapsed: Duration,
     times_completed: u32,
 }
 
@@ -739,7 +807,7 @@ impl<T> Tracks<T> {
         Self {
             tracks,
             duration,
-            time: Duration::ZERO,
+            elapsed: Duration::ZERO,
             times_completed: 0,
         }
     }
@@ -750,19 +818,29 @@ impl<T> Tweenable<T> for Tracks<T> {
         self.duration
     }
 
-    fn set_progress(&mut self, progress: f32) {
-        self.times_completed = if progress >= 1. { 1 } else { 0 }; // not looping
-        let progress = progress.clamp(0., 1.); // not looping
-        let time_secs = self.duration.as_secs_f64() * progress as f64;
-        self.time = Duration::from_secs_f64(time_secs);
+    fn set_elapsed(&mut self, elapsed: Duration) {
+        self.elapsed = elapsed;
+        self.times_completed = if elapsed >= self.duration { 1 } else { 0 }; // not looping
+
         for tweenable in &mut self.tracks {
-            let progress = time_secs / tweenable.duration().as_secs_f64();
-            tweenable.set_progress(progress as f32);
+            tweenable.set_elapsed(elapsed);
         }
     }
 
+    fn elapsed(&self) -> Duration {
+        self.elapsed
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        self.set_elapsed(self.duration.mul_f32(progress.max(0.)))
+    }
+
     fn progress(&self) -> f32 {
-        self.time.as_secs_f32() / self.duration.as_secs_f32()
+        if self.elapsed >= self.duration {
+            1.
+        } else {
+            fraction_progress(self.elapsed, self.duration)
+        }
     }
 
     fn tick(
@@ -772,7 +850,7 @@ impl<T> Tweenable<T> for Tracks<T> {
         entity: Entity,
         event_writer: &mut EventWriter<TweenCompleted>,
     ) -> TweenState {
-        self.time = (self.time + delta).min(self.duration);
+        self.elapsed = self.elapsed.saturating_add(delta).min(self.duration);
         let mut any_active = false;
         for tweenable in &mut self.tracks {
             let state = tweenable.tick(delta, target, entity, event_writer);
@@ -791,7 +869,7 @@ impl<T> Tweenable<T> for Tracks<T> {
     }
 
     fn rewind(&mut self) {
-        self.time = Duration::ZERO;
+        self.elapsed = Duration::ZERO;
         self.times_completed = 0;
         for tween in &mut self.tracks {
             tween.rewind();
@@ -836,16 +914,22 @@ impl<T> Tweenable<T> for Delay {
         self.timer.duration()
     }
 
-    fn set_progress(&mut self, progress: f32) {
+    fn set_elapsed(&mut self, elapsed: Duration) {
         // need to reset() to clear finished() unfortunately
         self.timer.reset();
-        self.timer.set_elapsed(Duration::from_secs_f64(
-            self.timer.duration().as_secs_f64() * progress as f64,
-        ));
+        self.timer.set_elapsed(elapsed);
         // set_elapsed() does not update finished() etc. which we rely on
         self.timer.tick(Duration::ZERO);
     }
 
+    fn elapsed(&self) -> Duration {
+        self.timer.elapsed()
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        <Delay as Tweenable<T>>::set_elapsed(self, self.timer.duration().mul_f32(progress.max(0.)));
+    }
+
     fn progress(&self) -> f32 {
         self.timer.percent()
     }
@@ -943,7 +1027,7 @@ mod tests {
         }
 
         assert_eq!(
-            (total_duration.as_secs_f64() / duration.as_secs_f64()) as u32,
+            (total_duration.as_secs_f64() / duration.as_secs_f64()) as i32,
             times_completed
         );
     }
@@ -1233,6 +1317,29 @@ mod tests {
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.6), 1e-5));
     }
 
+    #[test]
+    fn tween_elapsed() {
+        let mut tween = make_test_tween();
+
+        let duration = tween.duration();
+        let elapsed = tween.elapsed();
+
+        assert_eq!(elapsed, Duration::ZERO);
+        assert_eq!(duration, Duration::from_secs(1));
+
+        for ms in [0, 1, 500, 100, 300, 999, 847, 1000, 900] {
+            let elapsed = Duration::from_millis(ms);
+            tween.set_elapsed(elapsed);
+            assert_eq!(tween.elapsed(), elapsed);
+
+            let progress = (elapsed.as_secs_f64() / duration.as_secs_f64()) as f32;
+            assert_approx_eq!(tween.progress(), progress);
+
+            let times_completed = if ms == 1000 { 1 } else { 0 };
+            assert_eq!(tween.times_completed(), times_completed);
+        }
+    }
+
     /// Test ticking a sequence of tweens.
     #[test]
     fn seq_tick() {
@@ -1369,6 +1476,31 @@ mod tests {
         assert_eq!(seq.duration(), Duration::from_secs(1));
     }
 
+    #[test]
+    fn seq_elapsed() {
+        let mut seq = Sequence::new((1..5).map(|i| {
+            Tween::new(
+                EaseMethod::Linear,
+                Duration::from_millis(200 * i),
+                TransformPositionLens {
+                    start: Vec3::ZERO,
+                    end: Vec3::ONE,
+                },
+            )
+        }));
+
+        let mut elapsed = Duration::ZERO;
+        for i in 1..5 {
+            assert_eq!(seq.index(), i - 1);
+            assert_eq!(seq.elapsed(), elapsed);
+            let duration = Duration::from_millis(200 * i as u64);
+            assert_eq!(seq.current().duration(), duration);
+            elapsed += duration;
+            seq.set_elapsed(elapsed);
+            assert_eq!(seq.times_completed(), if i == 4 { 1 } else { 0 });
+        }
+    }
+
     /// Test ticking parallel tracks of tweens.
     #[test]
     fn tracks_tick() {
@@ -1532,6 +1664,24 @@ mod tests {
         assert_approx_eq!(tweenable.progress(), 1.);
     }
 
+    #[test]
+    fn delay_elapsed() {
+        let mut delay = Delay::new(Duration::from_secs(1));
+        let tweenable: &mut dyn Tweenable<Transform> = &mut delay;
+        let duration = tweenable.duration();
+        for ms in [0, 1, 500, 100, 300, 999, 847, 1000, 900] {
+            let elapsed = Duration::from_millis(ms);
+            tweenable.set_elapsed(elapsed);
+            assert_eq!(tweenable.elapsed(), elapsed);
+
+            let progress = (elapsed.as_secs_f64() / duration.as_secs_f64()) as f32;
+            assert_approx_eq!(tweenable.progress(), progress);
+
+            let times_completed = if ms == 1000 { 1 } else { 0 };
+            assert_eq!(tweenable.times_completed(), times_completed);
+        }
+    }
+
     #[test]
     #[should_panic]
     fn delay_zero_duration_panics() {
-- 
GitLab