diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd124a22c729396f0a08fd5e7661817ca289946..b8caeb957dcf333c39efd1ad7ba7fc05cff20303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,24 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added `is_forward()` and `is_backward()` convenience helpers to `TweeningDirection`. -- Added `Tween::set_direction()` and `Tween::with_direction()` which allow configuring the playback direction of a tween, allowing to play it backward from end to start. -- Added support for dynamically changing an animation's speed with `Animator::set_speed`. -- Added `AnimationSystem` label to tweening tick systems. -- Added `BoxedTweenable` type to make working with `Box<dyn Tweenable + ...>` easier. -- Added `RepeatCount` and `RepeatStrategy` for more granular control over animation looping. -- Added `with_repeat_count()` and `with_repeat_strategy()` builder methods to `Tween<T>`. +- Add `is_forward()` and `is_backward()` convenience helpers to `TweeningDirection`. +- Add `Tween::set_direction()` and `Tween::with_direction()` which allow configuring the playback direction of a tween, allowing to play it backward from end to start. +- Support dynamically changing an animation's speed with `Animator::set_speed` +- Add `AnimationSystem` label to tweening tick systems +- A `BoxedTweenable` type to make working with `Box<dyn Tweenable + ...>` easier ### Changed -- Double boxing in `Sequence` and `Tracks` was fixed. As a result, any custom tweenables. +- 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<T>::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 3e4285b3f150025a7ddb70a6dfff6d3cb30991b0..ad6e950728074fffd354348e3e1ed81510dd6c3b 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -77,14 +77,13 @@ 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 8eefcfb0a5271e00519bcbc8ce58bb2557fb28e1..1990afaec1f178eee7070f6a162b3052cce58d57 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -50,6 +50,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { 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 67e7ff36e6fede980eb1557c3a5274471854e911..6ae3f1923758fd9ddac19621bebbe8b2781f603f 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -1,8 +1,6 @@ -use std::time::Duration; - use bevy::prelude::*; - use bevy_tweening::{lens::*, *}; +use std::time::Duration; fn main() { App::default() @@ -109,31 +107,19 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { Vec3::new(margin, screen_y - margin, 0.), Vec3::new(margin, margin, 0.), ]; - // Build a sequence from an iterator over a Tweenable (here, a - // Tracks<Transform>) + // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>) let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| { - 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), - ]) + 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) })); commands @@ -152,6 +138,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { // 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.), @@ -161,6 +148,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { .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, @@ -169,6 +157,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { ); 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 a21578552a41f49fce711cc6d52ed9bc507cb88d..3f1a94a883dc9f40644330b943930fb70150a690 100644 --- a/examples/sprite_color.rs +++ b/examples/sprite_color.rs @@ -61,14 +61,13 @@ 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 66ee419b80260e9faf31eb1baa09cbca663784c5..523233008d50744112319a13c7ec34aa33e6b491 100644 --- a/examples/text_color.rs +++ b/examples/text_color.rs @@ -68,15 +68,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { ] { 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 a98be3df0fe42d337dd39abd88d9b5119ed4db92..a259ff323ad9913c63f60de89ceae761b632be48 100644 --- a/examples/transform_rotation.rs +++ b/examples/transform_rotation.rs @@ -77,14 +77,13 @@ 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(( diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs index 8f5138a6c2815e2e07f0792ca92e9197a95d9e94..75cc678cdfbf4cb9b13ade645b79e26d2e6fe912 100644 --- a/examples/transform_translation.rs +++ b/examples/transform_translation.rs @@ -76,14 +76,13 @@ 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 9043dd27c44a920ccbe30f7cf61bb49a8458b7cd..24ff0e3ab90c97f8a20ce11c9a70f772df008dfd 100644 --- a/examples/ui_position.rs +++ b/examples/ui_position.rs @@ -76,6 +76,7 @@ fn setup(mut commands: Commands) { ] { let tween = Tween::new( *ease_function, + TweeningType::PingPong, std::time::Duration::from_secs(1), UiPositionLens { start: Rect { @@ -91,9 +92,7 @@ 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 4ce635fd2af70e18dcfac9b0e10872918acd982e..55a104c815b1681f7157c45ecb0499bfde544742 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,10 @@ //! let tween = Tween::new( //! // Use a quadratic easing on both endpoints. //! EaseFunction::QuadraticInOut, -//! // Animation time. +//! // Loop animation back and forth. +//! TweeningType::PingPong, +//! // Animation time (one way only; for ping-pong it takes 2 seconds +//! // to come back to start). //! 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 @@ -87,6 +90,7 @@ //! let tween1 = Tween::new( //! // [...] //! # EaseFunction::BounceOut, +//! # TweeningType::Once, //! # Duration::from_secs(2), //! # TransformScaleLens { //! # start: Vec3::ZERO, @@ -96,6 +100,7 @@ //! let tween2 = Tween::new( //! // [...] //! # EaseFunction::QuadraticInOut, +//! # TweeningType::Once, //! # Duration::from_secs(1), //! # TransformPositionLens { //! # start: Vec3::ZERO, @@ -151,12 +156,16 @@ //! [`Sprite`]: https://docs.rs/bevy/0.7.0/bevy/sprite/struct.Sprite.html //! [`Transform`]: https://docs.rs/bevy/0.7.0/bevy/transform/components/struct.Transform.html +use bevy::{asset::Asset, prelude::*}; use std::time::Duration; -use bevy::{asset::Asset, prelude::*}; use interpolation::Ease as IEase; pub use interpolation::{EaseFunction, Lerp}; +pub mod lens; +mod plugin; +mod tweenable; + pub use lens::Lens; pub use plugin::{ asset_animator_system, component_animator_system, AnimationSystem, TweeningPlugin, @@ -165,47 +174,24 @@ pub use tweenable::{ BoxedTweenable, Delay, Sequence, Tracks, Tween, TweenCompleted, TweenState, Tweenable, }; -pub mod lens; -mod plugin; -mod tweenable; - -/// How many times to repeat a tween animation. See also: [`RepeatStrategy`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -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, -} - -/// What to do when a tween animation needs to be repeated. -/// -/// Only applicable when [`RepeatCount`] is greater than the animation duration. +/// Type of looping for a tween animation. #[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, +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, } -impl Default for RepeatCount { +impl Default for TweeningType { fn default() -> Self { - Self::Finite(1) - } -} - -impl Default for RepeatStrategy { - fn default() -> Self { - Self::Repeat + Self::Once } } @@ -289,12 +275,12 @@ impl From<EaseFunction> for EaseMethod { /// that target at the start bound of the lens, effectively making the animation /// play backward. /// -/// For all but [`RepeatStrategy::MirroredRepeat`] this is always +/// For all but [`TweeningType::PingPong`] this is always /// [`TweeningDirection::Forward`], unless manually configured with -/// [`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. +/// [`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. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TweeningDirection { /// Animation playing from start to end. @@ -395,8 +381,19 @@ macro_rules! animator_impl { } } - /// Get the current progress of the tweenable. See [`Tweenable::progress`] for - /// details. + /// 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. @@ -538,9 +535,8 @@ impl<T: Asset> AssetAnimator<T> { #[cfg(test)] mod tests { - use bevy::reflect::TypeUuid; - use super::{lens::*, *}; + use bevy::reflect::TypeUuid; struct DummyLens { start: f32, @@ -571,15 +567,9 @@ mod tests { } #[test] - 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); + fn tweening_type() { + let tweening_type = TweeningType::default(); + assert_eq!(tweening_type, TweeningType::Once); } #[test] @@ -629,6 +619,7 @@ mod tests { fn animator_new() { let tween = Tween::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -644,6 +635,7 @@ mod tests { for state in [AnimatorState::Playing, AnimatorState::Paused] { let tween = Tween::<DummyComponent>::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -661,6 +653,7 @@ mod tests { let tween = Tween::<DummyComponent>::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -674,6 +667,7 @@ mod tests { fn animator_controls() { let tween = Tween::<DummyComponent>::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -712,6 +706,7 @@ mod tests { fn asset_animator_new() { let tween = Tween::<DummyAsset>::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -727,6 +722,7 @@ mod tests { for state in [AnimatorState::Playing, AnimatorState::Paused] { let tween = Tween::<DummyAsset>::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -746,6 +742,7 @@ mod tests { let tween = Tween::new( EaseFunction::QuadraticInOut, + TweeningType::PingPong, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); @@ -760,6 +757,7 @@ 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 9b13dd1ed17ebb046d81ef6d7f6233625c3b8ed0..67d18c21814c5d4d78d1b46b71dd12349ce8258e 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -2,7 +2,7 @@ use std::time::Duration; use bevy::prelude::*; -use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection}; +use crate::{EaseMethod, Lens, TweeningDirection, TweeningType}; /// The dynamic tweenable type. /// @@ -28,6 +28,7 @@ use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection}; /// # struct MyTweenable; /// # impl Tweenable<Transform> 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<TweenCompleted>) -> TweenState { unimplemented!() } @@ -59,20 +60,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. - /// - /// Note that [`RepeatCount::Infinite`] tweenables never reach this state. + /// time. This can only happen for [`TweeningType::Once`], since other + /// types loop indefinitely. Completed, } /// Event raised when a tween completed. /// -/// 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). +/// 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). /// /// # Note /// @@ -96,94 +97,82 @@ pub struct TweenCompleted { pub user_data: u64, } -#[derive(Debug)] +#[derive(Debug, Default, Clone, Copy)] struct AnimClock { elapsed: Duration, duration: Duration, - times_completed: u32, - total_duration: TotalDuration, - strategy: RepeatStrategy, + is_looping: bool, } impl AnimClock { - fn new(duration: Duration) -> Self { + fn new(duration: Duration, is_looping: bool) -> Self { Self { elapsed: Duration::ZERO, duration, - total_duration: compute_total_duration(duration, RepeatCount::default()), - times_completed: 0, - strategy: RepeatStrategy::default(), + is_looping, } } - fn record_completions(&mut self, times_completed: u32) { - self.times_completed = self.times_completed.saturating_add(times_completed); - } + fn tick(&mut self, duration: Duration) -> u32 { + self.elapsed = self.elapsed.saturating_add(duration); - fn tick(&mut self, tick: Duration) -> u32 { - let duration = self.duration.as_nanos(); + if self.elapsed < self.duration { + 0 + } else if self.is_looping { + let elapsed = self.elapsed.as_nanos(); + let duration = self.duration.as_nanos(); - 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 = Duration::from_nanos((elapsed % duration) as u64); + (elapsed / duration) as u32 + } else { + self.elapsed = self.duration; + 1 } - (self.elapsed.as_nanos() / duration - before) as u32 } fn set_progress(&mut self, progress: f32) { - self.elapsed = self.duration.mul_f32(progress.max(0.)); + let progress = if self.is_looping { + progress.max(0.).fract() + } else { + progress.clamp(0., 1.) + }; + + self.elapsed = self.duration.mul_f32(progress); } fn progress(&self) -> f32 { self.elapsed.as_secs_f32() / self.duration.as_secs_f32() } - 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 completed(&self) -> bool { + self.elapsed >= self.duration } 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<T>: Send + Sync { /// Get the total duration of the animation. /// - /// This is always the duration of a single iteration, even when looping. + /// 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. /// - /// 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. + /// 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. 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. @@ -191,12 +180,20 @@ pub trait Tweenable<T>: Send + Sync { /// [`progress()`]: Tweenable::progress fn set_progress(&mut self, progress: f32); - /// Get the current progress in \[0:1\] of the animation. + /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) + /// of the animation. /// - /// 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`. + /// 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. fn progress(&self) -> f32; /// Tick the animation, advancing it by the given delta time and mutating @@ -226,10 +223,10 @@ pub trait Tweenable<T>: 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 - /// [`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). + /// 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. @@ -273,6 +270,8 @@ pub type CompletedCallback<T> = dyn Fn(Entity, &Tween<T>) + Send + Sync + 'stati pub struct Tween<T> { ease_function: EaseMethod, clock: AnimClock, + times_completed: u32, + tweening_type: TweeningType, direction: TweeningDirection, lens: Box<dyn Lens<T> + Send + Sync + 'static>, on_completed: Option<Box<CompletedCallback<T>>>, @@ -290,6 +289,7 @@ impl<T: 'static> Tween<T> { /// # use std::time::Duration; /// let tween1 = Tween::new( /// EaseFunction::QuadraticInOut, + /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformPositionLens { /// start: Vec3::ZERO, @@ -298,6 +298,7 @@ impl<T: 'static> Tween<T> { /// ); /// let tween2 = Tween::new( /// EaseFunction::QuadraticInOut, + /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformRotationLens { /// start: Quat::IDENTITY, @@ -322,6 +323,7 @@ impl<T> Tween<T> { /// # use std::time::Duration; /// let tween = Tween::new( /// EaseFunction::QuadraticInOut, + /// TweeningType::Once, /// Duration::from_secs_f32(1.0), /// TransformPositionLens { /// start: Vec3::ZERO, @@ -330,13 +332,20 @@ impl<T> Tween<T> { /// ); /// ``` #[must_use] - pub fn new<L>(ease_function: impl Into<EaseMethod>, duration: Duration, lens: L) -> Self + pub fn new<L>( + ease_function: impl Into<EaseMethod>, + tweening_type: TweeningType, + duration: Duration, + lens: L, + ) -> Self where L: Lens<T> + Send + Sync + 'static, { Self { ease_function: ease_function.into(), - clock: AnimClock::new(duration), + clock: AnimClock::new(duration, tweening_type != TweeningType::Once), + times_completed: 0, + tweening_type, direction: TweeningDirection::Forward, lens: Box::new(lens), on_completed: None, @@ -358,6 +367,7 @@ impl<T> Tween<T> { /// let tween = Tween::new( /// // [...] /// # EaseFunction::QuadraticInOut, + /// # TweeningType::Once, /// # Duration::from_secs_f32(1.0), /// # TransformPositionLens { /// # start: Vec3::ZERO, @@ -415,20 +425,6 @@ impl<T> Tween<T> { 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 @@ -473,6 +469,10 @@ impl<T> Tweenable<T> for Tween<T> { 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,17 +488,22 @@ impl<T> Tweenable<T> for Tween<T> { entity: Entity, event_writer: &mut EventWriter<TweenCompleted>, ) -> TweenState { - if self.clock.state() == TweenState::Completed { + if !self.is_looping() && self.clock.completed() { return TweenState::Completed; } // Tick the animation clock let times_completed = self.clock.tick(delta); - self.clock.record_completions(times_completed); - if self.clock.strategy == RepeatStrategy::MirroredRepeat && times_completed & 1 != 0 { + self.times_completed += times_completed; + if times_completed & 1 != 0 && self.tweening_type == TweeningType::PingPong { self.direction = !self.direction; } - let progress = self.progress(); + let state = if self.is_looping() || times_completed == 0 { + TweenState::Active + } else { + TweenState::Completed + }; + let progress = self.clock.progress(); // Apply the lens, even if the animation finished, to ensure the state is // consistent @@ -522,15 +527,16 @@ impl<T> Tweenable<T> for Tween<T> { } } - self.clock.state() + state } fn times_completed(&self) -> u32 { - self.clock.times_completed + self.times_completed } fn rewind(&mut self) { self.clock.reset(); + self.times_completed = 0; } } @@ -617,6 +623,10 @@ 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) { self.times_completed = if progress >= 1. { 1 } else { 0 }; let progress = progress.clamp(0., 1.); // not looping @@ -723,6 +733,10 @@ 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) { self.times_completed = if progress >= 1. { 1 } else { 0 }; // not looping let progress = progress.clamp(0., 1.); // not looping @@ -804,6 +818,10 @@ impl<T> Tweenable<T> 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(); @@ -873,8 +891,7 @@ mod tests { #[test] fn anim_clock_precision() { let duration = Duration::from_millis(1); - let mut clock = AnimClock::new(duration); - clock.total_duration = TotalDuration::Infinite; + let mut clock = AnimClock::new(duration, true); let test_ticks = [ Duration::from_micros(123), @@ -905,29 +922,27 @@ mod tests { #[test] fn tween_tick() { for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] { - 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), + for tweening_type in &[ + TweeningType::Once, + TweeningType::Loop, + TweeningType::PingPong, ] { println!( - "TweeningType: count={count:?} strategy={strategy:?} dir={tweening_direction:?}", + "TweeningType: type={:?} dir={:?}", + tweening_type, 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_repeat_count(*count) - .with_repeat_strategy(*strategy); + .with_direction(*tweening_direction); assert_eq!(tween.direction(), *tweening_direction); assert!(tween.on_completed.is_none()); assert!(tween.event_data.is_none()); @@ -967,8 +982,8 @@ mod tests { for i in 1..=11 { // Calculate expected values let (progress, times_completed, mut direction, expected_state, just_completed) = - match count { - RepeatCount::Finite(1) => { + match tweening_type { + TweeningType::Once => { let progress = (i as f32 * 0.2).min(1.0); let times_completed = if i >= 5 { 1 } else { 0 }; let state = if i < 5 { @@ -985,77 +1000,37 @@ mod tests { 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::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::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, - ) + 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 } 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, - TweenState::Active, - just_completed, - ) - } + 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; @@ -1093,6 +1068,7 @@ 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); @@ -1121,6 +1097,7 @@ 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); @@ -1156,6 +1133,7 @@ mod tests { fn tween_dir() { let mut tween = Tween::new( EaseMethod::Linear, + TweeningType::Once, Duration::from_secs_f32(1.0), TransformPositionLens { start: Vec3::ZERO, @@ -1213,6 +1191,7 @@ mod tests { fn seq_tick() { let tween1 = Tween::new( EaseMethod::Linear, + TweeningType::Once, Duration::from_secs_f32(1.0), TransformPositionLens { start: Vec3::ZERO, @@ -1221,6 +1200,7 @@ mod tests { ); let tween2 = Tween::new( EaseMethod::Linear, + TweeningType::Once, Duration::from_secs_f32(1.0), TransformRotationLens { start: Quat::IDENTITY, @@ -1251,13 +1231,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::ONE, 1e-5)); + 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::Completed); - assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); assert!(transform .rotation .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5)); @@ -1271,6 +1251,7 @@ 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, @@ -1278,6 +1259,7 @@ mod tests { }, ) })); + assert!(!seq.is_looping()); let mut progress = 0.; for i in 1..5 { @@ -1300,6 +1282,7 @@ mod tests { fn tracks_tick() { let tween1 = Tween::new( EaseMethod::Linear, + TweeningType::Once, Duration::from_secs_f32(1.), TransformPositionLens { start: Vec3::ZERO, @@ -1308,6 +1291,7 @@ mod tests { ); let tween2 = Tween::new( EaseMethod::Linear, + TweeningType::Once, Duration::from_secs_f32(0.8), // shorter TransformRotationLens { start: Quat::IDENTITY, @@ -1316,6 +1300,7 @@ 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(); @@ -1347,7 +1332,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::ONE, 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); assert!(transform .rotation .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5)); @@ -1403,6 +1388,7 @@ mod tests { { let tweenable: &dyn Tweenable<Transform> = &delay; assert_eq!(tweenable.duration(), duration); + assert!(!tweenable.is_looping()); assert!(tweenable.progress().abs() < 1e-5); }