From 221d8bf4962e9371b8f70e7e8faf904da8da0105 Mon Sep 17 00:00:00 2001 From: Jerome Humbert Date: Thu, 4 Aug 2022 21:06:06 +0100 Subject: [PATCH] Revert "Revert "Support running an animation N times (#19)"" This reverts commit 87ac60b2a848a38a99dba722b59b97ddb59c6275. --- CHANGELOG.md | 13 ++ examples/colormaterial_color.rs | 5 +- examples/menu.rs | 1 - examples/sequence.rs | 43 ++-- examples/sprite_color.rs | 5 +- examples/text_color.rs | 5 +- examples/transform_rotation.rs | 5 +- examples/transform_translation.rs | 5 +- examples/ui_position.rs | 5 +- src/lib.rs | 103 ++++----- src/tweenable.rs | 338 ++++++++++++++++-------------- 11 files changed, 286 insertions(+), 242 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a5d5c..e1e3275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ 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 + +- Added `RepeatCount` and `RepeatStrategy` for more granular control over animation looping. +- Added `with_repeat_count()` and `with_repeat_strategy()` builder methods to `Tween`. + ## [0.5.0] - 2022-08-04 ### Added @@ -18,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Compatible with Bevy 0.8 - Double boxing in `Sequence` and `Tracks` was fixed. As a result, any custom tweenables should implement `From` for `BoxedTweenable` to make those APIs easier to use. +- Removed the `tweening_type` parameter from the signature of `Tween::new()`; use `with_repeat_count()` and `with_repeat_strategy()` instead. + +### Removed + +- Removed `Tweenable::is_looping()`, which was not implemented for most tweenables. +- Removed `TweeningType` in favor of `RepeatCount` and `RepeatStrategy`. ## [0.4.0] - 2022-04-16 diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs index edf90e3..391bfc7 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -77,13 +77,14 @@ fn setup( let tween = Tween::new( *ease_function, - TweeningType::PingPong, Duration::from_secs(1), ColorMaterialColorLens { start: Color::RED, end: Color::BLUE, }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(MaterialMesh2dBundle { diff --git a/examples/menu.rs b/examples/menu.rs index 3c0fefd..fccc6f7 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -49,7 +49,6 @@ fn setup(mut commands: Commands, asset_server: Res) { start_time_ms += 500; let tween_scale = Tween::new( EaseFunction::BounceOut, - TweeningType::Once, Duration::from_secs(2), TransformScaleLens { start: Vec3::splat(0.01), diff --git a/examples/sequence.rs b/examples/sequence.rs index 9ed183f..6526df0 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -1,6 +1,8 @@ +use std::time::Duration; + use bevy::prelude::*; + use bevy_tweening::{lens::*, *}; -use std::time::Duration; fn main() { App::default() @@ -107,19 +109,31 @@ fn setup(mut commands: Commands, asset_server: Res) { Vec3::new(margin, screen_y - margin, 0.), Vec3::new(margin, margin, 0.), ]; - // Build a sequence from an iterator over a Tweenable (here, a Tween) + // Build a sequence from an iterator over a Tweenable (here, a + // Tracks) let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| { - Tween::new( - EaseFunction::QuadraticInOut, - TweeningType::Once, - Duration::from_secs(1), - TransformPositionLens { - start: pair[0] - center, - end: pair[1] - center, - }, - ) - // Get an event after each segment - .with_completed_event(index as u64) + Tracks::new([ + Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_millis(250), + TransformRotateZLens { + start: 0., + end: 180_f32.to_radians(), + }, + ) + .with_repeat_count(RepeatCount::Finite(4)) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat), + Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + TransformPositionLens { + start: pair[0] - center, + end: pair[1] - center, + }, + ) + // Get an event after each segment + .with_completed_event(index as u64), + ]) })); commands @@ -138,7 +152,6 @@ fn setup(mut commands: Commands, asset_server: Res) { // scaling size at the same time. let tween_move = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::Once, Duration::from_secs(1), TransformPositionLens { start: Vec3::new(-200., 100., 0.), @@ -148,7 +161,6 @@ fn setup(mut commands: Commands, asset_server: Res) { .with_completed_event(99); // Get an event once move completed let tween_rotate = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::Once, Duration::from_secs(1), TransformRotationLens { start: Quat::IDENTITY, @@ -157,7 +169,6 @@ fn setup(mut commands: Commands, asset_server: Res) { ); let tween_scale = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::Once, Duration::from_secs(1), TransformScaleLens { start: Vec3::ONE, diff --git a/examples/sprite_color.rs b/examples/sprite_color.rs index e42aa4e..a85d3bb 100644 --- a/examples/sprite_color.rs +++ b/examples/sprite_color.rs @@ -61,13 +61,14 @@ fn setup(mut commands: Commands) { ] { let tween = Tween::new( *ease_function, - TweeningType::PingPong, std::time::Duration::from_secs(1), SpriteColorLens { start: Color::RED, end: Color::BLUE, }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(SpriteBundle { diff --git a/examples/text_color.rs b/examples/text_color.rs index a1d3e9f..e02d834 100644 --- a/examples/text_color.rs +++ b/examples/text_color.rs @@ -68,14 +68,15 @@ fn setup(mut commands: Commands, asset_server: Res) { ] { let tween = Tween::new( *ease_function, - TweeningType::PingPong, std::time::Duration::from_secs(1), TextColorLens { start: Color::RED, end: Color::BLUE, section: 0, }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(TextBundle { diff --git a/examples/transform_rotation.rs b/examples/transform_rotation.rs index aa68b87..8efa704 100644 --- a/examples/transform_rotation.rs +++ b/examples/transform_rotation.rs @@ -77,13 +77,14 @@ fn setup(mut commands: Commands) { ] { let tween = Tween::new( *ease_function, - TweeningType::PingPong, std::time::Duration::from_secs(1), TransformRotationLens { start: Quat::IDENTITY, end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(SpatialBundle { diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs index 29088b0..85b52b4 100644 --- a/examples/transform_translation.rs +++ b/examples/transform_translation.rs @@ -76,13 +76,14 @@ fn setup(mut commands: Commands) { ] { let tween = Tween::new( *ease_function, - TweeningType::PingPong, std::time::Duration::from_secs(1), TransformPositionLens { start: Vec3::new(x, screen_y, 0.), end: Vec3::new(x, -screen_y, 0.), }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(SpriteBundle { diff --git a/examples/ui_position.rs b/examples/ui_position.rs index 236b6fb..928e021 100644 --- a/examples/ui_position.rs +++ b/examples/ui_position.rs @@ -76,7 +76,6 @@ fn setup(mut commands: Commands) { ] { let tween = Tween::new( *ease_function, - TweeningType::PingPong, std::time::Duration::from_secs(1), UiPositionLens { start: UiRect { @@ -92,7 +91,9 @@ fn setup(mut commands: Commands) { bottom: Val::Auto, }, }, - ); + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); commands .spawn_bundle(NodeBundle { diff --git a/src/lib.rs b/src/lib.rs index 84ac755..551b92e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,10 +44,7 @@ //! let tween = Tween::new( //! // Use a quadratic easing on both endpoints. //! EaseFunction::QuadraticInOut, -//! // Loop animation back and forth. -//! TweeningType::PingPong, -//! // Animation time (one way only; for ping-pong it takes 2 seconds -//! // to come back to start). +//! // Animation time. //! Duration::from_secs(1), //! // The lens gives access to the Transform component of the Entity, //! // for the Animator to animate it. It also contains the start and @@ -90,7 +87,6 @@ //! let tween1 = Tween::new( //! // [...] //! # EaseFunction::BounceOut, -//! # TweeningType::Once, //! # Duration::from_secs(2), //! # TransformScaleLens { //! # start: Vec3::ZERO, @@ -100,7 +96,6 @@ //! let tween2 = Tween::new( //! // [...] //! # EaseFunction::QuadraticInOut, -//! # TweeningType::Once, //! # Duration::from_secs(1), //! # TransformPositionLens { //! # start: Vec3::ZERO, @@ -178,24 +173,43 @@ pub use tweenable::{ #[cfg(feature = "bevy_asset")] pub use plugin::asset_animator_system; -/// Type of looping for a tween animation. +/// How many times to repeat a tween animation. See also: [`RepeatStrategy`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TweeningType { - /// Run the animation once from start to end only. - Once, - /// Loop the animation indefinitely, restarting from the start each time the - /// end is reached. - Loop, - /// Loop the animation back and forth, changing direction each time an - /// endpoint is reached. A complete cycle start -> end -> start always - /// counts as 2 loop iterations for the various operations where looping - /// matters. - PingPong, +pub enum RepeatCount { + /// Run the animation N times. + Finite(u32), + /// Run the animation for some amount of time. + For(Duration), + /// Loop the animation indefinitely. + Infinite, } -impl Default for TweeningType { +/// What to do when a tween animation needs to be repeated. +/// +/// Only applicable when [`RepeatCount`] is greater than the animation duration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatStrategy { + /// Reset the animation back to its starting position. + Repeat, + /// Follow a ping-pong pattern, changing the direction each time an endpoint + /// is reached. + /// + /// A complete cycle start -> end -> start always counts as 2 loop + /// iterations for the various operations where looping matters. That + /// is, a 1 second animation will take 2 seconds to end up back where it + /// started. + MirroredRepeat, +} + +impl Default for RepeatCount { fn default() -> Self { - Self::Once + Self::Finite(1) + } +} + +impl Default for RepeatStrategy { + fn default() -> Self { + Self::Repeat } } @@ -279,12 +293,12 @@ impl From for EaseMethod { /// that target at the start bound of the lens, effectively making the animation /// play backward. /// -/// For all but [`TweeningType::PingPong`] this is always +/// For all but [`RepeatStrategy::MirroredRepeat`] this is always /// [`TweeningDirection::Forward`], unless manually configured with -/// [`Tween::set_direction()`] in which case the value is constant equal -/// to the value set. For the [`TweeningType::PingPong`] tweening type, this is -/// either forward (from start to end; ping) or backward (from end to start; -/// pong), depending on the current iteration of the loop. +/// [`Tween::set_direction()`] in which case the value is constant equal to the +/// value set. When using [`RepeatStrategy::MirroredRepeat`], this is either +/// forward (from start to end; ping) or backward (from end to start; pong), +/// depending on the current iteration of the loop. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TweeningDirection { /// Animation playing from start to end. @@ -385,19 +399,8 @@ macro_rules! animator_impl { } } - /// 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. + /// Get the current progress of the tweenable. See [`Tweenable::progress`] for + /// details. /// /// 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. @@ -543,11 +546,11 @@ impl AssetAnimator { #[cfg(test)] mod tests { - use super::{lens::*, *}; - #[cfg(feature = "bevy_asset")] use bevy::reflect::TypeUuid; + use super::{lens::*, *}; + struct DummyLens { start: f32, end: f32, @@ -579,9 +582,15 @@ mod tests { } #[test] - fn tweening_type() { - let tweening_type = TweeningType::default(); - assert_eq!(tweening_type, TweeningType::Once); + fn repeat_count() { + let count = RepeatCount::default(); + assert_eq!(count, RepeatCount::Finite(1)); + } + + #[test] + fn repeat_strategy() { + let strategy = RepeatStrategy::default(); + assert_eq!(strategy, RepeatStrategy::Repeat); } #[test] @@ -631,7 +640,6 @@ mod tests { fn animator_new() { let tween = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -647,7 +655,6 @@ mod tests { for state in [AnimatorState::Playing, AnimatorState::Paused] { let tween = Tween::::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -665,7 +672,6 @@ mod tests { let tween = Tween::::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -679,7 +685,6 @@ mod tests { fn animator_controls() { let tween = Tween::::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -719,7 +724,6 @@ mod tests { fn asset_animator_new() { let tween = Tween::::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -736,7 +740,6 @@ mod tests { for state in [AnimatorState::Playing, AnimatorState::Paused] { let tween = Tween::::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -757,7 +760,6 @@ mod tests { let tween = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -773,7 +775,6 @@ mod tests { fn asset_animator_controls() { let tween = Tween::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); diff --git a/src/tweenable.rs b/src/tweenable.rs index 67d18c2..9b13dd1 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -2,7 +2,7 @@ use std::time::Duration; use bevy::prelude::*; -use crate::{EaseMethod, Lens, TweeningDirection, TweeningType}; +use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection}; /// The dynamic tweenable type. /// @@ -28,7 +28,6 @@ use crate::{EaseMethod, Lens, TweeningDirection, TweeningType}; /// # struct MyTweenable; /// # impl Tweenable for MyTweenable { /// # fn duration(&self) -> Duration { unimplemented!() } -/// # fn is_looping(&self) -> bool { 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) -> TweenState { unimplemented!() } @@ -60,20 +59,20 @@ 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. + /// time. + /// + /// Note that [`RepeatCount::Infinite`] tweenables never reach this state. Completed, } /// Event raised when a tween completed. /// -/// This event is raised when a tween completed. For non-looping tweens, this is -/// raised once at the end of the animation. For looping animations, this is -/// raised once per iteration. In case the animation direction changes -/// ([`TweeningType::PingPong`]), an iteration corresponds to a single progress -/// from one endpoint to the other, whatever the direction. Therefore a complete -/// cycle start -> end -> start counts as 2 iterations and raises 2 events (one -/// when reaching the end, one when reaching back the start). +/// This event is raised when a tween completed. When looping, this is raised +/// once per iteration. In case the animation direction changes +/// ([`RepeatStrategy::MirroredRepeat`]), an iteration corresponds to a single +/// progress from one endpoint to the other, whatever the direction. Therefore a +/// complete cycle start -> end -> start counts as 2 iterations and raises 2 +/// events (one when reaching the end, one when reaching back the start). /// /// # Note /// @@ -97,82 +96,94 @@ pub struct TweenCompleted { pub user_data: u64, } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug)] struct AnimClock { elapsed: Duration, duration: Duration, - is_looping: bool, + times_completed: u32, + total_duration: TotalDuration, + strategy: RepeatStrategy, } impl AnimClock { - fn new(duration: Duration, is_looping: bool) -> Self { + fn new(duration: Duration) -> Self { Self { elapsed: Duration::ZERO, duration, - is_looping, + total_duration: compute_total_duration(duration, RepeatCount::default()), + times_completed: 0, + strategy: RepeatStrategy::default(), } } - fn tick(&mut self, duration: Duration) -> u32 { - self.elapsed = self.elapsed.saturating_add(duration); + fn record_completions(&mut self, times_completed: u32) { + self.times_completed = self.times_completed.saturating_add(times_completed); + } - if self.elapsed < self.duration { - 0 - } else if self.is_looping { - let elapsed = self.elapsed.as_nanos(); - let duration = self.duration.as_nanos(); + fn tick(&mut self, tick: Duration) -> u32 { + let duration = self.duration.as_nanos(); - self.elapsed = Duration::from_nanos((elapsed % duration) as u64); - (elapsed / duration) as u32 - } else { - self.elapsed = self.duration; - 1 + let before = self.elapsed.as_nanos() / duration; + self.elapsed = self.elapsed.saturating_add(tick); + if let TotalDuration::Finite(duration) = self.total_duration { + self.elapsed = self.elapsed.min(duration); } + (self.elapsed.as_nanos() / duration - before) as u32 } fn set_progress(&mut self, progress: f32) { - let progress = if self.is_looping { - progress.max(0.).fract() - } else { - progress.clamp(0., 1.) - }; - - self.elapsed = self.duration.mul_f32(progress); + self.elapsed = self.duration.mul_f32(progress.max(0.)); } fn progress(&self) -> f32 { self.elapsed.as_secs_f32() / self.duration.as_secs_f32() } - fn completed(&self) -> bool { - self.elapsed >= self.duration + fn state(&self) -> TweenState { + match self.total_duration { + TotalDuration::Finite(duration) => { + if self.elapsed >= duration { + TweenState::Completed + } else { + TweenState::Active + } + } + TotalDuration::Infinite => TweenState::Active, + } } fn reset(&mut self) { + self.times_completed = 0; self.elapsed = Duration::ZERO; } } +#[derive(Debug)] +enum TotalDuration { + Finite(Duration), + Infinite, +} + +fn compute_total_duration(duration: Duration, count: RepeatCount) -> TotalDuration { + match count { + RepeatCount::Finite(times) => TotalDuration::Finite(duration.saturating_mul(times)), + RepeatCount::For(duration) => TotalDuration::Finite(duration), + RepeatCount::Infinite => TotalDuration::Infinite, + } +} + /// An animatable entity, either a single [`Tween`] or a collection of them. pub trait Tweenable: Send + Sync { /// Get the total duration of the animation. /// - /// For non-looping tweenables ([`TweeningType::Once`]), this is the total - /// animation duration. For looping ones, this is the duration of a - /// single iteration, since the total animation duration is infinite. + /// This is always the duration of a single iteration, even when looping. /// - /// Note that for [`TweeningType::PingPong`], this is the duration of a - /// single way, either from start to end or back from end to start. The - /// total "loop" duration start -> end -> start to reach back the same - /// state in this case is the double of the returned value. + /// Note that for [`RepeatStrategy::MirroredRepeat`], this is the duration + /// of a single way, either from start to end or back from end to start. + /// The total "loop" duration start -> end -> start to reach back the + /// same state in this case is the double of the returned value. fn duration(&self) -> Duration; - /// Return `true` if the animation is looping. - /// - /// Looping tweenables are of type [`TweeningType::Loop`] or - /// [`TweeningType::PingPong`]. - fn is_looping(&self) -> bool; - /// Set the current animation playback progress. /// /// See [`progress()`] for details on the meaning. @@ -180,20 +191,12 @@ pub trait Tweenable: Send + Sync { /// [`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. + /// Get the current progress in \[0:1\] 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. + /// While looping, the exact value `1.0` is never reached, since the + /// tweenable loops over to `0.0` immediately when it changes direction at + /// either endpoint. Upon completion, the tweenable always reports exactly + /// `1.0`. fn progress(&self) -> f32; /// Tick the animation, advancing it by the given delta time and mutating @@ -223,10 +226,10 @@ pub trait Tweenable: Send + Sync { /// 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). + /// playback was completed. In the case of + /// [`RepeatStrategy::MirroredRepeat`] 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. @@ -270,8 +273,6 @@ pub type CompletedCallback = dyn Fn(Entity, &Tween) + Send + Sync + 'stati pub struct Tween { ease_function: EaseMethod, clock: AnimClock, - times_completed: u32, - tweening_type: TweeningType, direction: TweeningDirection, lens: Box + Send + Sync + 'static>, on_completed: Option>>, @@ -289,7 +290,6 @@ impl Tween { /// # use std::time::Duration; /// let tween1 = Tween::new( /// EaseFunction::QuadraticInOut, - /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformPositionLens { /// start: Vec3::ZERO, @@ -298,7 +298,6 @@ impl Tween { /// ); /// let tween2 = Tween::new( /// EaseFunction::QuadraticInOut, - /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformRotationLens { /// start: Quat::IDENTITY, @@ -323,7 +322,6 @@ impl Tween { /// # use std::time::Duration; /// let tween = Tween::new( /// EaseFunction::QuadraticInOut, - /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformPositionLens { /// start: Vec3::ZERO, @@ -332,20 +330,13 @@ impl Tween { /// ); /// ``` #[must_use] - pub fn new( - ease_function: impl Into, - tweening_type: TweeningType, - duration: Duration, - lens: L, - ) -> Self + pub fn new(ease_function: impl Into, duration: Duration, lens: L) -> Self where L: Lens + Send + Sync + 'static, { Self { ease_function: ease_function.into(), - clock: AnimClock::new(duration, tweening_type != TweeningType::Once), - times_completed: 0, - tweening_type, + clock: AnimClock::new(duration), direction: TweeningDirection::Forward, lens: Box::new(lens), on_completed: None, @@ -367,7 +358,6 @@ impl Tween { /// let tween = Tween::new( /// // [...] /// # EaseFunction::QuadraticInOut, - /// # TweeningType::Once, /// # Duration::from_secs_f32(1.0), /// # TransformPositionLens { /// # start: Vec3::ZERO, @@ -425,6 +415,20 @@ impl Tween { self.direction } + /// Set the number of times to repeat the animation. + #[must_use] + pub fn with_repeat_count(mut self, count: RepeatCount) -> Self { + self.clock.total_duration = compute_total_duration(self.clock.duration, count); + self + } + + /// Choose how the animation behaves upon a repetition. + #[must_use] + pub fn with_repeat_strategy(mut self, strategy: RepeatStrategy) -> Self { + self.clock.strategy = strategy; + self + } + /// Set a callback invoked when the animation completes. /// /// The callback when invoked receives as parameters the [`Entity`] on which @@ -469,10 +473,6 @@ impl Tweenable for Tween { self.clock.duration } - fn is_looping(&self) -> bool { - self.tweening_type != TweeningType::Once - } - fn set_progress(&mut self, progress: f32) { self.clock.set_progress(progress); } @@ -488,22 +488,17 @@ impl Tweenable for Tween { entity: Entity, event_writer: &mut EventWriter, ) -> TweenState { - if !self.is_looping() && self.clock.completed() { + if self.clock.state() == TweenState::Completed { return TweenState::Completed; } // Tick the animation clock let times_completed = self.clock.tick(delta); - self.times_completed += times_completed; - if times_completed & 1 != 0 && self.tweening_type == TweeningType::PingPong { + self.clock.record_completions(times_completed); + if self.clock.strategy == RepeatStrategy::MirroredRepeat && times_completed & 1 != 0 { self.direction = !self.direction; } - let state = if self.is_looping() || times_completed == 0 { - TweenState::Active - } else { - TweenState::Completed - }; - let progress = self.clock.progress(); + let progress = self.progress(); // Apply the lens, even if the animation finished, to ensure the state is // consistent @@ -527,16 +522,15 @@ impl Tweenable for Tween { } } - state + self.clock.state() } fn times_completed(&self) -> u32 { - self.times_completed + self.clock.times_completed } fn rewind(&mut self) { self.clock.reset(); - self.times_completed = 0; } } @@ -623,10 +617,6 @@ impl Tweenable for Sequence { self.duration } - fn is_looping(&self) -> bool { - false // TODO - implement looping sequences... - } - fn set_progress(&mut self, progress: f32) { self.times_completed = if progress >= 1. { 1 } else { 0 }; let progress = progress.clamp(0., 1.); // not looping @@ -733,10 +723,6 @@ impl Tweenable for Tracks { self.duration } - fn is_looping(&self) -> bool { - false // TODO - implement looping tracks... - } - 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 @@ -818,10 +804,6 @@ impl Tweenable for Delay { self.timer.duration() } - fn is_looping(&self) -> bool { - false - } - fn set_progress(&mut self, progress: f32) { // need to reset() to clear finished() unfortunately self.timer.reset(); @@ -891,7 +873,8 @@ mod tests { #[test] fn anim_clock_precision() { let duration = Duration::from_millis(1); - let mut clock = AnimClock::new(duration, true); + let mut clock = AnimClock::new(duration); + clock.total_duration = TotalDuration::Infinite; let test_ticks = [ Duration::from_micros(123), @@ -922,27 +905,29 @@ mod tests { #[test] fn tween_tick() { for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] { - for tweening_type in &[ - TweeningType::Once, - TweeningType::Loop, - TweeningType::PingPong, + for (count, strategy) in &[ + (RepeatCount::Finite(1), RepeatStrategy::default()), + (RepeatCount::Infinite, RepeatStrategy::Repeat), + (RepeatCount::Finite(2), RepeatStrategy::Repeat), + (RepeatCount::Infinite, RepeatStrategy::MirroredRepeat), + (RepeatCount::Finite(2), RepeatStrategy::MirroredRepeat), ] { println!( - "TweeningType: type={:?} dir={:?}", - tweening_type, tweening_direction + "TweeningType: count={count:?} strategy={strategy:?} dir={tweening_direction:?}", ); // Create a linear tween over 1 second let mut tween = Tween::new( EaseMethod::Linear, - *tweening_type, Duration::from_secs_f32(1.0), TransformPositionLens { start: Vec3::ZERO, end: Vec3::ONE, }, ) - .with_direction(*tweening_direction); + .with_direction(*tweening_direction) + .with_repeat_count(*count) + .with_repeat_strategy(*strategy); assert_eq!(tween.direction(), *tweening_direction); assert!(tween.on_completed.is_none()); assert!(tween.event_data.is_none()); @@ -982,8 +967,8 @@ mod tests { for i in 1..=11 { // Calculate expected values let (progress, times_completed, mut direction, expected_state, just_completed) = - match tweening_type { - TweeningType::Once => { + match count { + RepeatCount::Finite(1) => { let progress = (i as f32 * 0.2).min(1.0); let times_completed = if i >= 5 { 1 } else { 0 }; let state = if i < 5 { @@ -1000,37 +985,77 @@ mod tests { just_completed, ) } - TweeningType::Loop => { - let progress = (i as f32 * 0.2).fract(); - let times_completed = i / 5; - let just_completed = i % 5 == 0; - ( - progress, - times_completed, - TweeningDirection::Forward, - TweenState::Active, - just_completed, - ) + RepeatCount::Finite(count) => { + let progress = (i as f32 * 0.2).min(1.0 * *count as f32); + if *strategy == RepeatStrategy::Repeat { + let times_completed = i / 5; + let just_completed = i % 5 == 0; + ( + progress, + times_completed, + TweeningDirection::Forward, + if i < 10 { + TweenState::Active + } else { + TweenState::Completed + }, + just_completed, + ) + } else { + let i5 = i % 5; + let times_completed = i / 5; + let i10 = i % 10; + let direction = if i10 >= 5 { + TweeningDirection::Backward + } else { + TweeningDirection::Forward + }; + let just_completed = i5 == 0; + ( + progress, + times_completed, + direction, + if i < 10 { + TweenState::Active + } else { + TweenState::Completed + }, + just_completed, + ) + } } - TweeningType::PingPong => { - let i5 = i % 5; - let progress = i5 as f32 * 0.2; - let times_completed = i / 5; - let i10 = i % 10; - let direction = if i10 >= 5 { - TweeningDirection::Backward + RepeatCount::Infinite => { + let progress = i as f32 * 0.2; + if *strategy == RepeatStrategy::Repeat { + let times_completed = i / 5; + let just_completed = i % 5 == 0; + ( + progress, + times_completed, + TweeningDirection::Forward, + TweenState::Active, + just_completed, + ) } else { - TweeningDirection::Forward - }; - let just_completed = i5 == 0; - ( - progress, - times_completed, - direction, - TweenState::Active, - just_completed, - ) + let i5 = i % 5; + let times_completed = i / 5; + let i10 = i % 10; + let direction = if i10 >= 5 { + TweeningDirection::Backward + } else { + TweeningDirection::Forward + }; + let just_completed = i5 == 0; + ( + progress, + times_completed, + direction, + TweenState::Active, + just_completed, + ) + } } + RepeatCount::For(_) => panic!("Untested"), }; let factor = if tweening_direction.is_backward() { direction = !direction; @@ -1068,7 +1093,6 @@ mod tests { // Check actual values 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(), progress, 1e-5)); assert_eq!(tween.times_completed(), times_completed); @@ -1097,7 +1121,6 @@ mod tests { // Rewind tween.rewind(); assert_eq!(tween.direction(), *tweening_direction); // does not change - assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once); assert!(abs_diff_eq(tween.progress(), 0., 1e-5)); assert_eq!(tween.times_completed(), 0); @@ -1133,7 +1156,6 @@ mod tests { fn tween_dir() { let mut tween = Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(1.0), TransformPositionLens { start: Vec3::ZERO, @@ -1191,7 +1213,6 @@ mod tests { fn seq_tick() { let tween1 = Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(1.0), TransformPositionLens { start: Vec3::ZERO, @@ -1200,7 +1221,6 @@ mod tests { ); let tween2 = Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(1.0), TransformRotationLens { start: Quat::IDENTITY, @@ -1231,13 +1251,13 @@ mod tests { } else if i < 10 { assert_eq!(state, TweenState::Active); let alpha_deg = (18 * (i - 5)) as f32; - assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5)); assert!(transform .rotation .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5)); } else { assert_eq!(state, TweenState::Completed); - assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5)); assert!(transform .rotation .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5)); @@ -1251,7 +1271,6 @@ mod tests { let mut seq = Sequence::new((1..5).map(|i| { Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(0.2 * i as f32), TransformPositionLens { start: Vec3::ZERO, @@ -1259,7 +1278,6 @@ mod tests { }, ) })); - assert!(!seq.is_looping()); let mut progress = 0.; for i in 1..5 { @@ -1282,7 +1300,6 @@ mod tests { fn tracks_tick() { let tween1 = Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(1.), TransformPositionLens { start: Vec3::ZERO, @@ -1291,7 +1308,6 @@ mod tests { ); let tween2 = Tween::new( EaseMethod::Linear, - TweeningType::Once, Duration::from_secs_f32(0.8), // shorter TransformRotationLens { start: Quat::IDENTITY, @@ -1300,7 +1316,6 @@ mod tests { ); let mut tracks = Tracks::new([tween1, tween2]); assert_eq!(tracks.duration(), Duration::from_secs_f32(1.)); // max(1., 0.8) - assert!(!tracks.is_looping()); let mut transform = Transform::default(); @@ -1332,7 +1347,7 @@ mod tests { assert_eq!(state, TweenState::Completed); assert_eq!(tracks.times_completed(), 1); assert!((tracks.progress() - 1.).abs() < 1e-5); - assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5)); assert!(transform .rotation .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5)); @@ -1388,7 +1403,6 @@ mod tests { { let tweenable: &dyn Tweenable = &delay; assert_eq!(tweenable.duration(), duration); - assert!(!tweenable.is_looping()); assert!(tweenable.progress().abs() < 1e-5); }