From f32c4a2a6cc87862d4c9365f0a65f17a76dc9c65 Mon Sep 17 00:00:00 2001 From: Jerome Humbert <djeedai@gmail.com> Date: Sat, 29 Jan 2022 17:46:29 +0000 Subject: [PATCH] Add tween started/ended callbacks Implement some callbacks invoked when a tween anim starts or ends. --- CHANGELOG | 15 ++ src/lib.rs | 442 ++++++++++++++++++++++++++++++-------------------- src/plugin.rs | 30 +++- 3 files changed, 300 insertions(+), 187 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 828678e..0eb1b62 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add user callbacks on tween started (`Tween::set_started`) and ended (`Tween::set_ended`). +- Implement `Default` for `AnimatorState` as `AnimatorState::Playing`. +- Added `Animator::with_state()` and `AssetAnimator::with_state()`, builder-like functions to override the default `AnimatorState`. +- Added `Tween::is_looping()` returning true for all but `TweeningType::Once`. + +### Changed + +- Moved tween duration out of the `TweeningType` enum, which combined with the removal of the "pause" feature in loops makes it a C-like enum. + +### Removed + +- Removed the "pause" feature in-between loops of `TweeningType::Loop` and `TweeningType::PingPong`, which can be replaced if needed with a sequence including a no-op tween of the desired duration. Removed `Tween::is_paused()`. + ### Fixed - Fix missing public export of `component_animator_system()` and `asset_animator_system()` preventing the animation of all but the built-in items. diff --git a/src/lib.rs b/src/lib.rs index ae6c401..2814648 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,12 +51,11 @@ //! .insert(Animator::new( //! // Use a quadratic easing on both endpoints //! EaseFunction::QuadraticInOut, -//! // Loop animation back and forth over 1 second, with a 0.5 second -//! // pause after each cycle (start -> end -> start). -//! TweeningType::PingPong { -//! duration: Duration::from_secs(1), -//! pause: Some(Duration::from_millis(500)), -//! }, +//! // 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 Sprite, //! // for the Animator to animate it. It also contains the start and //! // end values associated with the animation ratios 0. and 1. @@ -121,39 +120,32 @@ pub use lens::{ }; pub use plugin::{asset_animator_system, component_animator_system, TweeningPlugin}; -/// How should this easing loop repeat -#[derive(Clone, Copy)] +/// Type of looping for a tween animation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TweeningType { - /// Only happen once - Once { - /// duration of the easing - duration: Duration, - }, - /// Looping, restarting from the start once finished - Loop { - /// duration of the easing - duration: Duration, - /// duration of the pause between two loops - pause: Option<Duration>, - }, - /// Repeat the animation back and forth - PingPong { - /// duration of the easing - duration: Duration, - /// duration of the pause before starting again in the other direction - pause: Option<Duration>, - }, + /// Run the animation once from state to end only. + Once, + /// Looping, restarting from the start once finished. + Loop, + /// Repeat the animation back and forth. + PingPong, } /// Playback state of an animator. -#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AnimatorState { - /// The animation is playing. + /// The animation is playing. This is the default state. Playing, /// The animation is paused/stopped. Paused, } +impl Default for AnimatorState { + fn default() -> Self { + AnimatorState::Playing + } +} + impl std::ops::Not for AnimatorState { type Output = AnimatorState; @@ -226,14 +218,24 @@ impl std::ops::Not for TweeningDirection { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TweenState { + /// Not animated. + Stopped, + /// Animating. + Running, +} + /// Single tweening animation instance. pub struct Tween<T> { ease_function: EaseMethod, timer: Timer, - paused: bool, + state: TweenState, tweening_type: TweeningType, direction: TweeningDirection, 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>>, } impl<T> Tween<T> { @@ -241,6 +243,7 @@ impl<T> Tween<T> { pub fn new<L>( ease_function: impl Into<EaseMethod>, tweening_type: TweeningType, + duration: Duration, lens: L, ) -> Self where @@ -248,28 +251,16 @@ impl<T> Tween<T> { { Tween { ease_function: ease_function.into(), - timer: match tweening_type { - TweeningType::Once { duration } => Timer::new(duration, false), - TweeningType::Loop { duration, .. } => Timer::new(duration, false), - TweeningType::PingPong { duration, .. } => Timer::new(duration, false), - }, - paused: false, + timer: Timer::new(duration, tweening_type != TweeningType::Once), + state: TweenState::Stopped, tweening_type, direction: TweeningDirection::Forward, lens: Box::new(lens), + on_started: None, + on_ended: None, } } - /// A boolean indicating whether the animation is currently in the pause phase of a loop. - /// - /// The [`TweeningType::Loop`] and [`TweeningType::PingPong`] tweening types are looping over - /// infinitely, with an optional pause between each loop. This function returns `true` if the - /// animation is currently under such pause. For [`TweeningType::Once`], which has no pause, - /// this always returns `false`. - pub fn is_paused(&self) -> bool { - self.paused - } - /// The current animation direction. /// /// See [`TweeningDirection`] for details. @@ -289,64 +280,80 @@ impl<T> Tween<T> { } } + /// Set a callback invoked when the animation starts. + pub fn set_started<C>(&mut self, callback: C) + where + C: FnMut() + Send + Sync + 'static, + { + self.on_started = 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 ends. + pub fn clear_ended(&mut self) { + self.on_ended = None; + } + + /// Is the animation playback looping? + pub fn is_looping(&self) -> bool { + self.tweening_type != TweeningType::Once + } + fn tick(&mut self, delta: Duration, target: &mut T) { - self.timer.tick(delta); - if self.paused { - if self.timer.just_finished() { - match self.tweening_type { - TweeningType::Once { duration } => { - self.timer.set_duration(duration); - } - TweeningType::Loop { duration, .. } => { - self.timer.set_duration(duration); - } - TweeningType::PingPong { duration, .. } => { - self.timer.set_duration(duration); - } - } - self.timer.reset(); - self.paused = false; - } - } else { - if self.timer.duration().as_secs_f32() != 0. { - let progress = self.progress(); - let factor = self.ease_function.sample(progress); - self.apply(target, factor); + 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.timer.finished() { - match self.tweening_type { - TweeningType::Once { .. } => { - //commands.entity(entity).remove::<Animator>(); - } - TweeningType::Loop { pause, .. } => { - if let Some(pause) = pause { - self.timer.set_duration(pause); - self.paused = true; - } - self.timer.reset(); - } - TweeningType::PingPong { pause, .. } => { - if let Some(pause) = pause { - self.timer.set_duration(pause); - self.paused = true; - } - self.timer.reset(); - self.direction = !self.direction; - } + } + + self.timer.tick(delta); + + // Toggle direction immediately, so self.progress() returns the correct ratio + if self.timer.just_finished() && self.tweening_type == TweeningType::PingPong { + self.direction = !self.direction; + } + + let progress = self.progress(); + let factor = self.ease_function.sample(progress); + self.lens.lerp(target, factor); + + if self.timer.just_finished() { + // 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(); } } } } - #[inline(always)] - fn apply(&mut self, target: &mut T, ratio: f32) { - self.lens.lerp(target, ratio); + fn stop(&mut self) { + if self.state == TweenState::Running { + self.state = TweenState::Stopped; + self.timer.reset(); + } } } struct Sequence<T> { tweens: Vec<Tween<T>>, index: usize, + state: TweenState, } impl<T> Sequence<T> { @@ -357,6 +364,7 @@ impl<T> Sequence<T> { Sequence { tweens: tweens.into_iter().collect(), index: 0, + state: TweenState::Stopped, } } @@ -364,8 +372,10 @@ impl<T> Sequence<T> { Sequence { tweens: vec![tween], index: 0, + state: TweenState::Stopped, } } + fn tick(&mut self, delta: Duration, target: &mut T) { if self.index < self.tweens.len() { let tween = &mut self.tweens[self.index]; @@ -375,6 +385,16 @@ impl<T> Sequence<T> { } } } + + fn stop(&mut self) { + if self.state == TweenState::Running { + self.state = TweenState::Stopped; + if self.index < self.tweens.len() { + let tween = &mut self.tweens[self.index]; + tween.stop(); + } + } + } } struct Tracks<T> { @@ -386,6 +406,7 @@ struct Tracks<T> { pub struct Animator<T: Component> { /// Control if this animation is played or not. pub state: AnimatorState, + prev_state: AnimatorState, tracks: Tracks<T>, } @@ -404,14 +425,16 @@ impl<T: Component> Animator<T> { pub fn new<L>( ease_function: impl Into<EaseMethod>, tweening_type: TweeningType, + duration: Duration, lens: L, ) -> Self where L: Lens<T> + Send + Sync + 'static, { - let tween = Tween::new(ease_function, tweening_type, lens); + let tween = Tween::new(ease_function, tweening_type, duration, lens); Animator { - state: AnimatorState::Playing, + state: AnimatorState::default(), + prev_state: AnimatorState::default(), tracks: Tracks { tracks: vec![Sequence::from_single(tween)], }, @@ -421,7 +444,8 @@ impl<T: Component> Animator<T> { /// Create a new animator component from a single tween instance. pub fn new_single(tween: Tween<T>) -> Self { Animator { - state: AnimatorState::Playing, + state: AnimatorState::default(), + prev_state: AnimatorState::default(), tracks: Tracks { tracks: vec![Sequence::from_single(tween)], }, @@ -436,12 +460,20 @@ impl<T: Component> Animator<T> { } Animator { state: AnimatorState::Playing, + prev_state: AnimatorState::Playing, tracks: Tracks { tracks: vec![Sequence::new(tweens)], }, } } + /// Set the initial state of the animator. + pub fn with_state(mut self, state: AnimatorState) -> Self { + self.state = state; + self.prev_state = state; + self + } + #[allow(dead_code)] fn tracks(&self) -> &Tracks<T> { &self.tracks @@ -457,6 +489,7 @@ impl<T: Component> Animator<T> { pub struct AssetAnimator<T: Asset> { /// Control if this animation is played or not. pub state: AnimatorState, + prev_state: AnimatorState, tracks: Tracks<T>, handle: Handle<T>, } @@ -477,14 +510,16 @@ impl<T: Asset> AssetAnimator<T> { handle: Handle<T>, ease_function: impl Into<EaseMethod>, tweening_type: TweeningType, + duration: Duration, lens: L, ) -> Self where L: Lens<T> + Send + Sync + 'static, { - let tween = Tween::new(ease_function, tweening_type, lens); + let tween = Tween::new(ease_function, tweening_type, duration, lens); AssetAnimator { state: AnimatorState::Playing, + prev_state: AnimatorState::Playing, tracks: Tracks { tracks: vec![Sequence::from_single(tween)], }, @@ -496,6 +531,7 @@ impl<T: Asset> AssetAnimator<T> { pub fn new_single(handle: Handle<T>, tween: Tween<T>) -> Self { AssetAnimator { state: AnimatorState::Playing, + prev_state: AnimatorState::Playing, tracks: Tracks { tracks: vec![Sequence::from_single(tween)], }, @@ -511,6 +547,7 @@ impl<T: Asset> AssetAnimator<T> { } AssetAnimator { state: AnimatorState::Playing, + prev_state: AnimatorState::Playing, tracks: Tracks { tracks: vec![Sequence::new(tweens)], }, @@ -518,6 +555,13 @@ impl<T: Asset> AssetAnimator<T> { } } + /// Set the initial state of the animator. + pub fn with_state(mut self, state: AnimatorState) -> Self { + self.state = state; + self.prev_state = state; + self + } + fn handle(&self) -> Handle<T> { self.handle.clone() } @@ -535,137 +579,179 @@ impl<T: Asset> AssetAnimator<T> { #[cfg(test)] mod tests { use super::*; + use std::sync::{Arc, Mutex}; + + /// Utility to compare floating-point values with a tolerance. + fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool { + (a - b).abs() < tol + } + + /// Test ticking of a single tween in isolation. #[test] fn tween_tick() { - let mut tween = Tween { - ease_function: EaseMethod::Linear, - timer: Timer::from_seconds(1.0, false), - paused: false, - tweening_type: TweeningType::Once { - duration: Duration::from_secs_f32(1.0), - }, - direction: TweeningDirection::Forward, - lens: Box::new(TransformPositionLens { - start: Vec3::ZERO, - end: Vec3::ONE, - }), - }; - let mut transform = Transform::default(); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2))); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(0.4))); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(0.6))); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(0.8))); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); - tween.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); + for tweening_type in &[ + TweeningType::Once, + TweeningType::Loop, + TweeningType::PingPong, + ] { + // 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, + }, + ); + + // 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; + }); + assert_eq!(*started_count.lock().unwrap(), 0); + assert_eq!(*ended_count.lock().unwrap(), 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) = match tweening_type { + TweeningType::Once => { + let r = (i as f32 * 0.2).min(1.0); + let ec = if i >= 5 { 1 } else { 0 }; + (r, ec, TweeningDirection::Forward) + } + TweeningType::Loop => { + let r = (i as f32 * 0.2).fract(); + let ec = i / 5; + (r, ec, TweeningDirection::Forward) + } + TweeningType::PingPong => { + let i10 = i % 10; + let r = if i10 >= 5 { + (10 - i10) as f32 * 0.2 + } else { + i10 as f32 * 0.2 + }; + let ec = i / 10; + let dir = if i10 >= 5 { + TweeningDirection::Backward + } else { + TweeningDirection::Forward + }; + (r, ec, dir) + } + }; + + // Tick the tween + tween.tick(tick_duration, &mut transform); + + // Check actual values + assert_eq!(tween.direction(), dir); + assert!(abs_diff_eq(tween.progress(), ratio, 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::splat(ratio), 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); + } + } } + /// Test ticking a sequence of tweens. #[test] fn seq_tick() { - let tween1 = Tween { - ease_function: EaseMethod::Linear, - timer: Timer::from_seconds(1.0, false), - paused: false, - tweening_type: TweeningType::Once { - duration: Duration::from_secs_f32(1.0), - }, - direction: TweeningDirection::Forward, - lens: Box::new(TransformPositionLens { + let tween1 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformPositionLens { start: Vec3::ZERO, end: Vec3::ONE, - }), - }; - let tween2 = Tween { - ease_function: EaseMethod::Linear, - timer: Timer::from_seconds(1.0, false), - paused: false, - tweening_type: TweeningType::Once { - duration: Duration::from_secs_f32(1.0), }, - direction: TweeningDirection::Forward, - lens: Box::new(TransformRotationLens { + ); + let tween2 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformRotationLens { start: Quat::IDENTITY, end: Quat::from_rotation_x(180_f32.to_radians()), - }), - }; + }, + ); let mut seq = Sequence::new([tween1, tween2]); let mut transform = Transform::default(); - // First, translation alone (0->1) - seq.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2))); - seq.tick(Duration::from_secs_f32(0.8), &mut transform); - assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); - // Then, rotation alone, on top of final translation (1->2) - seq.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform.translation, Vec3::splat(1.0)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(36_f32.to_radians()), 1e-5)); - seq.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform.translation, Vec3::splat(1.0)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(72_f32.to_radians()), 1e-5)); - seq.tick(Duration::from_secs_f32(0.6), &mut transform); - assert_eq!(transform.translation, Vec3::splat(1.0)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); - seq.tick(Duration::from_secs_f32(0.2), &mut transform); - assert_eq!(transform.translation, Vec3::splat(1.0)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + for i in 1..=11 { + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + if i <= 5 { + let r = i as f32 * 0.2; + assert_eq!(transform, Transform::from_translation(Vec3::splat(r))); + } else if i <= 10 { + 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!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + } + } } + /// Animator::new() #[test] fn animator_new() { let animator = Animator::new( EaseFunction::QuadraticInOut, - TweeningType::PingPong { - duration: std::time::Duration::from_secs(1), - pause: Some(std::time::Duration::from_millis(500)), - }, + TweeningType::PingPong, + std::time::Duration::from_secs(1), TransformRotationLens { start: Quat::IDENTITY, end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), }, ); + assert_eq!(animator.state, AnimatorState::default()); let tracks = animator.tracks(); assert_eq!(tracks.tracks.len(), 1); let seq = &tracks.tracks[0]; assert_eq!(seq.tweens.len(), 1); let tween = &seq.tweens[0]; - assert_eq!(tween.is_paused(), false); assert_eq!(tween.direction(), TweeningDirection::Forward); assert_eq!(tween.progress(), 0.); } + /// AssetAnimator::new() #[test] fn asset_animator_new() { let animator = AssetAnimator::new( Handle::<ColorMaterial>::default(), EaseFunction::QuadraticInOut, - TweeningType::PingPong { - duration: std::time::Duration::from_secs(1), - pause: Some(std::time::Duration::from_millis(500)), - }, + TweeningType::PingPong, + std::time::Duration::from_secs(1), ColorMaterialColorLens { start: Color::RED, end: Color::BLUE, }, ); + assert_eq!(animator.state, AnimatorState::default()); let tracks = animator.tracks(); assert_eq!(tracks.tracks.len(), 1); let seq = &tracks.tracks[0]; assert_eq!(seq.tweens.len(), 1); let tween = &seq.tweens[0]; - assert_eq!(tween.is_paused(), false); assert_eq!(tween.direction(), TweeningDirection::Forward); assert_eq!(tween.progress(), 0.); } diff --git a/src/plugin.rs b/src/plugin.rs index 985af7f..790329f 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -19,7 +19,7 @@ use crate::{Animator, AnimatorState, AssetAnimator}; /// added manually by the application: /// - For components, add [`component_animator_system::<T>`] where `T: Component` /// - For assets, add [`asset_animator_system::<T>`] where `T: Asset` -/// +/// /// This plugin is entirely optional. If you want more control, you can instead add manually /// the relevant systems for the exact set of components and assets actually animated. /// @@ -50,12 +50,19 @@ 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 { - continue; - } - // Play all tracks in parallel - for seq in &mut animator.tracks_mut().tracks { - seq.tick(time.delta(), target); + if state_changed { + for seq in &mut animator.tracks_mut().tracks { + seq.stop(); + } + } + } else { + // Play all tracks in parallel + for seq in &mut animator.tracks_mut().tracks { + seq.tick(time.delta(), target); + } } } } @@ -69,10 +76,15 @@ 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 { - continue; - } - if let Some(target) = assets.get_mut(animator.handle()) { + if state_changed { + for seq in &mut animator.tracks_mut().tracks { + seq.stop(); + } + } + } else if let Some(target) = assets.get_mut(animator.handle()) { // Play all tracks in parallel for seq in &mut animator.tracks_mut().tracks { seq.tick(time.delta(), target); -- GitLab