use std::{any::TypeId, cmp::Ordering, time::Duration};
use bevy::{ecs::change_detection::MutUntyped, prelude::*};
use crate::{AnimTargetKind, EaseMethod, Lens, PlaybackDirection, RepeatCount, RepeatStrategy};
pub type BoxedTweenable = Box<dyn Tweenable + 'static>;
pub trait IntoBoxedTweenable {
fn into_boxed(self) -> BoxedTweenable;
}
impl IntoBoxedTweenable for BoxedTweenable {
fn into_boxed(self) -> BoxedTweenable {
self
}
}
impl<T: Tweenable + 'static> IntoBoxedTweenable for T {
fn into_boxed(self) -> BoxedTweenable {
Box::new(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TweenState {
Active,
Completed,
}
#[derive(Copy, Clone, EntityEvent, Message)]
pub struct CycleCompletedEvent {
#[event_target]
pub anim_entity: Entity,
pub target: AnimTargetKind,
}
#[derive(Debug)]
struct AnimClock {
elapsed: Duration,
cycle_duration: Duration,
total_duration: TotalDuration,
strategy: RepeatStrategy,
}
impl AnimClock {
fn new(cycle_duration: Duration) -> Self {
Self {
elapsed: Duration::ZERO,
cycle_duration,
total_duration: TotalDuration::from_cycles(cycle_duration, RepeatCount::default()),
strategy: RepeatStrategy::default(),
}
}
fn tick(&mut self, tick: Duration) -> (TweenState, i32) {
let mut next_elapsed = self.elapsed.saturating_add(tick);
let mut extra_completed: i32 = 0;
if !self.total_duration.is_finite() {
let period = if self.strategy == RepeatStrategy::MirroredRepeat {
self.cycle_duration * 2
} else {
self.cycle_duration
};
if next_elapsed >= period {
next_elapsed -= period;
extra_completed += 1;
if next_elapsed >= period {
let count = next_elapsed.div_duration_f64(period) as u32;
next_elapsed -= period * count;
extra_completed += count as i32;
debug_assert!(next_elapsed < period);
}
}
};
let (state, mut times_completed) =
self.set_elapsed(next_elapsed, PlaybackDirection::Forward);
if extra_completed > 0 && (times_completed < 0) {
debug_assert_eq!(-1, times_completed);
times_completed = 0;
}
(state, times_completed + extra_completed)
}
fn tick_back(&mut self, mut tick: Duration) -> (TweenState, i32) {
let mut next_elapsed = self.elapsed.saturating_sub(tick);
if !self.total_duration.is_finite() && (tick >= self.elapsed) {
let period = if self.strategy == RepeatStrategy::MirroredRepeat {
self.cycle_duration * 2
} else {
self.cycle_duration
};
tick -= self.elapsed;
if tick >= period {
let count = tick.div_duration_f64(period) as u32;
tick -= period * count;
}
debug_assert!(tick < period);
next_elapsed = if tick == Duration::ZERO {
Duration::ZERO
} else {
period - tick
};
};
self.set_elapsed(next_elapsed, PlaybackDirection::Backward)
}
fn cycle_index(&self) -> u32 {
let index = self.elapsed.div_duration_f64(self.cycle_duration) as u32;
if let TotalDuration::Finite(total_duration) = self.total_duration {
if self.elapsed >= total_duration {
return index - 1;
}
}
index
}
fn cycle_fraction(&self) -> f32 {
let factor = self.elapsed.div_duration_f64(self.cycle_duration).fract() as f32;
if let TotalDuration::Finite(total_duration) = self.total_duration {
if self.elapsed >= total_duration {
return 1.0;
}
}
factor
}
fn mirrored_cycle_fraction(&self) -> f32 {
let ratio = self.elapsed.div_duration_f64(self.cycle_duration);
let index = ratio as u32;
let factor = ratio.fract() as f32;
if let TotalDuration::Finite(total_duration) = self.total_duration {
if self.elapsed >= total_duration {
if self.is_cycle_mirrored(index - 1) {
return 0.0;
} else {
return 1.0;
}
}
}
if self.is_cycle_mirrored(index) {
1.0 - factor
} else {
factor
}
}
#[must_use]
#[inline]
pub fn is_cycle_mirrored(&self, index: u32) -> bool {
if self.strategy == RepeatStrategy::MirroredRepeat {
(index & 1) != 0
} else {
false
}
}
fn times_completed(&self) -> u32 {
self.elapsed.div_duration_f64(self.cycle_duration) as u32
}
fn set_elapsed(
&mut self,
elapsed: Duration,
direction: PlaybackDirection,
) -> (TweenState, i32) {
let old_times_completed = self.times_completed();
self.elapsed = elapsed;
let state = match self.total_duration {
TotalDuration::Finite(total_duration) => {
self.elapsed = self.elapsed.min(total_duration);
if (direction.is_forward() && self.elapsed >= total_duration)
|| (direction.is_backward() && self.elapsed == Duration::ZERO)
{
TweenState::Completed
} else {
TweenState::Active
}
}
TotalDuration::Infinite => TweenState::Active,
};
(
state,
self.times_completed() as i32 - old_times_completed as i32,
)
}
fn elapsed(&self) -> Duration {
self.elapsed
}
fn state(&self, playback_direction: PlaybackDirection) -> TweenState {
match self.total_duration {
TotalDuration::Finite(total_duration) => {
if (playback_direction.is_forward() && self.elapsed >= total_duration)
|| (playback_direction.is_backward() && self.elapsed == Duration::ZERO)
{
TweenState::Completed
} else {
TweenState::Active
}
}
TotalDuration::Infinite => TweenState::Active,
}
}
fn rewind(&mut self, direction: PlaybackDirection) {
self.elapsed = match direction {
PlaybackDirection::Forward => Duration::ZERO,
PlaybackDirection::Backward => self.total_duration.as_finite().unwrap(),
};
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TotalDuration {
Finite(Duration),
Infinite,
}
impl TotalDuration {
pub fn from_cycles(cycle_duration: Duration, repeat_count: RepeatCount) -> Self {
match repeat_count {
RepeatCount::Finite(times) => {
TotalDuration::Finite(cycle_duration.saturating_mul(times))
}
RepeatCount::For(duration) => TotalDuration::Finite(duration),
RepeatCount::Infinite => TotalDuration::Infinite,
}
}
pub fn is_finite(&self) -> bool {
matches!(self, TotalDuration::Finite(_))
}
pub fn as_finite(&self) -> Option<Duration> {
match self {
Self::Finite(duration) => Some(*duration),
Self::Infinite => None,
}
}
}
impl From<Duration> for TotalDuration {
fn from(value: Duration) -> Self {
TotalDuration::Finite(value)
}
}
impl std::ops::Add for TotalDuration {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(TotalDuration::Finite(d0), TotalDuration::Finite(d1)) => {
TotalDuration::Finite(d0 + d1)
}
_ => TotalDuration::Infinite,
}
}
}
impl std::iter::Sum for TotalDuration {
fn sum<I: Iterator<Item = Self>>(mut iter: I) -> Self {
let Some(mut acc) = iter.next() else {
return TotalDuration::Finite(Duration::ZERO);
};
for td in iter {
acc = acc + td;
}
acc
}
}
impl PartialOrd for TotalDuration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TotalDuration {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(TotalDuration::Finite(d0), TotalDuration::Finite(d1)) => d0.cmp(d1),
(TotalDuration::Finite(_), TotalDuration::Infinite) => Ordering::Less,
(TotalDuration::Infinite, TotalDuration::Finite(_)) => Ordering::Greater,
(TotalDuration::Infinite, TotalDuration::Infinite) => Ordering::Equal,
}
}
}
pub trait Tweenable: Send + Sync {
#[must_use]
fn cycle_duration(&self) -> Duration;
#[must_use]
fn total_duration(&self) -> TotalDuration;
fn set_elapsed(&mut self, elapsed: Duration);
#[must_use]
fn elapsed(&self) -> Duration;
fn step(
&mut self,
tween_id: Entity,
delta: Duration,
target: MutUntyped,
target_type_id: &TypeId,
notify_cycle_completed: &mut dyn FnMut(),
) -> (TweenState, bool);
fn rewind(&mut self);
#[must_use]
fn cycles_completed(&self) -> u32 {
self.elapsed().div_duration_f64(self.cycle_duration()) as u32
}
#[must_use]
fn cycle_fraction(&self) -> f32 {
self.elapsed()
.div_duration_f64(self.cycle_duration())
.fract() as f32
}
#[must_use]
fn target_type_id(&self) -> Option<TypeId>;
}
macro_rules! impl_boxed {
($tweenable:ty) => {
impl From<$tweenable> for BoxedTweenable {
fn from(t: $tweenable) -> Self {
Box::new(t)
}
}
};
}
impl_boxed!(Tween);
impl_boxed!(Sequence);
impl_boxed!(Delay);
type TargetAction = dyn FnMut(MutUntyped, f32) + Send + Sync + 'static;
#[doc(hidden)]
#[derive(Default, Clone, Copy)]
pub struct TweenConfig {
pub ease_method: EaseMethod,
pub playback_direction: PlaybackDirection,
pub send_cycle_completed_event: bool,
pub cycle_duration: Duration,
pub repeat_count: RepeatCount,
pub repeat_strategy: RepeatStrategy,
}
#[doc = include_str!("../images/tween_cycles.svg")]
#[doc = include_str!("../images/tween_mirrored.svg")]
pub struct Tween {
ease_method: EaseMethod,
clock: AnimClock,
playback_direction: PlaybackDirection,
action: Box<TargetAction>,
send_cycle_completed_event: bool,
type_id: TypeId,
}
impl Tween {
#[inline]
#[must_use]
pub fn new<T, L>(
ease_method: impl Into<EaseMethod>,
cycle_duration: Duration,
mut lens: L,
) -> Self
where
T: 'static,
L: Lens<T> + Send + Sync + 'static,
{
let action = move |ptr: MutUntyped, ratio: f32| {
#[allow(unsafe_code)]
let target = unsafe { ptr.with_type::<T>() };
lens.lerp(target, ratio);
};
Self {
ease_method: ease_method.into(),
clock: AnimClock::new(cycle_duration),
playback_direction: PlaybackDirection::Forward,
action: Box::new(action),
send_cycle_completed_event: false,
type_id: TypeId::of::<T>(),
}
}
#[inline]
#[must_use]
pub(crate) fn from_config<T, L>(config: TweenConfig, mut lens: L) -> Self
where
T: 'static,
L: Lens<T> + Send + Sync + 'static,
{
let action = move |ptr: MutUntyped, ratio: f32| {
#[allow(unsafe_code)]
let target = unsafe { ptr.with_type::<T>() };
lens.lerp(target, ratio);
};
let this = Self {
ease_method: config.ease_method,
clock: AnimClock::new(config.cycle_duration),
playback_direction: config.playback_direction,
action: Box::new(action),
send_cycle_completed_event: config.send_cycle_completed_event,
type_id: TypeId::of::<T>(),
};
this.with_repeat(config.repeat_count, config.repeat_strategy)
}
#[must_use]
pub fn with_repeat_count(mut self, count: impl Into<RepeatCount>) -> Self {
self.clock.total_duration =
TotalDuration::from_cycles(self.clock.cycle_duration, count.into());
self
}
pub fn set_repeat_count(&mut self, count: impl Into<RepeatCount>) {
self.clock.total_duration =
TotalDuration::from_cycles(self.clock.cycle_duration, count.into());
}
#[must_use]
pub fn with_repeat_strategy(mut self, strategy: RepeatStrategy) -> Self {
self.clock.strategy = strategy;
self
}
pub fn set_repeat_strategy(&mut self, strategy: RepeatStrategy) {
self.clock.strategy = strategy;
}
#[must_use]
#[inline]
pub fn with_repeat(self, count: impl Into<RepeatCount>, strategy: RepeatStrategy) -> Self {
self.with_repeat_count(count).with_repeat_strategy(strategy)
}
#[inline]
pub fn set_repeat(&mut self, count: impl Into<RepeatCount>, strategy: RepeatStrategy) {
self.set_repeat_count(count);
self.set_repeat_strategy(strategy);
}
#[must_use]
pub fn with_cycle_completed_event(mut self, send: bool) -> Self {
self.send_cycle_completed_event = send;
self
}
pub fn set_cycle_completed_event(&mut self, send: bool) {
self.send_cycle_completed_event = send;
}
pub fn set_playback_direction(&mut self, direction: PlaybackDirection) {
self.playback_direction = direction;
}
#[must_use]
pub fn with_playback_direction(mut self, direction: PlaybackDirection) -> Self {
self.playback_direction = direction;
self
}
#[must_use]
pub fn playback_direction(&self) -> PlaybackDirection {
self.playback_direction
}
#[must_use]
pub fn then(self, tween: impl Tweenable + 'static) -> Sequence {
Sequence::with_capacity(2).then(self).then(tween)
}
#[must_use]
#[inline]
pub fn cycle_index(&self) -> u32 {
self.clock.cycle_index()
}
#[must_use]
#[inline]
pub fn cycle_fraction(&self) -> f32 {
self.clock.cycle_fraction()
}
#[must_use]
#[inline]
pub fn is_cycle_mirrored(&self) -> bool {
self.clock.is_cycle_mirrored(self.clock.cycle_index())
}
}
impl Tweenable for Tween {
fn cycle_duration(&self) -> Duration {
self.clock.cycle_duration
}
fn total_duration(&self) -> TotalDuration {
self.clock.total_duration
}
fn set_elapsed(&mut self, elapsed: Duration) {
self.clock.set_elapsed(elapsed, self.playback_direction);
}
fn elapsed(&self) -> Duration {
self.clock.elapsed()
}
fn step(
&mut self,
_tween_id: Entity,
delta: Duration,
target: MutUntyped,
target_type_id: &TypeId,
notify_cycle_completed: &mut dyn FnMut(),
) -> (TweenState, bool) {
debug_assert_eq!(self.type_id, *target_type_id);
if self.clock.state(self.playback_direction) == TweenState::Completed {
return (TweenState::Completed, false);
}
let (state, times_completed) = if self.playback_direction.is_forward() {
self.clock.tick(delta)
} else {
self.clock.tick_back(delta)
};
let fraction = self.clock.mirrored_cycle_fraction();
let fraction = self.ease_method.sample(fraction);
(self.action)(target, fraction);
if times_completed != 0 && self.send_cycle_completed_event {
notify_cycle_completed();
}
(state, false)
}
fn rewind(&mut self) {
self.clock.rewind(self.playback_direction);
}
fn target_type_id(&self) -> Option<TypeId> {
Some(self.type_id)
}
}
pub struct Sequence {
tweens: Vec<BoxedTweenable>,
index: usize,
cycle_duration: TotalDuration,
total_duration: TotalDuration,
elapsed: Duration,
}
impl Sequence {
#[must_use]
#[inline]
pub fn new(items: impl IntoIterator<Item = impl Into<BoxedTweenable>>) -> Self {
let tweens: Vec<_> = items.into_iter().map(Into::into).collect();
assert!(!tweens.is_empty());
let total_duration = tweens.iter().map(|tween| tween.total_duration()).sum();
Self {
tweens,
index: 0,
cycle_duration: total_duration,
total_duration,
elapsed: Duration::ZERO,
}
}
#[must_use]
#[inline]
pub fn from_single(tweenable: impl Tweenable + 'static) -> Self {
let total_duration = tweenable.total_duration();
let boxed: BoxedTweenable = Box::new(tweenable);
Self {
tweens: vec![boxed],
index: 0,
cycle_duration: total_duration,
total_duration,
elapsed: Duration::ZERO,
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let total_duration = TotalDuration::Finite(Duration::ZERO);
Self {
tweens: Vec::with_capacity(capacity),
index: 0,
cycle_duration: total_duration,
total_duration,
elapsed: Duration::ZERO,
}
}
#[must_use]
pub fn then(mut self, tween: impl Tweenable + 'static) -> Self {
self.total_duration = self.total_duration + tween.total_duration();
self.cycle_duration = self.cycle_duration + tween.total_duration();
self.tweens.push(Box::new(tween));
self
}
#[must_use]
pub fn index(&self) -> usize {
self.index.min(self.tweens.len() - 1)
}
#[must_use]
pub fn current(&self) -> &dyn Tweenable {
self.tweens[self.index()].as_ref()
}
}
impl Tweenable for Sequence {
fn cycle_duration(&self) -> Duration {
self.cycle_duration.as_finite().unwrap()
}
fn total_duration(&self) -> TotalDuration {
self.total_duration
}
fn set_elapsed(&mut self, elapsed: Duration) {
self.elapsed = elapsed;
let mut accum_duration = Duration::ZERO;
for (index, tween) in self.tweens.iter_mut().enumerate() {
let tween_duration = tween.total_duration();
if let TotalDuration::Finite(duration) = tween_duration {
if elapsed >= accum_duration + duration {
accum_duration += duration;
tween.set_elapsed(duration);
continue;
}
};
self.index = index;
let local_duration = elapsed - accum_duration;
tween.set_elapsed(local_duration);
for i in (index + 1)..self.tweens.len() {
self.tweens[i].set_elapsed(Duration::ZERO);
}
return;
}
self.index = self.tweens.len();
}
fn elapsed(&self) -> Duration {
self.elapsed
}
fn step(
&mut self,
tween_id: Entity,
mut delta: Duration,
mut target: MutUntyped,
target_type_id: &TypeId,
notify_completed: &mut dyn FnMut(),
) -> (TweenState, bool) {
if self.index >= self.tweens.len() {
return (TweenState::Completed, false);
}
self.elapsed = self.elapsed.saturating_add(delta);
if let TotalDuration::Finite(total_duration) = self.total_duration {
self.elapsed = self.elapsed.min(total_duration);
}
while self.index < self.tweens.len() {
let tween = &mut self.tweens[self.index];
let prev_elapsed = tween.elapsed();
if let (TweenState::Active, retarget) = tween.step(
tween_id,
delta,
target.reborrow(),
target_type_id,
notify_completed,
) {
return (TweenState::Active, retarget);
}
let TotalDuration::Finite(total_duration) = tween.total_duration() else {
return (TweenState::Active, false);
};
let consumed_duration = total_duration.saturating_sub(prev_elapsed);
delta -= consumed_duration;
self.index += 1;
if self.index < self.tweens.len() {
if let Some(type_id) = self.tweens[self.index].target_type_id() {
if type_id != *target_type_id {
return (TweenState::Active, true);
}
}
}
}
(TweenState::Completed, false)
}
fn rewind(&mut self) {
self.elapsed = Duration::ZERO;
self.index = 0;
for tween in &mut self.tweens {
tween.rewind();
}
}
fn target_type_id(&self) -> Option<TypeId> {
let mut target_type_id = None;
for tween in &self.tweens {
if let Some(type_id) = tween.target_type_id() {
assert!(target_type_id.is_none() || target_type_id == Some(type_id), "TODO: Cannot use tweenable animations with different targets inside the same Sequence. Create separate animations for each target.");
target_type_id = Some(type_id);
}
}
target_type_id
}
}
#[derive(Debug)]
pub struct Delay {
timer: Timer,
}
impl Delay {
#[must_use]
pub fn then(self, tween: impl Tweenable + 'static) -> Sequence {
Sequence::with_capacity(2).then(self).then(tween)
}
#[must_use]
pub fn new(duration: Duration) -> Self {
assert!(!duration.is_zero());
Self {
timer: Timer::new(duration, TimerMode::Once),
}
}
pub fn is_completed(&self) -> bool {
self.timer.is_finished()
}
pub fn state(&self) -> TweenState {
if self.is_completed() {
TweenState::Completed
} else {
TweenState::Active
}
}
}
impl Tweenable for Delay {
fn cycle_duration(&self) -> Duration {
self.timer.duration()
}
fn total_duration(&self) -> TotalDuration {
TotalDuration::Finite(self.cycle_duration())
}
fn set_elapsed(&mut self, elapsed: Duration) {
self.timer.reset();
self.timer.set_elapsed(elapsed);
self.timer.tick(Duration::ZERO);
}
fn elapsed(&self) -> Duration {
self.timer.elapsed()
}
fn step(
&mut self,
_tween_id: Entity,
delta: Duration,
_target: MutUntyped,
_target_type_id: &TypeId,
_notify_completed: &mut dyn FnMut(),
) -> (TweenState, bool) {
self.timer.tick(delta);
let state = self.state();
(state, false)
}
fn rewind(&mut self) {
self.timer.reset();
}
fn target_type_id(&self) -> Option<TypeId> {
None
}
}
#[cfg(test)]
mod tests {
use std::ops::{Deref as _, DerefMut as _};
use bevy::ecs::{change_detection::MaybeLocation, change_detection::Tick, system::SystemState};
use super::*;
use crate::{lens::*, test_utils::assert_approx_eq};
#[test]
fn anim_clock_cycles() {
for dummy in [PlaybackDirection::Forward, PlaybackDirection::Backward] {
let cycle_duration = Duration::from_millis(100);
let repeat_count = 4;
let total_duration = cycle_duration * repeat_count;
let mut clock = AnimClock::new(cycle_duration);
clock.total_duration = TotalDuration::Finite(total_duration);
assert_eq!(cycle_duration, clock.cycle_duration);
assert_eq!(RepeatStrategy::Repeat, clock.strategy);
assert_eq!(Duration::ZERO, clock.elapsed());
assert_eq!(0, clock.cycle_index());
assert_approx_eq!(0.0, clock.cycle_fraction());
assert_approx_eq!(0.0, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(30);
clock.set_elapsed(dt, dummy);
assert_eq!(dt, clock.elapsed());
assert_eq!(0, clock.cycle_index());
assert_approx_eq!(0.3, clock.cycle_fraction());
assert_approx_eq!(0.3, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(110);
clock.set_elapsed(dt, dummy);
assert_eq!(dt, clock.elapsed());
assert_eq!(1, clock.cycle_index());
assert_approx_eq!(0.1, clock.cycle_fraction());
assert_approx_eq!(0.1, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(400);
clock.set_elapsed(dt, dummy);
assert_eq!(dt, clock.elapsed());
assert_eq!(3, clock.cycle_index()); assert_approx_eq!(1.0, clock.cycle_fraction()); assert_approx_eq!(1.0, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(410); clock.set_elapsed(dt, dummy);
assert_eq!(total_duration, clock.elapsed()); assert_eq!(3, clock.cycle_index()); assert_approx_eq!(1.0, clock.cycle_fraction()); assert_approx_eq!(1.0, clock.mirrored_cycle_fraction());
clock.strategy = RepeatStrategy::MirroredRepeat;
let dt = Duration::from_millis(110);
clock.set_elapsed(dt, dummy);
assert_eq!(dt, clock.elapsed());
assert_eq!(1, clock.cycle_index());
assert_approx_eq!(0.1, clock.cycle_fraction());
assert_approx_eq!(0.9, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(400);
clock.set_elapsed(dt, dummy);
assert_eq!(dt, clock.elapsed());
assert_eq!(3, clock.cycle_index()); assert_approx_eq!(1.0, clock.cycle_fraction()); assert_approx_eq!(0.0, clock.mirrored_cycle_fraction());
let dt = Duration::from_millis(410); clock.set_elapsed(dt, dummy);
assert_eq!(total_duration, clock.elapsed()); assert_eq!(3, clock.cycle_index()); assert_approx_eq!(1.0, clock.cycle_fraction()); assert_approx_eq!(0.0, clock.mirrored_cycle_fraction()); }
}
#[test]
fn anim_clock_precision() {
let cycle_duration = Duration::from_millis(1);
let mut clock = AnimClock::new(cycle_duration);
clock.total_duration = TotalDuration::Infinite;
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;
let num_iter = 100_000_000; for i in 0..num_iter {
let tick = test_ticks[i % test_ticks.len()];
times_completed += clock.tick(tick).1;
total_duration += tick;
}
assert_eq!(
total_duration.div_duration_f64(cycle_duration) as i32,
times_completed
);
}
fn make_test_tween() -> Tween {
Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
)
}
fn make_test_env() -> (World, Entity) {
let mut world = World::new();
world.init_resource::<Messages<CycleCompletedEvent>>();
let entity = world.spawn(Transform::default()).id();
(world, entity)
}
fn manual_tick_component(
anim_entity: Entity,
duration: Duration,
tween: &mut dyn Tweenable,
world: &mut World,
entity: Entity,
) -> TweenState {
let target_type_id = TypeId::of::<Transform>();
let ret = world.resource_scope(
|world: &mut World, mut events: Mut<Messages<CycleCompletedEvent>>| {
let component_id = world.component_id::<Transform>().unwrap();
let entity_mut = &mut world.get_entity_mut([entity]).unwrap()[0];
if let Ok(mut target) = entity_mut.get_mut_by_id(component_id) {
let world_target = AnimTargetKind::Component { entity };
let mut notify_completed = || {
events.write(CycleCompletedEvent {
anim_entity,
target: world_target,
});
};
tween.step(
anim_entity,
duration,
target.reborrow(),
&target_type_id,
&mut notify_completed,
)
} else {
(TweenState::Completed, false)
}
},
);
{
let mut events = world.resource_mut::<Messages<CycleCompletedEvent>>();
events.update();
}
ret.0
}
#[derive(Debug, Default, Clone, Copy, Component)]
struct DummyComponent {
_value: f32,
}
#[test]
fn targetable_change_detect() {
let mut c = DummyComponent::default();
let mut added = Tick::new(0);
let mut last_changed = Tick::new(0);
let mut caller = MaybeLocation::caller();
let mut target = Mut::new(
&mut c,
&mut added,
&mut last_changed,
Tick::new(0),
Tick::new(1),
caller.as_mut(),
);
assert!(!target.is_added());
assert!(!target.is_changed());
let _ = target.deref();
assert!(!target.is_added());
assert!(!target.is_changed());
let _ = target.deref_mut();
assert!(!target.is_added());
assert!(target.is_changed());
}
#[test]
fn into_repeat_count() {
let tween = Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
)
.with_repeat_count(5);
assert_eq!(
tween.total_duration(),
TotalDuration::Finite(Duration::from_secs(5))
);
let tween = Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
)
.with_repeat_count(Duration::from_secs(3));
assert_eq!(
tween.total_duration(),
TotalDuration::Finite(Duration::from_secs(3))
);
}
#[test]
fn tween_tick() {
for playback_direction in [PlaybackDirection::Forward, PlaybackDirection::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),
] {
println!(
"TweeningType: playback={playback_direction:?} count={count:?} strategy={strategy:?}",
);
let mut tween = make_test_tween()
.with_playback_direction(playback_direction)
.with_repeat_count(count)
.with_repeat_strategy(strategy)
.with_cycle_completed_event(true);
assert_eq!(tween.playback_direction(), playback_direction);
assert!(tween.send_cycle_completed_event);
let backward_start_time = tween
.total_duration()
.as_finite()
.unwrap_or(Duration::from_secs(1));
if playback_direction == PlaybackDirection::Backward {
tween.set_elapsed(backward_start_time);
}
let (mut world, entity) = make_test_env();
let mut event_reader_system_state: SystemState<MessageReader<CycleCompletedEvent>> =
SystemState::new(&mut world);
let tick_duration = Duration::from_millis(200);
let tween_duration = Duration::from_secs(1);
for i in 1..=11 {
let (elapsed_ms, factor, mut direction, expected_state, just_completed) =
match count {
RepeatCount::Finite(1) => {
let (elapsed_ms, state) = if playback_direction.is_forward() {
if i < 5 {
(i * 200i32, TweenState::Active)
} else {
(1000i32, TweenState::Completed)
}
} else if i < 5 {
(1000i32 - i * 200i32, TweenState::Active)
} else {
(0i32, TweenState::Completed)
};
let just_completed = i == 5;
(
elapsed_ms,
elapsed_ms as f32 / 1000.0,
PlaybackDirection::Forward,
state,
just_completed,
)
}
RepeatCount::Finite(count) => {
if strategy == RepeatStrategy::Repeat {
let just_completed = i % 5 == 0;
let elapsed_ms = if playback_direction.is_forward() {
(i * 200) % 1000
} else {
800 - (((i - 1) * 200 - 1000) % 1000 + 1000) % 1000
};
let factor = if i >= 10 {
if playback_direction.is_forward() {
1.0
} else {
0.0
}
} else {
elapsed_ms as f32 / 1000.0
};
let total_duration_ms = count as i32 * 1000;
let elapsed_ms = if playback_direction.is_forward() {
(i * 200).min(total_duration_ms)
} else {
(total_duration_ms - i * 200).max(0)
};
let state = if i >= 10 {
TweenState::Completed
} else {
TweenState::Active
};
(
elapsed_ms,
factor,
PlaybackDirection::Forward,
state,
just_completed,
)
} else {
let i5 = i % 5;
let just_completed = i5 == 0;
let elapsed_ms = if playback_direction.is_forward() {
((i - 5) * 200).rem_euclid(2000) - 1000
} else {
((i + 5) * 200).rem_euclid(2000) - 1000
}
.abs();
let (elapsed_ms, state) = if i < 10 {
(elapsed_ms, TweenState::Active)
} else {
(0, TweenState::Completed)
};
let ratio = elapsed_ms as f32 / 1000.;
let total_duration_ms = count as i32 * 1000;
let elapsed_ms = if playback_direction.is_forward() {
(i * 200).min(total_duration_ms)
} else {
(total_duration_ms - i * 200).max(0)
};
let direction = if playback_direction.is_forward() {
if i >= 5 {
PlaybackDirection::Backward
} else {
PlaybackDirection::Forward
}
} else {
if i <= 5 {
PlaybackDirection::Backward
} else {
PlaybackDirection::Forward
}
};
(elapsed_ms, ratio, direction, state, just_completed)
}
}
RepeatCount::Infinite => {
if strategy == RepeatStrategy::Repeat {
let just_completed = i % 5 == 0;
let elapsed_ms = if playback_direction.is_forward() {
(i * 200) % 1000
} else {
800 - (((i - 1) * 200 - 1000) % 1000 + 1000) % 1000
};
(
elapsed_ms,
elapsed_ms as f32 / 1000.0,
PlaybackDirection::Forward,
TweenState::Active,
just_completed,
)
} else {
let elapsed_ms = if playback_direction.is_forward() {
((i - 5) * 200).rem_euclid(2000) - 1000
} else {
(i * 200).rem_euclid(2000) - 1000
}
.abs();
let factor = elapsed_ms as f32 / 1000.0;
let elapsed_ms = if playback_direction.is_forward() {
(i * 200).rem_euclid(2000)
} else {
(2000i32 - (i - 5) * 200).rem_euclid(2000)
};
let direction = if playback_direction.is_forward() {
if (i % 10) >= 5 {
PlaybackDirection::Backward
} else {
PlaybackDirection::Forward
}
} else {
if ((i - 1) % 10) >= 5 {
PlaybackDirection::Backward
} else {
PlaybackDirection::Forward
}
};
let just_completed = (i % 5) == 0;
(
elapsed_ms,
factor,
direction,
TweenState::Active,
just_completed,
)
}
}
RepeatCount::For(_) => panic!("Untested"),
};
if playback_direction.is_backward() {
direction = !direction;
}
let expected_translation = Vec3::splat(factor);
let elapsed = Duration::from_millis(elapsed_ms as u64);
let cycles_completed = match count {
RepeatCount::Infinite => elapsed,
RepeatCount::Finite(count) => elapsed.min(tween_duration * count),
RepeatCount::For(time) => elapsed.min(time),
}
.div_duration_f64(tween_duration)
as u32;
println!(
"+ Expected: elapsed={:?} factor={} times_completed={} direction={:?} state={:?} just_completed={} translation={:?}",
elapsed, factor, cycles_completed, direction, expected_state, just_completed, expected_translation
);
let actual_state = manual_tick_component(
Entity::PLACEHOLDER, tick_duration,
&mut tween,
&mut world,
entity,
);
assert_eq!(tween.is_cycle_mirrored(), direction != playback_direction);
assert_eq!(actual_state, expected_state);
assert_eq!(tween.elapsed(), elapsed);
assert_eq!(tween.cycles_completed(), cycles_completed);
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(expected_translation, transform.translation, 1e-5);
assert_approx_eq!(Quat::IDENTITY, transform.rotation, 1e-5);
if playback_direction.is_forward() {
let mut event_reader = event_reader_system_state.get_mut(&mut world);
let event = event_reader.read().next();
if just_completed {
assert!(event.is_some());
if let Some(event) = event {
let AnimTargetKind::Component {
entity: comp_target,
} = &event.target
else {
panic!("Expected AnimTargetKind::Component");
};
assert_eq!(*comp_target, entity);
}
} else {
assert!(event.is_none());
}
}
}
if tween.total_duration().is_finite()
|| (playback_direction != PlaybackDirection::Backward)
{
println!("+ Rewind");
tween.rewind();
assert_eq!(tween.playback_direction(), playback_direction); if playback_direction.is_forward() {
assert_eq!(tween.elapsed(), Duration::ZERO);
assert_eq!(tween.cycles_completed(), 0);
} else {
assert_eq!(tween.elapsed(), backward_start_time);
let cycles_completed = match count {
RepeatCount::Infinite => backward_start_time,
RepeatCount::Finite(count) => {
backward_start_time.min(tween_duration * count)
}
RepeatCount::For(time) => backward_start_time.min(time),
}
.div_duration_f64(tween_duration)
as u32;
assert_eq!(cycles_completed, tween.cycles_completed());
}
let actual_state = manual_tick_component(
Entity::PLACEHOLDER, Duration::ZERO,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Active, actual_state);
let expected_translation = if playback_direction.is_backward()
&& strategy != RepeatStrategy::MirroredRepeat
{
Vec3::ONE
} else {
Vec3::ZERO
};
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(expected_translation, transform.translation, 1e-5);
assert_approx_eq!(Quat::IDENTITY, transform.rotation, 1e-5);
}
tween.set_cycle_completed_event(false);
assert!(!tween.send_cycle_completed_event);
}
}
}
#[test]
fn tween_dir() {
let mut tween = make_test_tween();
assert_eq!(tween.playback_direction(), PlaybackDirection::Forward);
assert_eq!(tween.elapsed(), Duration::ZERO);
tween.set_playback_direction(PlaybackDirection::Forward);
assert_eq!(tween.playback_direction(), PlaybackDirection::Forward);
assert_eq!(tween.elapsed(), Duration::ZERO);
tween.set_playback_direction(PlaybackDirection::Backward);
assert_eq!(tween.playback_direction(), PlaybackDirection::Backward);
assert_eq!(tween.elapsed(), Duration::ZERO);
let d300 = Duration::from_millis(300);
tween.set_playback_direction(PlaybackDirection::Forward);
tween.set_elapsed(d300);
assert_eq!(tween.elapsed(), d300);
tween.set_playback_direction(PlaybackDirection::Backward);
assert_eq!(tween.elapsed(), d300);
let (mut world, entity) = make_test_env();
tween.set_playback_direction(PlaybackDirection::Backward);
assert_eq!(tween.elapsed(), d300);
manual_tick_component(
Entity::PLACEHOLDER, Duration::from_millis(100),
&mut tween,
&mut world,
entity,
);
assert_eq!(tween.elapsed(), Duration::from_millis(200)); let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.2), transform.translation, 1e-5);
}
#[test]
fn tween_elapsed() {
let mut tween = make_test_tween();
let cycle_duration = tween.cycle_duration();
let elapsed = tween.elapsed();
assert_eq!(elapsed, Duration::ZERO);
assert_eq!(cycle_duration, Duration::from_secs(1));
for ms in [0, 1, 500, 100, 300, 999, 847, 1000, 900] {
let elapsed = Duration::from_millis(ms);
tween.set_elapsed(elapsed);
assert_eq!(tween.elapsed(), elapsed);
let times_completed = u32::from(ms == 1000);
assert_eq!(tween.cycles_completed(), times_completed);
}
}
#[test]
fn seq_tick() {
let tween1 = Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
);
let tween2 = Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformRotationLens {
start: Quat::IDENTITY,
end: Quat::from_rotation_x(90_f32.to_radians()),
},
);
let mut seq = tween1.then(tween2);
let (mut world, entity) = make_test_env();
for i in 1..=16 {
let state = manual_tick_component(
Entity::PLACEHOLDER, Duration::from_millis(200),
&mut seq,
&mut world,
entity,
);
let transform = world.entity(entity).get::<Transform>().unwrap();
if i < 5 {
assert_eq!(state, TweenState::Active);
let r = i as f32 * 0.2;
assert_approx_eq!(Transform::from_translation(Vec3::splat(r)), *transform);
} else if i < 10 {
assert_eq!(state, TweenState::Active);
let alpha_deg = (18 * (i - 5)) as f32;
assert_approx_eq!(Vec3::ONE, transform.translation);
assert_approx_eq!(
Quat::from_rotation_x(alpha_deg.to_radians()),
transform.rotation
);
} else {
assert_eq!(state, TweenState::Completed);
assert_approx_eq!(Vec3::ONE, transform.translation);
assert_approx_eq!(
Quat::from_rotation_x(90_f32.to_radians()),
transform.rotation
);
}
}
}
#[test]
fn seq_tick_boundaries() {
let mut seq = Sequence::new((0..3).map(|i| {
Tween::new(
EaseMethod::default(),
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::splat(i as f32),
end: Vec3::splat((i + 1) as f32),
},
)
.with_repeat_count(RepeatCount::Finite(1))
}));
let (mut world, entity) = make_test_env();
for delta_ms in [500, 2000] {
manual_tick_component(
Entity::PLACEHOLDER, Duration::from_millis(delta_ms),
&mut seq,
&mut world,
entity,
);
}
assert_eq!(seq.index(), 2);
let transform = world.entity(entity).get::<Transform>().unwrap();
assert!(transform.translation.abs_diff_eq(Vec3::splat(2.5), 1e-5));
}
#[test]
fn seq_iter() {
let mut seq = Sequence::new((1..5).map(|i| {
Tween::new(
EaseMethod::default(),
Duration::from_millis(200 * i),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
)
}));
let mut time = Duration::ZERO;
for i in 1..5 {
assert_eq!(seq.index(), i - 1);
assert_eq!(time, seq.elapsed());
let dt = Duration::from_millis(200 * i as u64);
assert_eq!(dt, seq.current().cycle_duration());
time += dt;
seq.set_elapsed(time);
assert_eq!(seq.cycles_completed(), u32::from(i == 4));
}
seq.rewind();
assert_eq!(Duration::ZERO, seq.elapsed());
assert_eq!(0, seq.cycles_completed());
}
#[test]
fn seq_from_single() {
let dt = Duration::from_secs(1);
let tween = Tween::new(
EaseMethod::default(),
dt,
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
);
let seq = Sequence::from_single(tween);
assert_eq!(1, seq.tweens.len());
assert_eq!(dt, seq.cycle_duration());
assert_eq!(TotalDuration::Finite(dt), seq.total_duration());
}
#[test]
fn seq_elapsed() {
let mut seq = Sequence::new((1..5).map(|i| {
Tween::new(
EaseMethod::default(),
Duration::from_millis(200 * i),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
)
}));
let mut elapsed = Duration::ZERO;
for i in 1..5 {
assert_eq!(seq.index(), i - 1);
assert_eq!(seq.elapsed(), elapsed);
let duration = Duration::from_millis(200 * i as u64);
assert_eq!(seq.current().cycle_duration(), duration);
elapsed += duration;
seq.set_elapsed(elapsed);
assert_eq!(seq.cycles_completed(), u32::from(i == 4));
}
}
#[test]
fn delay_then() {
let dt1 = Duration::from_secs(1);
let dt2 = Duration::from_secs(2);
let seq: Sequence = Delay::new(dt1).then(Delay::new(dt2));
assert_eq!(2, seq.tweens.len());
assert_eq!(dt1 + dt2, seq.cycle_duration());
assert_eq!(TotalDuration::Finite(dt1 + dt2), seq.total_duration());
for (i, tweenable) in seq.tweens.iter().enumerate() {
let dt = Duration::from_secs(i as u64 + 1);
assert_eq!(dt, tweenable.cycle_duration());
assert_eq!(TotalDuration::Finite(dt), tweenable.total_duration());
}
}
#[test]
fn delay_tick() {
let total_dt = Duration::from_secs(1);
let mut delay = Delay::new(total_dt);
{
let tweenable: &dyn Tweenable = &delay;
assert_eq!(total_dt, tweenable.cycle_duration());
assert_eq!(TotalDuration::Finite(total_dt), tweenable.total_duration());
assert_eq!(Duration::ZERO, tweenable.elapsed());
}
let (mut world, entity) = make_test_env();
let mut time = Duration::ZERO;
for i in 1..=6 {
let dt = Duration::from_millis(200);
time += dt;
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut delay,
&mut world,
entity,
);
{
assert_eq!(state, delay.state());
let tweenable: &dyn Tweenable = &delay;
if i < 5 {
assert_eq!(state, TweenState::Active);
assert!(!delay.is_completed());
assert_eq!(0, tweenable.cycles_completed());
assert_eq!(dt * i, tweenable.elapsed());
} else {
assert_eq!(state, TweenState::Completed);
assert!(delay.is_completed());
assert_eq!(1, tweenable.cycles_completed());
assert_eq!(total_dt, tweenable.elapsed());
}
}
}
delay.rewind();
assert_eq!(0, delay.cycles_completed());
assert_eq!(Duration::ZERO, delay.elapsed());
let state: TweenState = manual_tick_component(
Entity::PLACEHOLDER, Duration::ZERO,
&mut delay,
&mut world,
entity,
);
assert_eq!(TweenState::Active, state);
let dt = Duration::from_millis(300);
delay.set_elapsed(dt);
assert_eq!(0, delay.cycles_completed());
assert_eq!(dt, delay.elapsed());
let dt = Duration::from_millis(1200);
delay.set_elapsed(dt);
assert_eq!(1, delay.cycles_completed());
assert_eq!(total_dt, delay.elapsed());
}
#[test]
fn delay_elapsed() {
let dt = Duration::from_secs(1);
let mut delay = Delay::new(dt);
assert_eq!(dt, delay.cycle_duration());
assert_eq!(TotalDuration::Finite(dt), delay.total_duration());
for ms in [0, 1, 500, 100, 300, 999, 847, 1000, 900] {
let elapsed = Duration::from_millis(ms);
delay.set_elapsed(elapsed);
assert_eq!(elapsed, delay.elapsed());
let times_completed = u32::from(ms == 1000);
assert_eq!(times_completed, delay.cycles_completed());
assert_eq!(ms >= 1000, delay.is_completed());
assert_eq!(
delay.state(),
if ms >= 1000 {
TweenState::Completed
} else {
TweenState::Active
}
);
}
}
#[test]
#[should_panic]
fn delay_zero_duration_panics() {
let _ = Delay::new(Duration::ZERO);
}
#[test]
fn tween_repeat() {
let mut tween = make_test_tween()
.with_repeat_count(RepeatCount::Finite(5))
.with_repeat_strategy(RepeatStrategy::Repeat);
assert_eq!(Duration::ZERO, tween.elapsed());
let (mut world, entity) = make_test_env();
let mut time = Duration::ZERO;
let dt = Duration::from_millis(100);
time += dt;
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Active, state);
assert_eq!(0, tween.cycles_completed());
assert_eq!(time, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.1), transform.translation, 1e-5);
let dt = Duration::from_millis(1200);
time += dt;
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Active, state);
assert_eq!(1, tween.cycles_completed());
assert_eq!(time, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.3), transform.translation, 1e-5);
let dt = Duration::from_millis(3500);
time += dt;
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Active, state);
assert_eq!(4, tween.cycles_completed());
assert_eq!(time, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.8), transform.translation, 1e-5);
let dt = Duration::from_millis(200);
time += dt;
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Completed, state);
assert_eq!(5, tween.cycles_completed());
assert_eq!(time, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::ONE, transform.translation, 1e-5);
}
#[test]
fn tween_mirrored_rewind() {
let mut tween = make_test_tween()
.with_repeat_count(RepeatCount::Finite(4))
.with_repeat_strategy(RepeatStrategy::MirroredRepeat);
assert_eq!(Duration::ZERO, tween.elapsed());
let (mut world, entity) = make_test_env();
let dt = Duration::from_millis(100);
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Active, state);
assert!(!tween.is_cycle_mirrored());
assert_eq!(0, tween.cycles_completed());
assert_eq!(dt, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.1), transform.translation, 1e-5);
tween.rewind();
assert!(!tween.is_cycle_mirrored());
assert_eq!(0, tween.cycles_completed());
assert_eq!(Duration::ZERO, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.1), transform.translation, 1e-5);
let dt = Duration::from_millis(1200);
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert!(tween.is_cycle_mirrored());
assert_eq!(TweenState::Active, state);
assert_eq!(1, tween.cycles_completed());
assert_eq!(dt, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.8), transform.translation, 1e-5);
assert!(tween.is_cycle_mirrored());
tween.rewind();
assert!(!tween.is_cycle_mirrored()); assert_eq!(0, tween.cycles_completed());
assert_eq!(Duration::ZERO, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::splat(0.8), transform.translation, 1e-5);
let dt = Duration::from_millis(4000);
let state = manual_tick_component(
Entity::PLACEHOLDER, dt,
&mut tween,
&mut world,
entity,
);
assert_eq!(TweenState::Completed, state);
assert!(tween.is_cycle_mirrored()); assert_eq!(4, tween.cycles_completed());
assert_eq!(dt, tween.elapsed()); let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::ZERO, transform.translation, 1e-5);
assert!(tween.is_cycle_mirrored());
tween.rewind();
assert!(!tween.is_cycle_mirrored()); assert_eq!(0, tween.cycles_completed());
assert_eq!(Duration::ZERO, tween.elapsed());
let transform = world.entity(entity).get::<Transform>().unwrap();
assert_approx_eq!(Vec3::ZERO, transform.translation, 1e-5); }
}