From 38e99394749d834f5afd7e5bf76b4692b7d44ce1 Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Sun, 13 Feb 2022 21:49:30 +0000
Subject: [PATCH] Clarify and increase control on playback state

Add some new methods and refactor some existing ones to clarify the
playback state of a tweenable, and give increased control to the
`Animator` or `AssetAnimator` to rewind a tweenable, set its progress to
an arbitrary value, or query its current state.
---
 CHANGELOG        |  18 ++
 Cargo.toml       |   6 +-
 src/lib.rs       | 159 ++++++++++++++---
 src/plugin.rs    |  24 +--
 src/tweenable.rs | 447 +++++++++++++++++++++++++++++++++--------------
 5 files changed, 474 insertions(+), 180 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index ba036aa..ddf2c3c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,24 @@
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added
+
+- Implemented `Default` for `TweeningType` (= `Once`), `EaseMethod` (= `Linear`), `TweeningDirection` (= `Forward`).
+- Added `Tweenable::is_looping()`, `Tweenable::set_progress()`, `Tweenable::times_completed()`, and `Tweenable::rewind()`.
+- Added `Animator::set_progress()`, `Animator::progress()`, `Animator::stop()`, and `Animator::rewind()`.
+- Added `AssetAnimator::set_progress()`, `AssetAnimator::progress()`, `AssetAnimator::stop()`, and `AssetAnimator::rewind()`.
+
+### Changed
+
+- `TweenState` now contains only two states: `Active` and `Completed`. Looping animations are always active, and non-looping ones are completed once they reach their end point.
+- Merged the `started` and `ended` callbacks into a `completed` one (`Tween::set_completed()` and `Tween::clear_completed()`), which is invoked when the tween completes a single iteration. That is, for non-looping animations, when `TweenState::Completed` is reached. And for looping animations, once per iteration (going from start -> end, or from end -> start).
+
+### Removed
+
+- Removed `Tweenable::stop()`. Tweenables do not have a "stop" state anymore, they are only either active or completed. The playback state is only relevant on the `Animator` or `AssetAnimator` which controls them.
+
 ## [0.3.1] - 2022-02-12
 
 ### Added
diff --git a/Cargo.toml b/Cargo.toml
index 27ddf83..c95f946 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,11 +14,7 @@ exclude = ["examples/*.gif", ".github"]
 
 [dependencies]
 interpolation = "0.2"
-
-[dependencies.bevy]
-version = "0.6"
-default-features = false
-features = [ "render" ]
+bevy = { version = "0.6", default-features = false, features = [ "render" ] }
 
 [dev-dependencies]
 bevy-inspector-egui = "0.8"
diff --git a/src/lib.rs b/src/lib.rs
index 2b56d56..5c26b30 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -165,7 +165,7 @@ mod tweenable;
 
 pub use lens::Lens;
 pub use plugin::{asset_animator_system, component_animator_system, TweeningPlugin};
-pub use tweenable::{Delay, Sequence, Tracks, Tween, Tweenable};
+pub use tweenable::{Delay, Sequence, Tracks, Tween, TweenState, Tweenable};
 
 /// Type of looping for a tween animation.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -178,12 +178,18 @@ pub enum TweeningType {
     PingPong,
 }
 
+impl Default for TweeningType {
+    fn default() -> Self {
+        TweeningType::Once
+    }
+}
+
 /// Playback state of an animator.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum AnimatorState {
     /// The animation is playing. This is the default state.
     Playing,
-    /// The animation is paused/stopped.
+    /// The animation is paused in its current state.
     Paused,
 }
 
@@ -235,6 +241,12 @@ impl EaseMethod {
     }
 }
 
