Newer
Older
use bevy::prelude::*;
use std::time::Duration;
use crate::{EaseMethod, Lens, TweeningDirection, TweeningType};
/// Playback state of a [`Tweenable`].
///
/// This is returned by [`Tweenable::tick()`] to allow the caller to execute some logic based on the
/// updated state of the tweenable, like advanding a sequence to its next child tweenable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TweenState {
/// The tweenable is still active, and did not reach its end state yet.
Active,
/// Animation reached its end state. The tweenable is idling at its latest time. This can only happen
/// for [`TweeningType::Once`], since other types loop indefinitely.
Completed,
}
///
/// 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
///
/// The semantic is slightly different from [`TweenState::Completed`], which indicates that the tweenable
/// has finished ticking and do not need to be updated anymore, a state which is never reached for looping
/// animation. Here the [`TweenCompleted`] event instead marks the end of a single loop iteration.
#[derive(Copy, Clone)]
pub struct TweenCompleted {
/// The [`Entity`] the tween which completed and its animator are attached to.
pub entity: Entity,
/// An opaque value set by the user when activating event raising, used to identify the particular
/// tween which raised this event. The value is passed unmodified from a call to [`with_completed_event()`]
/// or [`set_completed_event()`].
///
/// [`with_completed_event()`]: Tween::with_completed_event
/// [`set_completed_event()`]: Tween::set_completed_event
pub user_data: u64,
#[derive(Debug, Default, Clone, Copy)]
struct AnimClock {
elapsed: Duration,
duration: Duration,
is_looping: bool,
}
impl AnimClock {
fn new(duration: Duration, is_looping: bool) -> Self {
AnimClock {
elapsed: Duration::ZERO,
is_looping,
}
}
fn tick(&mut self, duration: Duration) -> u32 {
self.elapsed = self.elapsed.saturating_add(duration);
if self.elapsed < self.duration {
0
} else if self.is_looping {
let elapsed = self.elapsed.as_nanos();
let duration = self.duration.as_nanos();
self.elapsed = Duration::from_nanos((elapsed % duration) as u64);
(elapsed / duration) as u32
self.elapsed = self.duration;
1
}
fn set_progress(&mut self, progress: f32) {
let progress = if self.is_looping {
progress.max(0.).fract()
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 completed(&self) -> bool {
self.elapsed >= self.duration
}
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 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.
/// 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.
///
/// [`progress()`]: Tweenable::progress
fn set_progress(&mut self, progress: f32);
/// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) of the animation.
///
/// For looping animations, this reports the progress of the current iteration, 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.
/// 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.0`),
/// or [`TweenState::Completed`] if the tweenable completed this tick. Only non-looping tweenables return
/// a completed state, since looping ones continue forever.
///
/// Calling this method with a duration of [`Duration::ZERO`] is valid, and updates the target to the current
/// state of the tweenable without actually modifying the tweenable state. This is useful after certain operations
/// like [`rewind()`] or [`set_progress()`] whose effect is otherwise only visible on target on next frame.
/// [`rewind()`]: Tweenable::rewind
/// [`set_progress()`]: Tweenable::set_progress
fn tick(
&mut self,
delta: Duration,
target: &mut T,
entity: Entity,
event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState;
/// Get the number of times this tweenable completed.
///
/// For looping animations, this returns the number of times a single playback was completed. In the
/// case of [`TweeningType::PingPong`] this corresponds to a playback in a single direction, so tweening
/// from start to end and back to start counts as two completed times (one forward, one backward).
fn times_completed(&self) -> u32;
/// Rewind the animation to its starting state.
///
/// 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.
}
impl<T> Tweenable<T> for Box<dyn Tweenable<T> + Send + Sync + 'static> {
fn duration(&self) -> Duration {
self.as_ref().duration()
}
fn is_looping(&self) -> bool {
self.as_ref().is_looping()
}
fn set_progress(&mut self, progress: f32) {
self.as_mut().set_progress(progress);
}
fn progress(&self) -> f32 {
self.as_ref().progress()
}
fn tick(
&mut self,
delta: Duration,
target: &mut T,
entity: Entity,
event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState {
self.as_mut().tick(delta, target, entity, event_writer)
fn times_completed(&self) -> u32 {
self.as_ref().times_completed()
}
fn rewind(&mut self) {
self.as_mut().rewind();
}
}
/// Trait for boxing a [`Tweenable`] trait object.
pub trait IntoBoxDynTweenable<T> {
/// Convert the current object into a boxed [`Tweenable`].
fn into_box_dyn(this: Self) -> Box<dyn Tweenable<T> + Send + Sync + 'static>;
}
impl<T, U: Tweenable<T> + Send + Sync + 'static> IntoBoxDynTweenable<T> for U {
fn into_box_dyn(this: U) -> Box<dyn Tweenable<T> + Send + Sync + 'static> {
Box::new(this)
}
}
/// Type of a callback invoked when a [`Tween`] has completed.
///
/// See [`Tween::set_completed()`] for usage.
pub type CompletedCallback<T> = dyn Fn(Entity, &Tween<T>) + Send + Sync + 'static;
/// Single tweening animation instance.
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>,
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/// Chain another [`Tweenable`] after this tween, making a [`Sequence`] with the two.
///
/// # Example
/// ```
/// # use bevy_tweening::{lens::*, *};
/// # use bevy::math::*;
/// # use std::time::Duration;
/// let tween1 = Tween::new(
/// EaseFunction::QuadraticInOut,
/// TweeningType::Once,
/// Duration::from_secs_f32(1.0),
/// TransformPositionLens {
/// start: Vec3::ZERO,
/// end: Vec3::new(3.5, 0., 0.),
/// },
/// );
/// let tween2 = Tween::new(
/// EaseFunction::QuadraticInOut,
/// TweeningType::Once,
/// Duration::from_secs_f32(1.0),
/// TransformRotationLens {
/// start: Quat::IDENTITY,
/// end: Quat::from_rotation_x(90.0_f32.to_radians()),
/// },
/// );
/// let seq = tween1.then(tween2);
/// ```
pub fn then(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> {
Sequence::with_capacity(2).then(self).then(tween)
}
}
impl<T> Tween<T> {
/// Create a new tween animation.
///
/// # Example
/// ```
/// # use bevy_tweening::{lens::*, *};
/// # use bevy::math::Vec3;
/// # use std::time::Duration;
/// let tween = Tween::new(
/// EaseFunction::QuadraticInOut,
/// TweeningType::Once,
/// Duration::from_secs_f32(1.0),
/// TransformPositionLens {
/// start: Vec3::ZERO,
/// end: Vec3::new(3.5, 0., 0.),
/// },
/// );
/// ```
pub fn new<L>(
ease_function: impl Into<EaseMethod>,
tweening_type: TweeningType,
duration: Duration,
lens: L,
) -> Self
where
L: Lens<T> + Send + Sync + 'static,
{
Tween {
ease_function: ease_function.into(),
clock: AnimClock::new(duration, tweening_type != TweeningType::Once),
times_completed: 0,
tweening_type,
direction: TweeningDirection::Forward,
lens: Box::new(lens),
/// Enable or disable raising a completed event.
///
/// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed.
/// This is similar to the [`set_completed()`] callback, but uses Bevy events instead.
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
/// # Example
/// ```
/// # use bevy_tweening::{lens::*, *};
/// # use bevy::{ecs::event::EventReader, math::Vec3};
/// # use std::time::Duration;
/// let tween = Tween::new(
/// // [...]
/// # EaseFunction::QuadraticInOut,
/// # TweeningType::Once,
/// # Duration::from_secs_f32(1.0),
/// # TransformPositionLens {
/// # start: Vec3::ZERO,
/// # end: Vec3::new(3.5, 0., 0.),
/// # },
/// )
/// .with_completed_event(true, 42);
///
/// fn my_system(mut reader: EventReader<TweenCompleted>) {
/// for ev in reader.iter() {
/// assert_eq!(ev.user_data, 42);
/// println!("Entity {:?} raised TweenCompleted!", ev.entity);
/// }
/// }
/// ```
///
/// [`set_completed()`]: Tween::set_completed
pub fn with_completed_event(mut self, enabled: bool, user_data: u64) -> Self {
self.event_data = if enabled { Some(user_data) } else { None };
/// 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.
pub fn direction(&self) -> TweeningDirection {
self.direction
}
/// Set a callback invoked when the animation completed.
///
/// The callback when invoked receives as parameters the [`Entity`] on which the target and the
/// animator are, as well as a reference to the current [`Tween`].
///
/// Only non-looping tweenables can complete.
pub fn set_completed<C>(&mut self, callback: C)
C: Fn(Entity, &Tween<T>) + Send + Sync + 'static,
self.on_completed = Some(Box::new(callback));
/// Clear the callback invoked when the animation completed.
pub fn clear_completed(&mut self) {
self.on_completed = None;
/// Enable or disable raising a completed event.
///
/// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed.
/// This is similar to the [`set_completed()`] callback, but uses Bevy events instead.
///
/// See [`with_completed_event()`] for details.
/// [`set_completed()`]: Tween::set_completed
/// [`with_completed_event()`]: Tween::with_completed_event
pub fn set_completed_event(&mut self, enabled: bool, user_data: u64) {
self.event_data = if enabled { Some(user_data) } else { None };
impl<T> Tweenable<T> for Tween<T> {
fn duration(&self) -> Duration {
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);
self.clock.progress()
fn tick(
&mut self,
delta: Duration,
target: &mut T,
entity: Entity,
event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState {
if !self.is_looping() && self.clock.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.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 mut factor = progress;
if self.direction.is_backward() {
factor = 1. - factor;
}
let factor = self.ease_function.sample(factor);
// 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,
user_data: *user_data,
});
if let Some(cb) = &self.on_completed {
state
}
fn times_completed(&self) -> u32 {
self.times_completed
self.clock.reset();
self.times_completed = 0;
}
}
/// A sequence of tweens played back in order one after the other.
pub struct Sequence<T> {
tweens: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
index: usize,
duration: Duration,
time: Duration,
}
impl<T> Sequence<T> {
/// Create a new sequence of tweens.
///
/// This method panics if the input collection is empty.
pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self {
let tweens: Vec<_> = items
.into_iter()
.map(IntoBoxDynTweenable::into_box_dyn)
.collect();
assert!(!tweens.is_empty());
let duration = tweens.iter().map(|t| t.duration()).sum();
Sequence {
tweens,
index: 0,
duration,
time: Duration::ZERO,
}
}
/// Create a new sequence containing a single tween.
pub fn from_single(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
let duration = tween.duration();
Sequence {
tweens: vec![Box::new(tween)],
index: 0,
duration,
time: Duration::ZERO,
times_completed: 0,
}
}
/// Create a new sequence with the specified capacity.
pub fn with_capacity(capacity: usize) -> Self {
Sequence {
tweens: Vec::with_capacity(capacity),
index: 0,
duration: Duration::ZERO,
time: Duration::ZERO,
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
}
}
/// Append a [`Tweenable`] to this sequence.
pub fn then(mut self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
self.duration += tween.duration();
self.tweens.push(Box::new(tween));
self
}
/// Index of the current active tween in the sequence.
pub fn index(&self) -> usize {
self.index.min(self.tweens.len() - 1)
}
/// Get the current active tween in the sequence.
pub fn current(&self) -> &dyn Tweenable<T> {
self.tweens[self.index()].as_ref()
}
}
impl<T> Tweenable<T> for Sequence<T> {
fn duration(&self) -> Duration {
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
// Set the total sequence progress
let total_elapsed_secs = self.duration().as_secs_f64() * progress as f64;
self.time = Duration::from_secs_f64(total_elapsed_secs);
// Find which tween is active in the sequence
let mut accum_duration = 0.;
for index in 0..self.tweens.len() {
let tween = &mut self.tweens[index];
let tween_duration = tween.duration().as_secs_f64();
if total_elapsed_secs < accum_duration + tween_duration {
self.index = index;
let local_duration = total_elapsed_secs - accum_duration;
tween.set_progress((local_duration / tween_duration) as f32);
// TODO?? set progress of other tweens after that one to 0. ??
return;
}
tween.set_progress(1.); // ?? to prepare for next loop/rewind?
accum_duration += tween_duration;
}
// None found; sequence ended
self.index = self.tweens.len();
}
fn progress(&self) -> f32 {
self.time.as_secs_f32() / self.duration.as_secs_f32()
}
fn tick(
&mut self,
delta: Duration,
target: &mut T,
entity: Entity,
event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState {
let mut state = TweenState::Active;
self.time = (self.time + delta).min(self.duration);
let tween = &mut self.tweens[self.index];
let tween_state = tween.tick(delta, target, entity, event_writer);
if tween_state == TweenState::Completed {
tween.rewind();
self.index += 1;
if self.index >= self.tweens.len() {
state = TweenState::Completed;
self.times_completed = 1;
state
} else {
TweenState::Completed
fn times_completed(&self) -> u32 {
self.times_completed
}
fn rewind(&mut self) {
self.time = Duration::ZERO;
self.index = 0;
self.times_completed = 0;
for tween in &mut self.tweens {
// or only first?
tween.rewind();
}
}
}
/// A collection of [`Tweenable`] executing in parallel.
pub struct Tracks<T> {
tracks: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
duration: Duration,
time: Duration,
}
impl<T> Tracks<T> {
/// Create a new [`Tracks`] from an iterator over a collection of [`Tweenable`].
pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self {
let tracks: Vec<_> = items
.into_iter()
.map(IntoBoxDynTweenable::into_box_dyn)
.collect();
let duration = tracks.iter().map(|t| t.duration()).max().unwrap();
Tracks {
tracks,
duration,
time: Duration::ZERO,
}
}
}
impl<T> Tweenable<T> for Tracks<T> {
fn duration(&self) -> Duration {
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
let time_secs = self.duration.as_secs_f64() * progress as f64;
self.time = Duration::from_secs_f64(time_secs);
for tweenable in &mut self.tracks {
let progress = time_secs / tweenable.duration().as_secs_f64();
tweenable.set_progress(progress as f32);
}
fn progress(&self) -> f32 {
self.time.as_secs_f32() / self.duration.as_secs_f32()
}
fn tick(
&mut self,
delta: Duration,
target: &mut T,
entity: Entity,
event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState {
self.time = (self.time + delta).min(self.duration);
let mut any_active = false;
let state = tweenable.tick(delta, target, entity, event_writer);
any_active = any_active || (state == TweenState::Active);
if any_active {
TweenState::Active
self.times_completed = 1;
fn times_completed(&self) -> u32 {
self.times_completed
}
fn rewind(&mut self) {
self.time = Duration::ZERO;
self.times_completed = 0;
for tween in &mut self.tracks {
tween.rewind();
/// A time delay that doesn't animate anything.
///
/// This is generally useful for combining with other tweenables into sequences and tracks,
/// for example to delay the start of a tween in a track relative to another track. The `menu`
/// example (`examples/menu.rs`) uses this technique to delay the animation of its buttons.
pub struct Delay {
timer: Timer,
}
impl Delay {
/// Create a new [`Delay`] with a given duration.
pub fn new(duration: Duration) -> Self {
Delay {
timer: Timer::new(duration, false),
}
}
/// Chain another [`Tweenable`] after this tween, making a sequence with the two.
pub fn then<T>(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> {
Sequence::with_capacity(2).then(self).then(tween)
}
}
impl<T> Tweenable<T> for Delay {
fn duration(&self) -> Duration {
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();
self.timer.set_elapsed(Duration::from_secs_f64(
self.timer.duration().as_secs_f64() * progress as f64,
));
// set_elapsed() does not update finished() etc. which we rely on
self.timer.tick(Duration::ZERO);
}
fn progress(&self) -> f32 {
self.timer.percent()
}
fn tick(
&mut self,
delta: Duration,
_target: &mut T,
_entity: Entity,
_event_writer: &mut EventWriter<TweenCompleted>,
) -> TweenState {
self.timer.tick(delta);
if self.timer.finished() {
TweenState::Completed
} else {
TweenState::Active
}
}
fn times_completed(&self) -> u32 {
if self.timer.finished() {
1
self.timer.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::ecs::{event::Events, system::SystemState};
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// Utility to compare floating-point values with a tolerance.
fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() < tol
}
#[derive(Default, Copy, Clone)]
struct CallbackMonitor {
invoke_count: u64,
last_reported_count: u32,
}
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
#[test]
fn anim_clock_precision() {
let duration = Duration::from_millis(1);
let mut clock = AnimClock::new(duration, true);
let test_ticks = [
Duration::from_micros(123),
Duration::from_millis(1),
Duration::from_secs_f32(1. / 24.),
Duration::from_secs_f32(1. / 30.),
Duration::from_secs_f32(1. / 60.),
Duration::from_secs_f32(1. / 120.),
Duration::from_secs_f32(1. / 144.),
Duration::from_secs_f32(1. / 240.),
];
let mut times_completed = 0;
let mut total_duration = Duration::ZERO;
for i in 0..10_000_000 {
let tick = test_ticks[i % test_ticks.len()];
times_completed += clock.tick(tick);
total_duration += tick;
}
assert_eq!(
(total_duration.as_secs_f64() / duration.as_secs_f64()) as u32,
times_completed
);
}
/// Test ticking of a single tween in isolation.
#[test]
fn tween_tick() {
for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] {
for tweening_type in &[
TweeningType::Once,
TweeningType::Loop,
TweeningType::PingPong,
] {
"TweeningType: type={:?} dir={:?}",
tweening_type, tweening_direction
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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);