diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a086427b08300116f551257ccfeca07d932614..ce467c56fd61253a0b5575f578fcd579318dbe77 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 + +- 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. + ## [0.4.0] - 2022-04-16 ### Changed diff --git a/src/lib.rs b/src/lib.rs index 57b86e9a40af675d6b067b2291f497f366038658..75624386088df1d65903310cecde4dff2f0bb057 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,17 +250,37 @@ impl From<EaseFunction> for EaseMethod { /// Direction a tweening animation is playing. /// -/// For all but [`TweeningType::PingPong`] this is always [`TweeningDirection::Forward`]. For the -/// [`TweeningType::PingPong`] tweening type, this is either forward (from start to end; ping) or -/// backward (from end to start; pong). +/// When playing a tweenable forward, the progress values `0` and `1` are respectively mapped to +/// the start and end bounds of the lens(es) being used. Conversely, when playing backward, this +/// mapping is reversed, such that a progress value of `0` corresponds to the state of the target +/// at the end bound of the lens, while a progress value of `1` corresponds to the state of that +/// target at the start bound of the lens, effectively making the animation play backward. +/// +/// 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. 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. Forward, - /// Animation playing from end to start. + /// Animation playing from end to start, in reverse. Backward, } +impl TweeningDirection { + /// Is the direction equal to [`TweeningDirection::Forward`]? + pub fn is_forward(&self) -> bool { + *self == TweeningDirection::Forward + } + + /// Is the direction equal to [`TweeningDirection::Backward`]? + pub fn is_backward(&self) -> bool { + *self == TweeningDirection::Backward + } +} + impl Default for TweeningDirection { fn default() -> Self { TweeningDirection::Forward diff --git a/src/tweenable.rs b/src/tweenable.rs index 5e2f8fee72022e0d95ad3db7d7d40c535ccc5362..abd377d6886d2022729fd5919b2642bbb0c73afc 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -42,16 +42,86 @@ pub struct TweenCompleted { pub user_data: u64, } +#[derive(Debug, Default, Clone, Copy)] +struct AnimClock { + elapsed: Duration, + total: Duration, + is_looping: bool, +} + +impl AnimClock { + fn new(duration: Duration, is_looping: bool) -> Self { + AnimClock { + elapsed: Duration::ZERO, + total: duration, + is_looping, + } + } + + #[allow(dead_code)] // TEMP + fn elapsed(&self) -> Duration { + self.elapsed + } + + fn total(&self) -> Duration { + self.total + } + + fn tick(&mut self, duration: Duration) -> u32 { + let new_elapsed = self.elapsed.saturating_add(duration); + let progress = new_elapsed.as_secs_f64() / self.total.as_secs_f64(); + let times_completed = progress as u32; + let progress = if self.is_looping { + progress.fract() + } else { + progress.min(1.) + }; + self.elapsed = self.total.mul_f64(progress); + times_completed + } + + fn set_progress(&mut self, progress: f32) -> u32 { + let progress = progress.max(0.); + let times_completed = progress as u32; + let progress = if self.is_looping { + progress.fract() + } else { + progress.min(1.) + }; + self.elapsed = self.total.mul_f32(progress); + times_completed + } + + fn progress(&self) -> f32 { + //self.elapsed.div_duration_f32(self.total) // TODO: unstable + (self.elapsed.as_secs_f64() / self.total.as_secs_f64()) as f32 + } + + fn completed(&self) -> bool { + self.elapsed >= self.total + } + + fn reset(&mut self) { + self.elapsed = Duration::ZERO; + } +} + /// 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. /// - /// 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 in this - /// case is the double of the returned value. + /// 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 [`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. @@ -63,19 +133,19 @@ pub trait Tweenable<T>: Send + Sync { /// 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, + /// 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 [`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 the given target component or asset. /// - /// This returns [`TweenState::Active`] if the tweenable didn't reach its final state yet (progress < 1.), + /// This returns [`TweenState::Active`] if the tweenable didn't reach its final state yet (progress < `1.0`), /// or [`TweenState::Completed`] if the tweenable completed this tick. Only non-looping tweenables return /// a completed state, since looping ones continue forever. /// @@ -101,6 +171,9 @@ pub trait Tweenable<T>: Send + Sync { fn times_completed(&self) -> u32; /// Rewind the animation to its starting state. + /// + /// Note that the starting state depends on the current direction. For [`TweeningDirection::Forward`] + /// this is the start point of the lens, whereas for [`TweeningDirection::Backward`] this is the end one. fn rewind(&mut self); } @@ -130,7 +203,7 @@ impl<T> Tweenable<T> for Box<dyn Tweenable<T> + Send + Sync + 'static> { self.as_ref().times_completed() } fn rewind(&mut self) { - self.as_mut().rewind() + self.as_mut().rewind(); } } @@ -154,10 +227,10 @@ pub type CompletedCallback<T> = dyn Fn(Entity, &Tween<T>) + Send + Sync + 'stati /// Single tweening animation instance. pub struct Tween<T> { ease_function: EaseMethod, - timer: Timer, + clock: AnimClock, + times_completed: u32, tweening_type: TweeningType, direction: TweeningDirection, - times_completed: u32, lens: Box<dyn Lens<T> + Send + Sync + 'static>, on_completed: Option<Box<CompletedCallback<T>>>, event_data: Option<u64>, @@ -225,10 +298,10 @@ impl<T> Tween<T> { { Tween { ease_function: ease_function.into(), - timer: Timer::new(duration, tweening_type != TweeningType::Once), + clock: AnimClock::new(duration, tweening_type != TweeningType::Once), + times_completed: 0, tweening_type, direction: TweeningDirection::Forward, - times_completed: 0, lens: Box::new(lens), on_completed: None, event_data: None, @@ -271,6 +344,29 @@ impl<T> Tween<T> { self } + /// Set the playback direction of the tween. + /// + /// The playback direction influences the mapping of the progress ratio (in \[0:1\]) to the + /// actual ratio passed to the lens. [`TweeningDirection::Forward`] maps the `0` value of + /// progress to the `0` value of the lens ratio. Conversely, [`TweeningDirection::Backward`] + /// reverses the mapping, which effectively makes the tween play reversed, going from end to + /// start. + /// + /// Changing the direction doesn't change any target state, nor any progress of the tween. Only + /// the direction of animation from this moment potentially changes. To force a target state + /// change, call [`Tweenable::tick()`] with a zero delta (`Duration::ZERO`). + pub fn set_direction(&mut self, direction: TweeningDirection) { + self.direction = direction; + } + + /// Set the playback direction of the tween. + /// + /// See [`Tween::set_direction()`]. + pub fn with_direction(mut self, direction: TweeningDirection) -> Self { + self.direction = direction; + self + } + /// The current animation direction. /// /// See [`TweeningDirection`] for details. @@ -312,7 +408,7 @@ impl<T> Tween<T> { impl<T> Tweenable<T> for Tween<T> { fn duration(&self) -> Duration { - self.timer.duration() + self.clock.total() } fn is_looping(&self) -> bool { @@ -320,20 +416,11 @@ impl<T> Tweenable<T> for Tween<T> { } fn set_progress(&mut self, progress: f32) { - // need to reset() to clear finished() unfortunately - self.timer.reset(); - self.timer.set_elapsed(Duration::from_secs_f64( - self.timer.duration().as_secs_f64() * progress as f64, - )); - // set_elapsed() does not update finished() etc. which we rely on - self.timer.tick(Duration::ZERO); + self.clock.set_progress(progress); } fn progress(&self) -> f32 { - match self.direction { - TweeningDirection::Forward => self.timer.percent(), - TweeningDirection::Backward => self.timer.percent_left(), - } + self.clock.progress() } fn tick( @@ -343,33 +430,33 @@ impl<T> Tweenable<T> for Tween<T> { entity: Entity, event_writer: &mut EventWriter<TweenCompleted>, ) -> TweenState { - if !self.is_looping() && self.timer.finished() { + if !self.is_looping() && self.clock.completed() { return TweenState::Completed; } - let mut state = TweenState::Active; - - // Tick the timer to update the animation time - self.timer.tick(delta); - - // Toggle direction immediately, so self.progress() returns the correct ratio - if self.timer.just_finished() && self.tweening_type == TweeningType::PingPong { + // 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.direction = !self.direction; } + 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 - let progress = self.progress(); - let factor = self.ease_function.sample(progress); + let mut factor = progress; + if self.direction.is_backward() { + factor = 1. - factor; + } + let factor = self.ease_function.sample(factor); self.lens.lerp(target, factor); - if self.timer.just_finished() { - if self.tweening_type == TweeningType::Once { - state = TweenState::Completed; - } - - // Timer::times_finished() returns the number of finished times since last tick only - self.times_completed += self.timer.times_finished(); - + // If completed at least once this frame, notify the user + if times_completed > 0 { if let Some(user_data) = &self.event_data { event_writer.send(TweenCompleted { entity, @@ -389,7 +476,7 @@ impl<T> Tweenable<T> for Tween<T> { } fn rewind(&mut self) { - self.timer.reset(); + self.clock.reset(); self.times_completed = 0; } } @@ -713,191 +800,271 @@ mod tests { /// Test ticking of a single tween in isolation. #[test] fn tween_tick() { - for tweening_type in &[ - TweeningType::Once, - TweeningType::Loop, - TweeningType::PingPong, - ] { - println!("TweeningType: {:?}", tweening_type); - - // 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, - }, - ); - assert!(tween.on_completed.is_none()); - assert!(tween.event_data.is_none()); - - let dummy_entity = Entity::from_raw(42); - - // Register callbacks to count started/ended events - let callback_monitor = Arc::new(Mutex::new(CallbackMonitor::default())); - let cb_mon_ptr = Arc::clone(&callback_monitor); - tween.set_completed(move |entity, tween| { - assert_eq!(dummy_entity, entity); - let mut cb_mon = cb_mon_ptr.lock().unwrap(); - cb_mon.invoke_count += 1; - cb_mon.last_reported_count = tween.times_completed(); - }); - assert!(tween.on_completed.is_some()); - assert!(tween.event_data.is_none()); - assert_eq!(callback_monitor.lock().unwrap().invoke_count, 0); - - // Activate event sending - const USER_DATA: u64 = 54789; // dummy - tween.set_completed_event(true, USER_DATA); - assert!(tween.event_data.is_some()); - assert_eq!(tween.event_data.unwrap(), USER_DATA); - - // Dummy world and event writer - let mut world = World::new(); - world.insert_resource(Events::<TweenCompleted>::default()); - let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> = - SystemState::new(&mut world); - let mut event_reader_system_state: SystemState<EventReader<TweenCompleted>> = - SystemState::new(&mut world); - - // 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 (progress, times_completed, direction, expected_state, just_completed) = - 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 { - TweenState::Active - } else { - TweenState::Completed - }; - let just_completed = i == 5; - ( - progress, - times_completed, - TweeningDirection::Forward, - state, - 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, - ) - } - TweeningType::PingPong => { - let i10 = i % 10; - let progress = if i10 >= 5 { - (10 - i10) as f32 * 0.2 - } else { - i10 as f32 * 0.2 - }; - let times_completed = i / 5; - let direction = if i10 >= 5 { - TweeningDirection::Backward - } else { - TweeningDirection::Forward - }; - let just_completed = i % 5 == 0; - ( - progress, - times_completed, - direction, - TweenState::Active, - just_completed, - ) - } - }; + for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] { + for tweening_type in &[ + TweeningType::Once, + TweeningType::Loop, + TweeningType::PingPong, + ] { println!( - "Expected: progress={} times_completed={} direction={:?} state={:?} just_completed={}", - progress, times_completed, direction, expected_state, just_completed + "TweeningType: type={:?} dir={:?}", + tweening_type, tweening_direction ); - // Tick the tween + // 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); + assert_eq!(tween.direction(), *tweening_direction); + assert!(tween.on_completed.is_none()); + assert!(tween.event_data.is_none()); + + let dummy_entity = Entity::from_raw(42); + + // Register callbacks to count started/ended events + let callback_monitor = Arc::new(Mutex::new(CallbackMonitor::default())); + let cb_mon_ptr = Arc::clone(&callback_monitor); + tween.set_completed(move |entity, tween| { + assert_eq!(dummy_entity, entity); + let mut cb_mon = cb_mon_ptr.lock().unwrap(); + cb_mon.invoke_count += 1; + cb_mon.last_reported_count = tween.times_completed(); + }); + assert!(tween.on_completed.is_some()); + assert!(tween.event_data.is_none()); + assert_eq!(callback_monitor.lock().unwrap().invoke_count, 0); + + // Activate event sending + const USER_DATA: u64 = 54789; // dummy + tween.set_completed_event(true, USER_DATA); + assert!(tween.event_data.is_some()); + assert_eq!(tween.event_data.unwrap(), USER_DATA); + + // Dummy world and event writer + let mut world = World::new(); + world.insert_resource(Events::<TweenCompleted>::default()); + let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> = + SystemState::new(&mut world); + let mut event_reader_system_state: SystemState<EventReader<TweenCompleted>> = + SystemState::new(&mut world); + + // 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 (progress, times_completed, mut direction, expected_state, just_completed) = + 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 { + TweenState::Active + } else { + TweenState::Completed + }; + let just_completed = i == 5; + ( + progress, + times_completed, + TweeningDirection::Forward, + state, + 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, + ) + } + 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 { + TweeningDirection::Forward + }; + let just_completed = i5 == 0; + ( + progress, + times_completed, + direction, + TweenState::Active, + just_completed, + ) + } + }; + let factor = if tweening_direction.is_backward() { + direction = !direction; + 1. - progress + } else { + progress + }; + let expected_translation = if direction.is_forward() { + Vec3::splat(progress) + } else { + Vec3::splat(1. - progress) + }; + println!( + "Expected: progress={} factor={} times_completed={} direction={:?} state={:?} just_completed={} translation={:?}", + progress, factor, times_completed, direction, expected_state, just_completed, expected_translation + ); + + // Tick the tween + let actual_state = { + let mut event_writer = event_writer_system_state.get_mut(&mut world); + tween.tick( + tick_duration, + &mut transform, + dummy_entity, + &mut event_writer, + ) + }; + + // Propagate events + { + let mut events = + world.get_resource_mut::<Events<TweenCompleted>>().unwrap(); + events.update(); + } + + // 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); + assert!(transform + .translation + .abs_diff_eq(expected_translation, 1e-5)); + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + let cb_mon = callback_monitor.lock().unwrap(); + assert_eq!(cb_mon.invoke_count, times_completed as u64); + assert_eq!(cb_mon.last_reported_count, times_completed); + { + let mut event_reader = event_reader_system_state.get_mut(&mut world); + let event = event_reader.iter().next(); + if just_completed { + assert!(event.is_some()); + if let Some(event) = event { + assert_eq!(event.entity, dummy_entity); + assert_eq!(event.user_data, USER_DATA); + } + } else { + assert!(event.is_none()); + } + } + } + + // 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); + + // Dummy tick to update target let actual_state = { let mut event_writer = event_writer_system_state.get_mut(&mut world); tween.tick( - tick_duration, + Duration::ZERO, &mut transform, - dummy_entity, + Entity::from_raw(0), &mut event_writer, ) }; - - // Propagate events - { - let mut events = world.get_resource_mut::<Events<TweenCompleted>>().unwrap(); - events.update(); - } - - // 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); + assert_eq!(actual_state, TweenState::Active); + let expected_translation = if tweening_direction.is_backward() { + Vec3::ONE + } else { + Vec3::ZERO + }; assert!(transform .translation - .abs_diff_eq(Vec3::splat(progress), 1e-5)); + .abs_diff_eq(expected_translation, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); - let cb_mon = callback_monitor.lock().unwrap(); - assert_eq!(cb_mon.invoke_count, times_completed as u64); - assert_eq!(cb_mon.last_reported_count, times_completed); - { - let mut event_reader = event_reader_system_state.get_mut(&mut world); - let event = event_reader.iter().next(); - if just_completed { - assert!(event.is_some()); - if let Some(event) = event { - assert_eq!(event.entity, dummy_entity); - assert_eq!(event.user_data, USER_DATA); - } - } else { - assert!(event.is_none()); - } - } - } - // Rewind - tween.rewind(); - assert_eq!(tween.direction(), TweeningDirection::Forward); - assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once); - assert!(abs_diff_eq(tween.progress(), 0., 1e-5)); - assert_eq!(tween.times_completed(), 0); - - // Dummy tick to update target - let actual_state = { - let mut event_writer = event_writer_system_state.get_mut(&mut world); - tween.tick( - Duration::ZERO, - &mut transform, - Entity::from_raw(0), - &mut event_writer, - ) - }; - assert_eq!(actual_state, TweenState::Active); - assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); - assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); - - // Clear callback - tween.clear_completed(); - assert!(tween.on_completed.is_none()); + // Clear callback + tween.clear_completed(); + assert!(tween.on_completed.is_none()); + } } } + #[test] + fn tween_dir() { + let mut tween = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }, + ); + + // Default + assert_eq!(tween.direction(), TweeningDirection::Forward); + assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5)); + + // no-op + tween.set_direction(TweeningDirection::Forward); + assert_eq!(tween.direction(), TweeningDirection::Forward); + assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5)); + + // Backward + tween.set_direction(TweeningDirection::Backward); + assert_eq!(tween.direction(), TweeningDirection::Backward); + // progress is independent of direction + assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5)); + + // Progress-invariant + tween.set_direction(TweeningDirection::Forward); + tween.set_progress(0.3); + assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5)); + tween.set_direction(TweeningDirection::Backward); + // progress is independent of direction + assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5)); + + // Dummy world and event writer + let mut world = World::new(); + world.insert_resource(Events::<TweenCompleted>::default()); + let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> = + SystemState::new(&mut world); + + // Progress always increases alongside the current direction + let dummy_entity = Entity::from_raw(0); + let mut transform = Transform::default(); + let mut event_writer = event_writer_system_state.get_mut(&mut world); + tween.set_direction(TweeningDirection::Backward); + assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5)); + tween.tick( + Duration::from_secs_f32(0.1), + &mut transform, + dummy_entity, + &mut event_writer, + ); + assert!(abs_diff_eq(tween.progress(), 0.4, 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::splat(0.6), 1e-5)); + } + /// Test ticking a sequence of tweens. #[test] fn seq_tick() {