+impl Default for EaseMethod {
+    fn default() -> Self {
+        EaseMethod::Linear
+    }
+}
+
 impl Into<EaseMethod> for EaseFunction {
     fn into(self) -> EaseMethod {
         EaseMethod::EaseFunction(self)
@@ -254,6 +266,12 @@ pub enum TweeningDirection {
     Backward,
 }
 
+impl Default for TweeningDirection {
+    fn default() -> Self {
+        TweeningDirection::Forward
+    }
+}
+
 impl std::ops::Not for TweeningDirection {
     type Output = TweeningDirection;
 
@@ -265,25 +283,11 @@ impl std::ops::Not for TweeningDirection {
     }
 }
 
-/// Playback state of a [`Tweenable`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TweenState {
-    /// Not animated. If controlled by an [`Animator`] or [`AssetAnimator`], that animator is paused.
-    Stopped,
-    /// Actively animating. The tweenable did not reach its end state yet.
-    Running,
-    /// Animation ended but [`Tweenable::stop()`] was not called. The tweenable is idling at its latest
-    /// time. This can only happen for [`TweeningType::Once`], since other types loop indefinitely
-    /// until they're stopped.
-    Ended,
-}
-
 /// Component to control the animation of another component.
 #[derive(Component)]
 pub struct Animator<T: Component> {
     /// Control if this animation is played or not.
     pub state: AnimatorState,
-    prev_state: AnimatorState,
     tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
 }
 
@@ -299,14 +303,13 @@ impl<T: Component> Default for Animator<T> {
     fn default() -> Self {
         Animator {
             state: Default::default(),
-            prev_state: Default::default(),
             tweenable: None,
         }
     }
 }
 
 impl<T: Component> Animator<T> {
-    /// Create a new animator component from a single [`Tween`] or [`Sequence`].
+    /// Create a new animator component from a single tweenable.
     pub fn new(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
         Animator {
             tweenable: Some(Box::new(tween)),
@@ -314,10 +317,9 @@ impl<T: Component> Animator<T> {
         }
     }
 
-    /// Set the initial state of the animator.
+    /// Set the initial playback state of the animator.
     pub fn with_state(mut self, state: AnimatorState) -> Self {
         self.state = state;
-        self.prev_state = state;
         self
     }
 
@@ -326,7 +328,7 @@ impl<T: Component> Animator<T> {
         self.tweenable = Some(Box::new(tween));
     }
 
-    /// Get the collection of sequences forming the parallel tracks of animation.
+    /// Get the top-level tweenable this animator is currently controlling.
     pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
         if let Some(tweenable) = &self.tweenable {
             Some(tweenable.as_ref())
@@ -335,7 +337,7 @@ impl<T: Component> Animator<T> {
         }
     }
 
-    /// Get the mutable collection of sequences forming the parallel tracks of animation.
+    /// Get the top-level mutable tweenable this animator is currently controlling.
     pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
         if let Some(tweenable) = &mut self.tweenable {
             Some(tweenable.as_mut())
@@ -343,6 +345,57 @@ impl<T: Component> Animator<T> {
             None
         }
     }
+
+    /// Set the current animation playback progress.
+    ///
+    /// See [`progress()`] for details on the meaning.
+    ///
+    /// [`progress()`]: Animator::progress
+    pub fn set_progress(&mut self, progress: f32) {
+        if let Some(tweenable) = &mut self.tweenable {
+            tweenable.set_progress(progress)
+        }
+    }
+
+    /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) of the animation.
+    ///
+    /// For looping animations, this reports the progress of the current iteration, in the current direction:
+    /// - [`TweeningType::Loop`] is 0 at start and 1 at end. The exact value 1.0 is never reached,
+    ///   since the tweenable loops over to 0.0 immediately.
+    /// - [`TweeningType::PingPong`] is 0 at the source endpoint and 1 and the destination one,
+    ///   which are respectively the start/end for [`TweeningDirection::Forward`], or the end/start
+    ///   for [`TweeningDirection::Backward`]. The exact value 1.0 is never reached, since the tweenable
+    ///   loops over to 0.0 immediately when it changes direction at either endpoint.
+    ///
+    /// For sequences, the progress is measured over the entire sequence, from 0 at the start of the first
+    /// child tweenable to 1 at the end of the last one.
+    ///
+    /// For tracks (parallel execution), the progress is measured like a sequence over the longest "path" of
+    /// child tweenables. In other words, this is the current elapsed time over the total tweenable duration.
+    pub fn progress(&self) -> f32 {
+        if let Some(tweenable) = &self.tweenable {
+            tweenable.progress()
+        } else {
+            0.
+        }
+    }
+
+    /// Stop animation playback and rewind the animation.
+    ///
+    /// This changes the animator state to [`AnimatorState::Paused`] and rewind its tweenable.
+    pub fn stop(&mut self) {
+        self.state = AnimatorState::Paused;
+        self.rewind();
+    }
+
+    /// Rewind animation playback to its initial state.
+    ///
+    /// This does not change the playback state (playing/paused).
+    pub fn rewind(&mut self) {
+        if let Some(tweenable) = &mut self.tweenable {
+            tweenable.rewind();
+        }
+    }
 }
 
 /// Component to control the animation of an asset.
@@ -350,7 +403,6 @@ impl<T: Component> Animator<T> {
 pub struct AssetAnimator<T: Asset> {
     /// Control if this animation is played or not.
     pub state: AnimatorState,
-    prev_state: AnimatorState,
     tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
     handle: Handle<T>,
 }
@@ -367,7 +419,6 @@ impl<T: Asset> Default for AssetAnimator<T> {
     fn default() -> Self {
         AssetAnimator {
             state: Default::default(),
-            prev_state: Default::default(),
             tweenable: None,
             handle: Default::default(),
         }
@@ -375,7 +426,7 @@ impl<T: Asset> Default for AssetAnimator<T> {
 }
 
 impl<T: Asset> AssetAnimator<T> {
-    /// Create a new animator component from a single [`Tween`] or [`Sequence`].
+    /// Create a new asset animator component from a single tweenable.
     pub fn new(handle: Handle<T>, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
         AssetAnimator {
             tweenable: Some(Box::new(tween)),
@@ -384,10 +435,9 @@ impl<T: Asset> AssetAnimator<T> {
         }
     }
 
-    /// Set the initial state of the animator.
+    /// Set the initial playback state of the animator.
     pub fn with_state(mut self, state: AnimatorState) -> Self {
         self.state = state;
-        self.prev_state = state;
         self
     }
 
@@ -396,7 +446,7 @@ impl<T: Asset> AssetAnimator<T> {
         self.tweenable = Some(Box::new(tween));
     }
 
-    /// Get the collection of sequences forming the parallel tracks of animation.
+    /// Get the top-level tweenable this animator is currently controlling.
     pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
         if let Some(tweenable) = &self.tweenable {
             Some(tweenable.as_ref())
@@ -405,7 +455,7 @@ impl<T: Asset> AssetAnimator<T> {
         }
     }
 
-    /// Get the mutable collection of sequences forming the parallel tracks of animation.
+    /// Get the top-level mutable tweenable this animator is currently controlling.
     pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
         if let Some(tweenable) = &mut self.tweenable {
             Some(tweenable.as_mut())
@@ -414,6 +464,57 @@ impl<T: Asset> AssetAnimator<T> {
         }
     }
 
+    /// Set the current animation playback progress.
+    ///
+    /// See [`progress()`] for details on the meaning.
+    ///
+    /// [`progress()`]: Animator::progress
+    pub fn set_progress(&mut self, progress: f32) {
+        if let Some(tweenable) = &mut self.tweenable {
+            tweenable.set_progress(progress)
+        }
+    }
+
+    /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) of the animation.
+    ///
+    /// For looping animations, this reports the progress of the current iteration, in the current direction:
+    /// - [`TweeningType::Loop`] is 0 at start and 1 at end. The exact value 1.0 is never reached,
+    ///   since the tweenable loops over to 0.0 immediately.
+    /// - [`TweeningType::PingPong`] is 0 at the source endpoint and 1 and the destination one,
+    ///   which are respectively the start/end for [`TweeningDirection::Forward`], or the end/start
+    ///   for [`TweeningDirection::Backward`]. The exact value 1.0 is never reached, since the tweenable
+    ///   loops over to 0.0 immediately when it changes direction at either endpoint.
+    ///
+    /// For sequences, the progress is measured over the entire sequence, from 0 at the start of the first
+    /// child tweenable to 1 at the end of the last one.
+    ///
+    /// For tracks (parallel execution), the progress is measured like a sequence over the longest "path" of
+    /// child tweenables. In other words, this is the current elapsed time over the total tweenable duration.
+    pub fn progress(&self) -> f32 {
+        if let Some(tweenable) = &self.tweenable {
+            tweenable.progress()
+        } else {
+            0.
+        }
+    }
+
+    /// Stop animation playback and rewind the animation.
+    ///
+    /// This changes the animator state to [`AnimatorState::Paused`] and rewind its tweenable.
+    pub fn stop(&mut self) {
+        self.state = AnimatorState::Paused;
+        self.rewind();
+    }
+
+    /// Rewind animation playback to its initial state.
+    ///
+    /// This does not change the playback state (playing/paused).
+    pub fn rewind(&mut self) {
+        if let Some(tweenable) = &mut self.tweenable {
+            tweenable.rewind();
+        }
+    }
+
     fn handle(&self) -> Handle<T> {
         self.handle.clone()
     }
diff --git a/src/plugin.rs b/src/plugin.rs
index f344d7c..84d4323 100644
--- a/src/plugin.rs
+++ b/src/plugin.rs
@@ -50,16 +50,10 @@ pub fn component_animator_system<T: Component>(
     mut query: Query<(&mut T, &mut Animator<T>)>,
 ) {
     for (ref mut target, ref mut animator) in query.iter_mut() {
-        let state_changed = animator.state != animator.prev_state;
-        animator.prev_state = animator.state;
-        if animator.state == AnimatorState::Paused {
-            if state_changed {
-                if let Some(tweenable) = animator.tweenable_mut() {
-                    tweenable.stop();
-                }
+        if animator.state != AnimatorState::Paused {
+            if let Some(tweenable) = animator.tweenable_mut() {
+                tweenable.tick(time.delta(), target);
             }
-        } else if let Some(tweenable) = animator.tweenable_mut() {
-            tweenable.tick(time.delta(), target);
         }
     }
 }
@@ -73,18 +67,12 @@ pub fn asset_animator_system<T: Asset>(
     mut query: Query<&mut AssetAnimator<T>>,
 ) {
     for ref mut animator in query.iter_mut() {
-        let state_changed = animator.state != animator.prev_state;
-        animator.prev_state = animator.state;
-        if animator.state == AnimatorState::Paused {
-            if state_changed {
+        if animator.state != AnimatorState::Paused {
+            if let Some(target) = assets.get_mut(animator.handle()) {
                 if let Some(tweenable) = animator.tweenable_mut() {
-                    tweenable.stop();
+                    tweenable.tick(time.delta(), target);
                 }
             }
-        } else if let Some(target) = assets.get_mut(animator.handle()) {
-            if let Some(tweenable) = animator.tweenable_mut() {
-                tweenable.tick(time.delta(), target);
-            }
         }
     }
 }
diff --git a/src/tweenable.rs b/src/tweenable.rs
index a2e9cad..13309db 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -1,7 +1,20 @@
 use bevy::prelude::*;
 use std::time::Duration;
 
-use crate::{EaseMethod, Lens, TweenState, TweeningDirection, TweeningType};
+use crate::{EaseMethod, Lens, TweeningDirection, TweeningType};
+
+/// Playback state of a [`Tweenable`].
+///
+/// This is returned by [`Tweenable::tick()`] to allow the caller to execute some logic based on the
+/// updated state of the tweenable, like advanding a sequence to its next child tweenable.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TweenState {
+    /// The tweenable is still active, and did not reach its end state yet.
+    Active,
+    /// Animation reached its end state. The tweenable is idling at its latest time. This can only happen
+    /// for [`TweeningType::Once`], since other types loop indefinitely.
+    Completed,
+}
 
 /// An animatable entity, either a single [`Tween`] or a collection of them.
 pub trait Tweenable<T>: Send + Sync {
@@ -12,6 +25,16 @@ pub trait Tweenable<T>: Send + Sync {
     /// case is the double of the returned value.
     fn duration(&self) -> Duration;
 
+    /// Return `true` if the animation is looping.
+    fn is_looping(&self) -> bool;
+
+    /// Set the current animation playback progress.
+    ///
+    /// See [`progress()`] for details on the meaning.
+    ///
+    /// [`progress()`]: Tweenable::progress
+    fn set_progress(&mut self, progress: f32);
+
     /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) of the animation.
     ///
     /// For looping animations, this reports the progress of the current iteration,
@@ -24,29 +47,52 @@ pub trait Tweenable<T>: Send + Sync {
     ///   loops over to 0.0 immediately when it changes direction at either endpoint.
     fn progress(&self) -> f32;
 
-    /// Tick the animation, advancing it by the given delta time and mutating the
-    /// given target component or asset
+    /// Tick the animation, advancing it by the given delta time and mutating the given target component or asset.
+    ///
+    /// This returns [`TweenState::Active`] if the tweenable didn't reach its final state yet (progress < 1.),
+    /// or [`TweenState::Completed`] if the tweenable completed this tick. Only non-looping tweenables return
+    /// a completed state, since looping ones continue forever.
+    ///
+    /// Calling this method with a duration of [`Duration::ZERO`] is valid, and updates the target to the current
+    /// state of the tweenable without actually modifying the tweenable state. This is useful after certain operations
+    /// like [`rewind()`] or [`set_progress()`] whose effect is otherwise only visible on target on next frame.
     ///
-    /// This changes the tweenable state to [`TweenState::Running`] before updating it.
-    /// If the tick brings the tweenable to its end, the state changes to [`TweenState::Ended`].
+    /// [`rewind()`]: Tweenable::rewind
+    /// [`set_progress()`]: Tweenable::set_progress
     fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState;
 
-    /// Stop the animation. This changes its state to [`TweenState::Stopped`].
-    fn stop(&mut self);
+    /// Get the number of times this tweenable completed.
+    ///
+    /// For looping animations, this returns the number of times a single playback was completed. In the
+    /// case of [`TweeningType::PingPong`] this corresponds to a playback in a single direction, so tweening
+    /// from start to end and back to start counts as two completed times (one forward, one backward).
+    fn times_completed(&self) -> u32;
+
+    /// Rewind the animation to its starting state.
+    fn rewind(&mut self);
 }
 
 impl<T> Tweenable<T> for Box<dyn Tweenable<T> + Send + Sync + 'static> {
     fn duration(&self) -> Duration {
         self.as_ref().duration()
     }
+    fn is_looping(&self) -> bool {
+        self.as_ref().is_looping()
+    }
+    fn set_progress(&mut self, progress: f32) {
+        self.as_mut().set_progress(progress);
+    }
     fn progress(&self) -> f32 {
         self.as_ref().progress()
     }
     fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
         self.as_mut().tick(delta, target)
     }
-    fn stop(&mut self) {
-        self.as_mut().stop()
+    fn times_completed(&self) -> u32 {
+        self.as_ref().times_completed()
+    }
+    fn rewind(&mut self) {
+        self.as_mut().rewind()
     }
 }
 
@@ -66,16 +112,41 @@ impl<T, U: Tweenable<T> + Send + Sync + 'static> IntoBoxDynTweenable<T> for U {
 pub struct Tween<T> {
     ease_function: EaseMethod,
     timer: Timer,
-    state: TweenState,
     tweening_type: TweeningType,
     direction: TweeningDirection,
+    times_completed: u32,
     lens: Box<dyn Lens<T> + Send + Sync + 'static>,
-    on_started: Option<Box<dyn FnMut() + Send + Sync + 'static>>,
-    on_ended: Option<Box<dyn FnMut() + Send + Sync + 'static>>,
+    on_completed: Option<Box<dyn Fn(&Tween<T>) + Send + Sync + 'static>>,
 }
 
 impl<T: 'static> Tween<T> {
-    /// Chain another [`Tweenable`] after this tween, making a sequence with the two.
+    /// Chain another [`Tweenable`] after this tween, making a [`Sequence`] with the two.
+    ///
+    /// # Example
+    /// ```
+    /// # use bevy_tweening::{lens::*, *};
+    /// # use bevy::math::*;
+    /// # use std::time::Duration;
+    /// let tween1 = Tween::new(
+    ///     EaseFunction::QuadraticInOut,
+    ///     TweeningType::Once,
+    ///     Duration::from_secs_f32(1.0),
+    ///     TransformPositionLens {
+    ///         start: Vec3::ZERO,
+    ///         end: Vec3::new(3.5, 0., 0.),
+    ///     },
+    /// );
+    /// let tween2 = Tween::new(
+    ///     EaseFunction::QuadraticInOut,
+    ///     TweeningType::Once,
+    ///     Duration::from_secs_f32(1.0),
+    ///     TransformRotationLens {
+    ///         start: Quat::IDENTITY,
+    ///         end: Quat::from_rotation_x(90.0_f32.to_radians()),
+    ///     },
+    /// );
+    /// let seq = tween1.then(tween2);
+    /// ```
     pub fn then(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> {
         Sequence::from_single(self).then(tween)
     }
@@ -83,6 +154,22 @@ impl<T: 'static> Tween<T> {
 
 impl<T> Tween<T> {
     /// Create a new tween animation.
+    ///
+    /// # Example
+    /// ```
+    /// # use bevy_tweening::{lens::*, *};
+    /// # use bevy::math::Vec3;
+    /// # use std::time::Duration;
+    /// let tween = Tween::new(
+    ///     EaseFunction::QuadraticInOut,
+    ///     TweeningType::Once,
+    ///     Duration::from_secs_f32(1.0),
+    ///     TransformPositionLens {
+    ///         start: Vec3::ZERO,
+    ///         end: Vec3::new(3.5, 0., 0.),
+    ///     },
+    /// );
+    /// ```
     pub fn new<L>(
         ease_function: impl Into<EaseMethod>,
         tweening_type: TweeningType,
@@ -95,12 +182,11 @@ impl<T> Tween<T> {
         Tween {
             ease_function: ease_function.into(),
             timer: Timer::new(duration, tweening_type != TweeningType::Once),
-            state: TweenState::Stopped,
             tweening_type,
             direction: TweeningDirection::Forward,
+            times_completed: 0,
             lens: Box::new(lens),
-            on_started: None,
-            on_ended: None,
+            on_completed: None,
         }
     }
 
@@ -111,48 +197,39 @@ impl<T> Tween<T> {
         self.direction
     }
 
-    /// Set a callback invoked when the animation starts.
-    pub fn set_started<C>(&mut self, callback: C)
+    /// Set a callback invoked when the animation completed.
+    ///
+    /// Only non-looping tweenables can complete.
+    pub fn set_completed<C>(&mut self, callback: C)
     where
-        C: FnMut() + Send + Sync + 'static,
+        C: Fn(&Tween<T>) + Send + Sync + 'static,
     {
-        self.on_started = Some(Box::new(callback));
+        self.on_completed = Some(Box::new(callback));
     }
 
-    /// Clear the callback invoked when the animation starts.
-    pub fn clear_started(&mut self) {
-        self.on_started = None;
-    }
-
-    /// Set a callback invoked when the animation ends.
-    pub fn set_ended<C>(&mut self, callback: C)
-    where
-        C: FnMut() + Send + Sync + 'static,
-    {
-        self.on_ended = Some(Box::new(callback));
+    /// Clear the callback invoked when the animation completed.
+    pub fn clear_completed(&mut self) {
+        self.on_completed = None;
     }
+}
 
-    /// Clear the callback invoked when the animation ends.
-    pub fn clear_ended(&mut self) {
-        self.on_ended = None;
+impl<T> Tweenable<T> for Tween<T> {
+    fn duration(&self) -> Duration {
+        self.timer.duration()
     }
 
-    /// Is the animation playback looping?
-    pub fn is_looping(&self) -> bool {
+    fn is_looping(&self) -> bool {
         self.tweening_type != TweeningType::Once
     }
-}
 
-impl<T> Tweenable<T> for Tween<T> {
-    fn duration(&self) -> Duration {
-        self.timer.duration()
+    fn set_progress(&mut self, progress: f32) {
+        self.timer.set_elapsed(Duration::from_secs_f64(
+            self.timer.duration().as_secs_f64() * progress as f64,
+        ));
+        // set_elapsed() does not update finished() etc. which we rely on
+        self.timer.tick(Duration::ZERO);
     }
 
-    /// Current animation progress ratio between 0 and 1.
-    ///
-    /// For reversed playback ([`TweeningDirection::Backward`]), the ratio goes from 0 at the
-    /// end point (beginning of backward playback) to 1 at the start point (end of backward
-    /// playback).
     fn progress(&self) -> f32 {
         match self.direction {
             TweeningDirection::Forward => self.timer.percent(),
@@ -161,14 +238,13 @@ impl<T> Tweenable<T> for Tween<T> {
     }
 
     fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
-        let old_state = self.state;
-        if old_state == TweenState::Stopped {
-            self.state = TweenState::Running;
-            if let Some(cb) = &mut self.on_started {
-                cb();
-            }
+        if !self.is_looping() && self.timer.finished() {
+            return TweenState::Completed;
         }
 
+        let mut state = TweenState::Active;
+
+        // Tick the timer to update the animation time
         self.timer.tick(delta);
 
         // Toggle direction immediately, so self.progress() returns the correct ratio
@@ -176,30 +252,34 @@ impl<T> Tweenable<T> for Tween<T> {
             self.direction = !self.direction;
         }
 
+        // Apply the lens, even if the animation finished, to ensure the state is consistent
         let progress = self.progress();
         let factor = self.ease_function.sample(progress);
         self.lens.lerp(target, factor);
 
         if self.timer.just_finished() {
             if self.tweening_type == TweeningType::Once {
-                self.state = TweenState::Ended;
+                state = TweenState::Completed;
             }
 
-            // This is always true for non ping-pong, and is true for ping-pong when
-            // coming back to start after a full cycle start -> end -> start.
-            if self.direction == TweeningDirection::Forward {
-                if let Some(cb) = &mut self.on_ended {
-                    cb();
-                }
+            // Timer::times_finished() returns the number of finished times since last tick only
+            self.times_completed += self.timer.times_finished();
+
+            if let Some(cb) = &self.on_completed {
+                cb(&self);
             }
         }
 
-        self.state
+        state
+    }
+
+    fn times_completed(&self) -> u32 {
+        self.times_completed
     }
 
-    fn stop(&mut self) {
-        self.state = TweenState::Stopped;
+    fn rewind(&mut self) {
         self.timer.reset();
+        self.times_completed = 0;
     }
 }
 
@@ -207,25 +287,28 @@ impl<T> Tweenable<T> for Tween<T> {
 pub struct Sequence<T> {
     tweens: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
     index: usize,
-    state: TweenState,
     duration: Duration,
     time: Duration,
+    times_completed: u32,
 }
 
 impl<T> Sequence<T> {
     /// Create a new sequence of tweens.
+    ///
+    /// This method panics if the input collection is empty.
     pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self {
         let tweens: Vec<_> = items
             .into_iter()
             .map(IntoBoxDynTweenable::into_box_dyn)
             .collect();
+        assert!(!tweens.is_empty());
         let duration = tweens.iter().map(|t| t.duration()).sum();
         Sequence {
             tweens,
             index: 0,
-            state: TweenState::Stopped,
             duration,
             time: Duration::from_secs(0),
+            times_completed: 0,
         }
     }
 
@@ -235,9 +318,9 @@ impl<T> Sequence<T> {
         Sequence {
             tweens: vec![Box::new(tween)],
             index: 0,
-            state: TweenState::Stopped,
             duration,
             time: Duration::from_secs(0),
+            times_completed: 0,
         }
     }
 
@@ -264,34 +347,78 @@ impl<T> Tweenable<T> for Sequence<T> {
         self.duration
     }
 
+    fn is_looping(&self) -> bool {
+        false // TODO - implement looping sequences...
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        let progress = progress.max(0.);
+        self.times_completed = progress as u32;
+        let progress = if self.is_looping() {
+            progress.fract()
+        } else {
+            progress.min(1.)
+        };
+
+        // 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);
+
+        // Find which tween is active in the sequence
+        let mut accum_duration = 0.;
+        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 {
+                self.index = index;
+                let local_duration = total_elapsed_secs - accum_duration;
+                tween.set_progress((local_duration / tween_duration) as f32);
+                // TODO?? set progress of other tweens after that one to 0. ??
+                return;
+            }
+            tween.set_progress(1.); // ?? to prepare for next loop/rewind?
+            accum_duration += tween_duration;
+        }
+
+        // None found; sequence ended
+        self.index = self.tweens.len();
+    }
+
     fn progress(&self) -> f32 {
         self.time.as_secs_f32() / self.duration.as_secs_f32()
     }
 
     fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
         if self.index < self.tweens.len() {
-            self.state = TweenState::Running;
+            let mut state = TweenState::Active;
             self.time = (self.time + delta).min(self.duration);
             let tween = &mut self.tweens[self.index];
-            let state = tween.tick(delta, target);
-            if state == TweenState::Ended {
-                tween.stop();
+            let tween_state = tween.tick(delta, target);
+            if tween_state == TweenState::Completed {
+                tween.rewind();
                 self.index += 1;
                 if self.index >= self.tweens.len() {
-                    self.state = TweenState::Ended;
+                    state = TweenState::Completed;
+                    self.times_completed = 1;
                 }
             }
+            state
+        } else {
+            TweenState::Completed
         }
-        self.state
     }
 
-    fn stop(&mut self) {
-        if self.state != TweenState::Stopped {
-            self.state = TweenState::Stopped;
-            if self.index < self.tweens.len() {
-                let tween = &mut self.tweens[self.index];
-                tween.stop();
-            }
+    fn times_completed(&self) -> u32 {
+        self.times_completed
+    }
+
+    fn rewind(&mut self) {
+        self.time = Duration::from_secs(0);
+        self.index = 0;
+        self.times_completed = 0;
+        for tween in &mut self.tweens {
+            // or only first?
+            tween.rewind();
         }
     }
 }
@@ -301,6 +428,7 @@ pub struct Tracks<T> {
     tracks: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
     duration: Duration,
     time: Duration,
+    times_completed: u32,
 }
 
 impl<T> Tracks<T> {
@@ -315,6 +443,7 @@ impl<T> Tracks<T> {
             tracks,
             duration,
             time: Duration::from_secs(0),
+            times_completed: 0,
         }
     }
 }
@@ -324,27 +453,44 @@ impl<T> Tweenable<T> for Tracks<T> {
         self.duration
     }
 
+    fn is_looping(&self) -> bool {
+        false // TODO - implement looping tracks...
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        let progress = progress.max(0.);
+        self.times_completed = progress as u32;
+        let progress = progress.fract();
+        self.time = Duration::from_secs_f64(self.duration().as_secs_f64() * progress as f64);
+    }
+
     fn progress(&self) -> f32 {
         self.time.as_secs_f32() / self.duration.as_secs_f32()
     }
 
     fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
-        let mut any_running = false;
+        self.time = (self.time + delta).min(self.duration);
+        let mut any_active = false;
         for tweenable in &mut self.tracks {
             let state = tweenable.tick(delta, target);
-            any_running = any_running || (state == TweenState::Running);
+            any_active = any_active || (state == TweenState::Active);
         }
-        if any_running {
-            self.time = (self.time + delta).min(self.duration);
-            TweenState::Running
+        if any_active {
+            TweenState::Active
         } else {
-            TweenState::Ended
+            TweenState::Completed
         }
     }
 
-    fn stop(&mut self) {
-        for seq in &mut self.tracks {
-            seq.stop();
+    fn times_completed(&self) -> u32 {
+        self.times_completed
+    }
+
+    fn rewind(&mut self) {
+        self.time = Duration::from_secs(0);
+        self.times_completed = 0;
+        for tween in &mut self.tracks {
+            tween.rewind();
         }
     }
 }
@@ -377,6 +523,18 @@ impl<T> Tweenable<T> for Delay {
         self.timer.duration()
     }
 
+    fn is_looping(&self) -> bool {
+        false
+    }
+
+    fn set_progress(&mut self, progress: f32) {
+        self.timer.set_elapsed(Duration::from_secs_f64(
+            self.timer.duration().as_secs_f64() * progress as f64,
+        ));
+        // set_elapsed() does not update finished() etc. which we rely on
+        self.timer.tick(Duration::ZERO);
+    }
+
     fn progress(&self) -> f32 {
         self.timer.percent()
     }
@@ -384,13 +542,21 @@ impl<T> Tweenable<T> for Delay {
     fn tick(&mut self, delta: Duration, _: &mut T) -> TweenState {
         self.timer.tick(delta);
         if self.timer.finished() {
-            TweenState::Ended
+            TweenState::Completed
+        } else {
+            TweenState::Active
+        }
+    }
+
+    fn times_completed(&self) -> u32 {
+        if self.timer.finished() {
+            1
         } else {
-            TweenState::Running
+            0
         }
     }
 
-    fn stop(&mut self) {
+    fn rewind(&mut self) {
         self.timer.reset();
     }
 }
@@ -407,6 +573,12 @@ mod tests {
         (a - b).abs() < tol
     }
 
+    #[derive(Default, Copy, Clone)]
+    struct CallbackMonitor {
+        invoke_count: u64,
+        last_reported_count: u32,
+    }
+
     /// Test ticking of a single tween in isolation.
     #[test]
     fn tween_tick() {
@@ -415,6 +587,8 @@ mod tests {
             TweeningType::Loop,
             TweeningType::PingPong,
         ] {
+            println!("TweeningType: {:?}", tweening_type);
+
             // Create a linear tween over 1 second
             let mut tween = Tween::new(
                 EaseMethod::Linear,
@@ -427,75 +601,92 @@ mod tests {
             );
 
             // Register callbacks to count started/ended events
-            let started_count = Arc::new(Mutex::new(0));
-            let ended_count = Arc::new(Mutex::new(0));
-            let sc = Arc::clone(&started_count);
-            let ec = Arc::clone(&ended_count);
-            tween.set_started(move || {
-                let mut sc = sc.lock().unwrap();
-                *sc += 1;
-            });
-            tween.set_ended(move || {
-                let mut ec = ec.lock().unwrap();
-                *ec += 1;
+            let callback_monitor = Arc::new(Mutex::new(CallbackMonitor::default()));
+            let cb_mon_ptr = Arc::clone(&callback_monitor);
+            tween.set_completed(move |tween| {
+                let mut cb_mon = cb_mon_ptr.lock().unwrap();
+                cb_mon.invoke_count += 1;
+                cb_mon.last_reported_count = tween.times_completed();
             });
-            assert_eq!(*started_count.lock().unwrap(), 0);
-            assert_eq!(*ended_count.lock().unwrap(), 0);
+            assert_eq!(callback_monitor.lock().unwrap().invoke_count, 0);
 
             // Loop over 2.2 seconds, so greater than one ping-pong loop
             let mut transform = Transform::default();
             let tick_duration = Duration::from_secs_f32(0.2);
             for i in 1..=11 {
                 // Calculate expected values
-                let (ratio, ec, dir, expected_state) = match tweening_type {
+                let (progress, times_completed, direction, expected_state) = match tweening_type {
                     TweeningType::Once => {
-                        let r = (i as f32 * 0.2).min(1.0);
-                        let ec = if i >= 5 { 1 } else { 0 };
-                        let state = if i >= 5 {
-                            TweenState::Ended
+                        let progress = (i as f32 * 0.2).min(1.0);
+                        let times_completed = if i >= 5 { 1 } else { 0 };
+                        let state = if i < 5 {
+                            TweenState::Active
                         } else {
-                            TweenState::Running
+                            TweenState::Completed
                         };
-                        (r, ec, TweeningDirection::Forward, state)
+                        (progress, times_completed, TweeningDirection::Forward, state)
                     }
                     TweeningType::Loop => {
-                        let r = (i as f32 * 0.2).fract();
-                        let ec = i / 5;
-                        (r, ec, TweeningDirection::Forward, TweenState::Running)
+                        let progress = (i as f32 * 0.2).fract();
+                        let times_completed = i / 5;
+                        (
+                            progress,
+                            times_completed,
+                            TweeningDirection::Forward,
+                            TweenState::Active,
+                        )
                     }
                     TweeningType::PingPong => {
                         let i10 = i % 10;
-                        let r = if i10 >= 5 {
+                        let progress = if i10 >= 5 {
                             (10 - i10) as f32 * 0.2
                         } else {
                             i10 as f32 * 0.2
                         };
-                        let ec = i / 10;
-                        let dir = if i10 >= 5 {
+                        let times_completed = i / 5;
+                        let direction = if i10 >= 5 {
                             TweeningDirection::Backward
                         } else {
                             TweeningDirection::Forward
                         };
-                        (r, ec, dir, TweenState::Running)
+                        (progress, times_completed, direction, TweenState::Active)
                     }
                 };
                 println!(
-                    "Expected; r={} ec={} dir={:?} state={:?}",
-                    ratio, ec, dir, expected_state
+                    "Expected: progress={} times_completed={} direction={:?} state={:?}",
+                    progress, times_completed, direction, expected_state
                 );
 
                 // Tick the tween
                 let actual_state = tween.tick(tick_duration, &mut transform);
 
                 // Check actual values
-                assert_eq!(tween.direction(), dir);
+                assert_eq!(tween.direction(), direction);
+                assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once);
                 assert_eq!(actual_state, expected_state);
-                assert!(abs_diff_eq(tween.progress(), ratio, 1e-5));
-                assert!(transform.translation.abs_diff_eq(Vec3::splat(ratio), 1e-5));
+                assert!(abs_diff_eq(tween.progress(), progress, 1e-5));
+                assert_eq!(tween.times_completed(), times_completed);
+                assert!(transform
+                    .translation
+                    .abs_diff_eq(Vec3::splat(progress), 1e-5));
                 assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5));
-                assert_eq!(*started_count.lock().unwrap(), 1);
-                assert_eq!(*ended_count.lock().unwrap(), ec);
+                let cb_mon = callback_monitor.lock().unwrap();
+                assert_eq!(cb_mon.invoke_count, times_completed as u64);
+                assert_eq!(cb_mon.last_reported_count, times_completed);
             }
+
+            // Rewind
+            tween.rewind();
+            assert_eq!(tween.direction(), TweeningDirection::Forward);
+            assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once);
+            assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+            assert_eq!(tween.times_completed(), 0);
+
+            // Dummy tick to update target
+            let actual_state = tween.tick(Duration::ZERO, &mut transform);
+            assert_eq!(actual_state, TweenState::Active);
+            assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
+            assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5));
         }
     }
 
@@ -525,18 +716,18 @@ mod tests {
         for i in 1..=16 {
             let state = seq.tick(Duration::from_secs_f32(0.2), &mut transform);
             if i < 5 {
-                assert_eq!(state, TweenState::Running);
+                assert_eq!(state, TweenState::Active);
                 let r = i as f32 * 0.2;
                 assert_eq!(transform, Transform::from_translation(Vec3::splat(r)));
             } else if i < 10 {
-                assert_eq!(state, TweenState::Running);
+                assert_eq!(state, TweenState::Active);
                 let alpha_deg = (36 * (i - 5)) as f32;
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5));
             } else {
-                assert_eq!(state, TweenState::Ended);
+                assert_eq!(state, TweenState::Completed);
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
@@ -571,7 +762,7 @@ mod tests {
         for i in 1..=6 {
             let state = tracks.tick(Duration::from_secs_f32(0.2), &mut transform);
             if i < 5 {
-                assert_eq!(state, TweenState::Running);
+                assert_eq!(state, TweenState::Active);
                 let r = i as f32 * 0.2;
                 let alpha_deg = (45 * i) as f32;
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(r), 1e-5));
@@ -579,7 +770,7 @@ mod tests {
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5));
             } else {
-                assert_eq!(state, TweenState::Ended);
+                assert_eq!(state, TweenState::Completed);
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
-- 
GitLab