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