#![deny(
warnings,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
missing_docs
)]
use std::{
any::TypeId,
ops::{Deref, DerefMut},
time::Duration,
};
use bevy::{
asset::UntypedAssetId,
ecs::{
change_detection::MutUntyped,
component::{ComponentId, Components, Mutable},
},
platform::collections::HashMap,
prelude::*,
};
pub use lens::Lens;
use lens::{
TransformRotateAdditiveXLens, TransformRotateAdditiveYLens, TransformRotateAdditiveZLens,
};
pub use plugin::{AnimationSystem, TweeningPlugin};
use thiserror::Error;
pub use tweenable::{
BoxedTweenable, CycleCompletedEvent, Delay, IntoBoxedTweenable, Sequence, TotalDuration, Tween,
TweenState, Tweenable,
};
use crate::{
lens::{TransformPositionLens, TransformScaleLens},
tweenable::TweenConfig,
};
pub mod lens;
mod plugin;
mod tweenable;
#[cfg(test)]
mod test_utils;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepeatCount {
Finite(u32),
For(Duration),
Infinite,
}
impl Default for RepeatCount {
fn default() -> Self {
Self::Finite(1)
}
}
impl From<u32> for RepeatCount {
fn from(value: u32) -> Self {
Self::Finite(value)
}
}
impl From<Duration> for RepeatCount {
fn from(value: Duration) -> Self {
Self::For(value)
}
}
impl RepeatCount {
pub fn total_duration(&self, cycle_duration: Duration) -> TotalDuration {
match self {
RepeatCount::Finite(count) => TotalDuration::Finite(cycle_duration * *count),
RepeatCount::For(duration) => TotalDuration::Finite(*duration),
RepeatCount::Infinite => TotalDuration::Infinite,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum RepeatStrategy {
#[default]
Repeat,
MirroredRepeat,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum PlaybackState {
#[default]
Playing,
Paused,
}
impl std::ops::Not for PlaybackState {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Self::Paused => Self::Playing,
Self::Playing => Self::Paused,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum EaseMethod {
EaseFunction(EaseFunction),
Discrete(f32),
CustomFunction(fn(f32) -> f32),
}
impl EaseMethod {
#[must_use]
fn sample(self, x: f32) -> f32 {
match self {
Self::EaseFunction(function) => EasingCurve::new(0.0, 1.0, function).sample(x).unwrap(),
Self::Discrete(limit) => {
if x > limit {
1.
} else {
0.
}
}
Self::CustomFunction(function) => function(x),
}
}
}
impl Default for EaseMethod {
fn default() -> Self {
Self::EaseFunction(EaseFunction::Linear)
}
}
impl From<EaseFunction> for EaseMethod {
fn from(ease_function: EaseFunction) -> Self {
Self::EaseFunction(ease_function)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum PlaybackDirection {
#[default]
Forward,
Backward,
}
impl PlaybackDirection {
#[must_use]
pub fn is_forward(&self) -> bool {
*self == Self::Forward
}
#[must_use]
pub fn is_backward(&self) -> bool {
*self == Self::Backward
}
}
impl std::ops::Not for PlaybackDirection {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Self::Forward => Self::Backward,
Self::Backward => Self::Forward,
}
}
}
pub trait EntityCommandsTweeningExtensions<'a> {
fn move_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn move_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn scale_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn scale_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_x_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_y_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
fn rotate_z_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand>;
}
pub trait TweenCommand: EntityCommand {
#[allow(unused)]
fn config(&self) -> &TweenConfig;
fn config_mut(&mut self) -> &mut TweenConfig;
}
#[derive(Clone, Copy)]
pub(crate) struct MoveToCommand {
end: Vec3,
config: TweenConfig,
}
impl EntityCommand for MoveToCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(start) = entity.get::<Transform>().map(|tr| tr.translation) {
let lens = TransformPositionLens {
start,
end: self.end,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for MoveToCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct MoveFromCommand {
start: Vec3,
config: TweenConfig,
}
impl EntityCommand for MoveFromCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(end) = entity.get::<Transform>().map(|tr| tr.translation) {
let lens = TransformPositionLens {
start: self.start,
end,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for MoveFromCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct ScaleToCommand {
end: Vec3,
config: TweenConfig,
}
impl EntityCommand for ScaleToCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(start) = entity.get::<Transform>().map(|tr| tr.scale) {
let lens = TransformScaleLens {
start,
end: self.end,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for ScaleToCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct ScaleFromCommand {
start: Vec3,
config: TweenConfig,
}
impl EntityCommand for ScaleFromCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(end) = entity.get::<Transform>().map(|tr| tr.scale) {
let lens = TransformScaleLens {
start: self.start,
end,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for ScaleFromCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateXCommand {
config: TweenConfig,
}
impl EntityCommand for RotateXCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveXLens {
base_rotation,
start: 0.,
end: std::f32::consts::TAU,
};
let tween = Tween::from_config(self.config, lens)
.with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateXCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateYCommand {
config: TweenConfig,
}
impl EntityCommand for RotateYCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveYLens {
base_rotation,
start: 0.,
end: std::f32::consts::TAU,
};
let tween = Tween::from_config(self.config, lens)
.with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateYCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateZCommand {
config: TweenConfig,
}
impl EntityCommand for RotateZCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveZLens {
base_rotation,
start: 0.,
end: std::f32::consts::TAU,
};
let tween = Tween::from_config(self.config, lens)
.with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateZCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateXByCommand {
angle: f32,
config: TweenConfig,
}
impl EntityCommand for RotateXByCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveXLens {
base_rotation,
start: 0.,
end: self.angle,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateXByCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateYByCommand {
angle: f32,
config: TweenConfig,
}
impl EntityCommand for RotateYByCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveYLens {
base_rotation,
start: 0.,
end: self.angle,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateYByCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
#[derive(Clone, Copy)]
pub(crate) struct RotateZByCommand {
angle: f32,
config: TweenConfig,
}
impl EntityCommand for RotateZByCommand {
fn apply(self, mut entity: EntityWorldMut) {
if let Some(base_rotation) = entity.get::<Transform>().map(|tr| tr.rotation) {
let lens = TransformRotateAdditiveZLens {
base_rotation,
start: 0.,
end: self.angle,
};
let tween = Tween::from_config(self.config, lens);
let anim_target = AnimTarget::component::<Transform>(entity.id());
entity.world_scope(|world| {
world.spawn((TweenAnim::new(tween), anim_target));
});
}
}
}
impl TweenCommand for RotateZByCommand {
#[inline]
fn config(&self) -> &TweenConfig {
&self.config
}
#[inline]
fn config_mut(&mut self) -> &mut TweenConfig {
&mut self.config
}
}
pub struct AnimatedEntityCommands<'a, C: TweenCommand> {
commands: EntityCommands<'a>,
cmd: Option<C>,
}
impl<'a, C: TweenCommand> AnimatedEntityCommands<'a, C> {
pub fn new(commands: EntityCommands<'a>, cmd: C) -> Self {
Self {
commands,
cmd: Some(cmd),
}
}
#[inline]
pub fn with_repeat_count(mut self, repeat_count: impl Into<RepeatCount>) -> Self {
if let Some(cmd) = self.cmd.as_mut() {
cmd.config_mut().repeat_count = repeat_count.into();
}
self
}
#[inline]
pub fn with_repeat_strategy(mut self, repeat_strategy: RepeatStrategy) -> Self {
if let Some(cmd) = self.cmd.as_mut() {
cmd.config_mut().repeat_strategy = repeat_strategy;
}
self
}
#[inline]
pub fn with_repeat(
self,
repeat_count: impl Into<RepeatCount>,
repeat_strategy: RepeatStrategy,
) -> Self {
self.with_repeat_count(repeat_count)
.with_repeat_strategy(repeat_strategy)
}
pub fn into_inner(mut self) -> EntityCommands<'a> {
self.flush();
let this = std::mem::ManuallyDrop::new(self);
#[allow(unsafe_code)]
unsafe {
std::ptr::read(&this.commands)
}
}
fn flush(&mut self) {
if let Some(cmd) = self.cmd.take() {
self.queue(cmd);
}
}
}
impl<'a, C: TweenCommand> EntityCommandsTweeningExtensions<'a> for AnimatedEntityCommands<'a, C> {
#[inline]
fn move_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().move_to(end, duration, ease_method)
}
#[inline]
fn move_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().move_from(start, duration, ease_method)
}
#[inline]
fn scale_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().scale_to(end, duration, ease_method)
}
#[inline]
fn scale_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().scale_from(start, duration, ease_method)
}
#[inline]
fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_x(cycle_duration)
}
#[inline]
fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_y(cycle_duration)
}
#[inline]
fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_z(cycle_duration)
}
#[inline]
fn rotate_x_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_x_by(angle, duration, ease_method)
}
#[inline]
fn rotate_y_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_y_by(angle, duration, ease_method)
}
#[inline]
fn rotate_z_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
self.into_inner().rotate_z_by(angle, duration, ease_method)
}
}
impl<'a, C: TweenCommand> Deref for AnimatedEntityCommands<'a, C> {
type Target = EntityCommands<'a>;
fn deref(&self) -> &Self::Target {
&self.commands
}
}
impl<C: TweenCommand> DerefMut for AnimatedEntityCommands<'_, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.flush();
&mut self.commands
}
}
impl<C: TweenCommand> Drop for AnimatedEntityCommands<'_, C> {
fn drop(&mut self) {
self.flush();
}
}
impl<'a> EntityCommandsTweeningExtensions<'a> for EntityCommands<'a> {
#[inline]
fn move_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
MoveToCommand {
end,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn move_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
MoveFromCommand {
start,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn scale_to(
self,
end: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
ScaleToCommand {
end,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn scale_from(
self,
start: Vec3,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
ScaleFromCommand {
start,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateXCommand {
config: TweenConfig {
ease_method: EaseFunction::Linear.into(),
cycle_duration,
..default()
},
},
)
}
#[inline]
fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateYCommand {
config: TweenConfig {
ease_method: EaseFunction::Linear.into(),
cycle_duration,
..default()
},
},
)
}
#[inline]
fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateZCommand {
config: TweenConfig {
ease_method: EaseFunction::Linear.into(),
cycle_duration,
..default()
},
},
)
}
#[inline]
fn rotate_x_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateXByCommand {
angle,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn rotate_y_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateYByCommand {
angle,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
#[inline]
fn rotate_z_by(
self,
angle: f32,
duration: Duration,
ease_method: impl Into<EaseMethod>,
) -> AnimatedEntityCommands<'a, impl TweenCommand> {
AnimatedEntityCommands::new(
self,
RotateZByCommand {
angle,
config: TweenConfig {
ease_method: ease_method.into(),
cycle_duration: duration,
..default()
},
},
)
}
}
#[derive(Debug, Clone, Copy, EntityEvent, Message)]
pub struct AnimCompletedEvent {
#[event_target]
pub anim_entity: Entity,
pub target: AnimTargetKind,
}
#[derive(Debug, Error, Clone, Copy)]
pub enum TweeningError {
#[error("Asset resolver for asset with resource ID {0:?} is not registered.")]
AssetResolverNotRegistered(ComponentId),
#[error("Entity {0:?} not found in the World.")]
EntityNotFound(Entity),
#[error("Entity {0:?} doesn't have a TweenAnim.")]
MissingTweenAnim(Entity),
#[error("Component of type {0:?} is not registered in the World.")]
ComponentNotRegistered(TypeId),
#[error("Resource of type {0:?} is not registered in the World.")]
ResourceNotRegistered(TypeId),
#[error("Asset container Assets<A> for asset type A = {0:?} is not registered in the World.")]
AssetNotRegistered(TypeId),
#[error("Component of type {0:?} is not present on entity {1:?}.")]
MissingComponent(TypeId, Entity),
#[error("Asset ID {0:?} is invalid.")]
InvalidAssetId(UntypedAssetId),
#[error("Expected type of asset ID to be {expected:?} but got {actual:?} instead.")]
InvalidAssetIdType {
expected: TypeId,
actual: TypeId,
},
#[error("Expected a typed Tweenable.")]
UntypedTweenable,
#[error("Invalid Entity {0:?}.")]
InvalidTweenId(Entity),
#[error("Unexpected target kind: was component={0}, now component={1}")]
MismatchingTargetKind(bool, bool),
#[error("Cannot change component type: was component_id={0:?}, now component_id={1:?}")]
MismatchingComponentId(ComponentId, ComponentId),
#[error("Cannot change asset type: was component_id={0:?}, now component_id={1:?}")]
MismatchingAssetResourceId(ComponentId, ComponentId),
}
type RegisterAction = dyn Fn(&Components, &mut TweenResolver) + Send + Sync + 'static;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AnimTargetKind {
Component {
entity: Entity,
},
Resource,
Asset {
asset_id: UntypedAssetId,
assets_type_id: TypeId,
},
}
#[derive(Component)]
pub struct AnimTarget {
pub kind: AnimTargetKind,
pub(crate) register_action: Option<Box<RegisterAction>>,
}
impl AnimTarget {
pub fn component<C: Component<Mutability = Mutable>>(entity: Entity) -> Self {
Self {
kind: AnimTargetKind::Component { entity },
register_action: None,
}
}
pub fn resource<R: Resource>() -> Self {
let register_action = |components: &Components, resolver: &mut TweenResolver| {
resolver.register_resource_resolver_for::<R>(components);
};
Self {
kind: AnimTargetKind::Resource,
register_action: Some(Box::new(register_action)),
}
}
pub fn asset<A: Asset>(asset_id: impl Into<AssetId<A>>) -> Self {
let register_action = |components: &Components, resolver: &mut TweenResolver| {
resolver.register_asset_resolver_for::<A>(components);
};
Self {
kind: AnimTargetKind::Asset {
asset_id: asset_id.into().untyped(),
assets_type_id: TypeId::of::<Assets<A>>(),
},
register_action: Some(Box::new(register_action)),
}
}
pub(crate) fn register(&self, components: &Components, resolver: &mut TweenResolver) {
if let Some(register_action) = self.register_action.as_ref() {
register_action(components, resolver);
}
}
}
#[derive(Component)]
pub struct TweenAnim {
tweenable: BoxedTweenable,
pub playback_state: PlaybackState,
pub speed: f64,
pub destroy_on_completion: bool,
tween_state: TweenState,
}
impl TweenAnim {
#[inline]
pub fn new(tweenable: impl IntoBoxedTweenable) -> Self {
let tweenable = tweenable.into_boxed();
assert!(
tweenable.target_type_id().is_some(),
"The top-level Tweenable of a TweenAnim must be typed (Tweenable::target_type_id() returns Some)."
);
Self {
tweenable,
playback_state: PlaybackState::Playing,
speed: 1.,
destroy_on_completion: true,
tween_state: TweenState::Active,
}
}
pub fn with_speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
pub fn with_destroy_on_completed(mut self, destroy_on_completed: bool) -> Self {
self.destroy_on_completion = destroy_on_completed;
self
}
#[inline]
pub fn step_one(
world: &mut World,
delta_time: Duration,
entity: Entity,
) -> Result<(), TweeningError> {
let num = Self::step_many(world, delta_time, &[entity]);
if num > 0 {
Ok(())
} else {
Err(TweeningError::EntityNotFound(entity))
}
}
pub fn step_many(world: &mut World, delta_time: Duration, anims: &[Entity]) -> usize {
let mut targets = vec![];
world.resource_scope(|world, mut resolver: Mut<TweenResolver>| {
let mut q_anims = world.query::<(Entity, &TweenAnim, Option<&AnimTarget>)>();
targets.reserve(anims.len());
for entity in anims {
if let Ok((entity, anim, maybe_target)) = q_anims.get(world, *entity) {
if let Some(anim_target) = maybe_target {
anim_target.register(world.components(), &mut resolver);
}
if let Ok((target_type_id, component_id, target, is_retargetable)) =
Self::resolve_target(
world.components(),
maybe_target,
entity,
anim.tweenable(),
)
{
targets.push((
entity,
target_type_id,
component_id,
target,
is_retargetable,
));
}
}
}
});
Self::step_impl(world, delta_time, &targets[..]);
targets.len()
}
pub fn step_all(world: &mut World, delta_time: Duration) {
let targets = world.resource_scope(|world, mut resolver: Mut<TweenResolver>| {
let mut q_anims = world.query::<(Entity, &TweenAnim, Option<&AnimTarget>)>();
q_anims
.iter(world)
.filter_map(|(entity, anim, maybe_target)| {
if let Some(anim_target) = maybe_target {
anim_target.register(world.components(), &mut resolver);
}
match Self::resolve_target(
world.components(),
maybe_target,
entity,
anim.tweenable(),
) {
Ok((target_type_id, component_id, target, is_retargetable)) => Some((
entity,
target_type_id,
component_id,
target,
is_retargetable,
)),
Err(err) => {
bevy::log::error!(
"Error while stepping TweenAnim on entity {:?}: {:?}",
entity,
err
);
None
}
}
})
.collect::<Vec<_>>()
});
Self::step_impl(world, delta_time, &targets[..]);
}
fn resolve_target(
components: &Components,
maybe_target: Option<&AnimTarget>,
anim_entity: Entity,
tweenable: &dyn Tweenable,
) -> Result<(TypeId, ComponentId, AnimTargetKind, bool), TweeningError> {
let type_id = tweenable
.target_type_id()
.ok_or(TweeningError::UntypedTweenable)?;
if let Some(target) = maybe_target {
let component_id = match &target.kind {
AnimTargetKind::Component { .. } => components
.get_id(type_id)
.ok_or(TweeningError::ComponentNotRegistered(type_id))?,
AnimTargetKind::Resource => components
.get_resource_id(type_id)
.ok_or(TweeningError::ResourceNotRegistered(type_id))?,
AnimTargetKind::Asset { assets_type_id, .. } => components
.get_resource_id(*assets_type_id)
.ok_or(TweeningError::AssetNotRegistered(type_id))?,
};
let is_retargetable = false; Ok((type_id, component_id, target.kind, is_retargetable))
} else {
let is_retargetable = true;
if let Some(component_id) = components.get_id(type_id) {
Ok((
type_id,
component_id,
AnimTargetKind::Component {
entity: anim_entity,
},
is_retargetable,
))
} else {
Err(TweeningError::ComponentNotRegistered(type_id))
}
}
}
fn step_impl(
world: &mut World,
delta_time: Duration,
anims: &[(Entity, TypeId, ComponentId, AnimTargetKind, bool)],
) {
let mut to_remove = Vec::with_capacity(anims.len());
world.resource_scope(|world, resolver: Mut<TweenResolver>| {
world.resource_scope(
|world, mut cycle_events: Mut<Messages<CycleCompletedEvent>>| {
world.resource_scope(
|world, mut anim_events: Mut<Messages<AnimCompletedEvent>>| {
let anim_comp_id = world.component_id::<TweenAnim>().unwrap();
for (
anim_entity,
target_type_id,
component_id,
anim_target,
is_retargetable,
) in anims
{
let retain = match anim_target {
AnimTargetKind::Component {
entity: comp_entity,
} => {
let (mut entities, commands) =
world.entities_and_commands();
let ret = if *anim_entity == *comp_entity {
let Ok([mut ent]) = entities.get_mut([*anim_entity])
else {
continue;
};
let Ok([anim, target]) =
ent.get_mut_by_id([anim_comp_id, *component_id])
else {
continue;
};
#[allow(unsafe_code)]
let mut anim = unsafe { anim.with_type::<TweenAnim>() };
anim.step_self(
commands,
*anim_entity,
delta_time,
anim_target,
target,
target_type_id,
cycle_events.reborrow(),
anim_events.reborrow(),
)
} else {
let Ok([mut anim, mut target]) =
entities.get_mut([*anim_entity, *comp_entity])
else {
continue;
};
let Some(mut anim) = anim.get_mut::<TweenAnim>() else {
continue;
};
let Ok(target) = target.get_mut_by_id(*component_id)
else {
continue;
};
anim.step_self(
commands,
*anim_entity,
delta_time,
anim_target,
target,
target_type_id,
cycle_events.reborrow(),
anim_events.reborrow(),
)
};
match ret {
Ok(res) => {
if res.needs_retarget {
assert!(res.retain);
if *is_retargetable {
bevy::log::warn!("TODO: Multi-target tweenable sequence is not yet supported. Ensure the animation of the TweenAnim component on entity {:?} targets a single component type.", *anim_entity);
false
} else {
bevy::log::warn!("Multi-target tweenable sequence cannot be used with an explicit single target. Remove the AnimTarget component from entity {:?}, or ensure all tweenables in the sequence target the same component.", *anim_entity);
false
}
} else {
res.retain
}
}
Err(_) => false,
}
}
AnimTargetKind::Resource => resolver
.resolve_resource(
world,
target_type_id,
*component_id,
*anim_entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.unwrap_or_else(|err| {
bevy::log::error!(
"Deleting resource animation due to error: {err:?}"
);
false
}),
AnimTargetKind::Asset { asset_id, .. } => resolver
.resolve_asset(
world,
target_type_id,
*component_id,
*asset_id,
*anim_entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.unwrap_or_else(|err| {
bevy::log::error!(
"Deleting asset animation due to error: {err:?}"
);
false
}),
};
if !retain {
to_remove.push(*anim_entity);
}
}
},
);
},
);
});
let mut cmds = world.commands();
for entity in to_remove.drain(..) {
cmds.entity(entity).try_remove::<TweenAnim>();
}
world.flush();
}
#[allow(clippy::too_many_arguments)]
fn step_self(
&mut self,
mut commands: Commands,
anim_entity: Entity,
delta_time: Duration,
target_kind: &AnimTargetKind,
mut mut_untyped: MutUntyped,
target_type_id: &TypeId,
mut cycle_events: Mut<Messages<CycleCompletedEvent>>,
mut anim_events: Mut<Messages<AnimCompletedEvent>>,
) -> Result<StepResult, TweeningError> {
let mut completed_events = Vec::with_capacity(8);
self.speed = self.speed.max(0.);
if self.tween_state == TweenState::Completed {
let ret = StepResult {
retain: !self.destroy_on_completion,
needs_retarget: false,
};
return Ok(ret);
}
if self.playback_state == PlaybackState::Paused || self.speed <= 0. {
let ret = StepResult {
retain: true,
needs_retarget: false,
};
return Ok(ret);
}
let delta_time = delta_time.mul_f64(self.speed);
let mut notify_completed = || {
completed_events.push(CycleCompletedEvent {
anim_entity,
target: *target_kind,
});
};
let (state, needs_retarget) = self.tweenable.step(
anim_entity,
delta_time,
mut_untyped.reborrow(),
target_type_id,
&mut notify_completed,
);
self.tween_state = state;
if !completed_events.is_empty() {
for event in completed_events.drain(..) {
cycle_events.write(event);
commands.trigger(CycleCompletedEvent {
anim_entity,
..event
});
}
}
if state == TweenState::Completed {
let event: AnimCompletedEvent = AnimCompletedEvent {
anim_entity,
target: *target_kind,
};
anim_events.write(event);
commands.trigger(event);
}
let ret = StepResult {
retain: state == TweenState::Active || !self.destroy_on_completion,
needs_retarget,
};
Ok(ret)
}
pub fn stop(&mut self) {
self.playback_state = PlaybackState::Paused;
self.tweenable.rewind();
self.tween_state = TweenState::Active;
}
#[inline]
pub fn tweenable(&self) -> &dyn Tweenable {
self.tweenable.as_ref()
}
pub fn set_tweenable<T>(&mut self, tweenable: T) -> Result<BoxedTweenable, TweeningError>
where
T: Tweenable + 'static,
{
let mut old_tweenable: BoxedTweenable = Box::new(tweenable);
std::mem::swap(&mut self.tweenable, &mut old_tweenable);
self.tween_state = TweenState::Active;
Ok(old_tweenable)
}
#[inline]
pub fn tween_state(&self) -> TweenState {
self.tween_state
}
}
type ResourceResolver = Box<
dyn for<'w> Fn(
&mut World,
Entity,
&TypeId,
Duration,
Mut<Messages<CycleCompletedEvent>>,
Mut<Messages<AnimCompletedEvent>>,
) -> Result<bool, TweeningError>
+ Send
+ Sync
+ 'static,
>;
type AssetResolver = Box<
dyn for<'w> Fn(
&mut World,
UntypedAssetId,
Entity,
&TypeId,
Duration,
Mut<Messages<CycleCompletedEvent>>,
Mut<Messages<AnimCompletedEvent>>,
) -> Result<bool, TweeningError>
+ Send
+ Sync
+ 'static,
>;
#[derive(Default, Resource)]
pub struct TweenResolver {
resource_resolver: HashMap<ComponentId, ResourceResolver>,
asset_resolver: HashMap<ComponentId, AssetResolver>,
}
impl TweenResolver {
pub(crate) fn register_resource_resolver_for<R: Resource>(&mut self, components: &Components) {
let resource_id = components.resource_id::<R>().unwrap();
let resolver = |world: &mut World,
entity: Entity,
target_type_id: &TypeId,
delta_time: Duration,
mut cycle_events: Mut<Messages<CycleCompletedEvent>>,
mut anim_events: Mut<Messages<AnimCompletedEvent>>|
-> Result<bool, TweeningError> {
world.resource_scope(|world, resource: Mut<R>| {
let target = AnimTargetKind::Resource;
let (mut entities, commands) = world.entities_and_commands();
let Ok([mut ent]) = entities.get_mut([entity]) else {
return Err(TweeningError::EntityNotFound(entity));
};
let Some(mut anim) = ent.get_mut::<TweenAnim>() else {
return Err(TweeningError::MissingTweenAnim(ent.id()));
};
let ret = anim.step_self(
commands,
entity,
delta_time,
&target,
resource.into(),
target_type_id,
cycle_events.reborrow(),
anim_events.reborrow(),
);
ret.map(|result| {
assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with a resource target.");
result.retain
})
})
};
self.resource_resolver
.entry(resource_id)
.or_insert(Box::new(resolver));
}
pub(crate) fn register_asset_resolver_for<A: Asset>(&mut self, components: &Components) {
let resource_id = components.resource_id::<Assets<A>>().unwrap();
let resolver = |world: &mut World,
asset_id: UntypedAssetId,
entity: Entity,
target_type_id: &TypeId,
delta_time: Duration,
mut cycle_events: Mut<Messages<CycleCompletedEvent>>,
mut anim_events: Mut<Messages<AnimCompletedEvent>>|
-> Result<bool, TweeningError> {
let asset_id = asset_id.typed::<A>();
world.resource_scope(|world, assets: Mut<Assets<A>>| {
let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut(asset_id))
else {
return Err(TweeningError::InvalidAssetId(asset_id.into()));
};
let target = AnimTargetKind::Asset {
asset_id: asset_id.untyped(),
assets_type_id: TypeId::of::<Assets<A>>(),
};
let (mut entities, commands) = world.entities_and_commands();
let Ok([mut ent]) = entities.get_mut([entity]) else {
return Err(TweeningError::EntityNotFound(entity));
};
let Some(mut anim) = ent.get_mut::<TweenAnim>() else {
return Err(TweeningError::MissingTweenAnim(ent.id()));
};
let ret = anim.step_self(
commands,
entity,
delta_time,
&target,
asset.into(),
target_type_id,
cycle_events.reborrow(),
anim_events.reborrow(),
);
ret.map(|result| {
assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with an asset target.");
result.retain
})
})
};
self.asset_resolver
.entry(resource_id)
.or_insert(Box::new(resolver));
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub(crate) fn resolve_resource(
&self,
world: &mut World,
target_type_id: &TypeId,
resource_id: ComponentId,
entity: Entity,
delta_time: Duration,
cycle_events: Mut<Messages<CycleCompletedEvent>>,
anim_events: Mut<Messages<AnimCompletedEvent>>,
) -> Result<bool, TweeningError> {
let Some(resolver) = self.resource_resolver.get(&resource_id) else {
println!("ERROR: resource not registered {:?}", resource_id);
return Err(TweeningError::AssetResolverNotRegistered(resource_id));
};
resolver(
world,
entity,
target_type_id,
delta_time,
cycle_events,
anim_events,
)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub(crate) fn resolve_asset(
&self,
world: &mut World,
target_type_id: &TypeId,
resource_id: ComponentId,
untyped_asset_id: UntypedAssetId,
entity: Entity,
delta_time: Duration,
cycle_events: Mut<Messages<CycleCompletedEvent>>,
anim_events: Mut<Messages<AnimCompletedEvent>>,
) -> Result<bool, TweeningError> {
let Some(resolver) = self.asset_resolver.get(&resource_id) else {
println!("ERROR: asset not registered {:?}", resource_id);
return Err(TweeningError::AssetResolverNotRegistered(resource_id));
};
resolver(
world,
untyped_asset_id,
entity,
target_type_id,
delta_time,
cycle_events,
anim_events,
)
}
}
pub(crate) struct StepResult {
pub retain: bool,
pub needs_retarget: bool,
}
#[cfg(test)]
mod tests {
use std::{
f32::consts::{FRAC_PI_2, TAU},
marker::PhantomData,
};
use bevy::ecs::{change_detection::MaybeLocation, change_detection::Tick};
use super::*;
use crate::test_utils::*;
struct DummyLens {
start: f32,
end: f32,
}
struct DummyLens2 {
start: i32,
end: i32,
}
#[derive(Debug, Default, Clone, Copy, Component)]
struct DummyComponent {
value: f32,
}
#[derive(Debug, Default, Clone, Copy, Component)]
struct DummyComponent2 {
value: i32,
}
#[derive(Debug, Default, Clone, Copy, Resource)]
struct DummyResource {
value: f32,
}
#[derive(Asset, Debug, Default, Reflect)]
struct DummyAsset {
value: f32,
}
impl Lens<DummyComponent> for DummyLens {
fn lerp(&mut self, mut target: Mut<DummyComponent>, ratio: f32) {
target.value = self.start.lerp(self.end, ratio);
}
}
impl Lens<DummyComponent2> for DummyLens2 {
fn lerp(&mut self, mut target: Mut<DummyComponent2>, ratio: f32) {
target.value = ((self.start as f32) * (1. - ratio) + (self.end as f32) * ratio) as i32;
}
}
#[test]
fn dummy_lens_component() {
let mut c = DummyComponent::default();
let mut l = DummyLens { start: 0., end: 1. };
for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] {
{
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(),
);
l.lerp(target.reborrow(), r);
assert!(target.is_changed());
}
assert_approx_eq!(c.value, r);
}
}
impl Lens<DummyResource> for DummyLens {
fn lerp(&mut self, mut target: Mut<DummyResource>, ratio: f32) {
target.value = self.start.lerp(self.end, ratio);
}
}
#[test]
fn dummy_lens_resource() {
let mut res = DummyResource::default();
let mut l = DummyLens { start: 0., end: 1. };
for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] {
{
let mut added = Tick::new(0);
let mut last_changed = Tick::new(0);
let mut caller = MaybeLocation::caller();
let mut target = Mut::new(
&mut res,
&mut added,
&mut last_changed,
Tick::new(0),
Tick::new(0),
caller.as_mut(),
);
l.lerp(target.reborrow(), r);
}
assert_approx_eq!(res.value, r);
}
}
impl Lens<DummyAsset> for DummyLens {
fn lerp(&mut self, mut target: Mut<DummyAsset>, ratio: f32) {
target.value = self.start.lerp(self.end, ratio);
}
}
#[test]
fn dummy_lens_asset() {
let mut assets = Assets::<DummyAsset>::default();
let handle = assets.add(DummyAsset::default());
let mut l = DummyLens { start: 0., end: 1. };
for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] {
{
let mut added = Tick::new(0);
let mut last_changed = Tick::new(0);
let mut caller = MaybeLocation::caller();
let asset = assets.get_mut(handle.id()).unwrap();
let target = Mut::new(
asset,
&mut added,
&mut last_changed,
Tick::new(0),
Tick::new(0),
caller.as_mut(),
);
l.lerp(target, r);
}
assert_approx_eq!(assets.get(handle.id()).unwrap().value, r);
}
}
#[test]
fn repeat_count() {
let cycle_duration = Duration::from_millis(100);
let repeat = RepeatCount::default();
assert_eq!(repeat, RepeatCount::Finite(1));
assert_eq!(
repeat.total_duration(cycle_duration),
TotalDuration::Finite(cycle_duration)
);
let repeat: RepeatCount = 3u32.into();
assert_eq!(repeat, RepeatCount::Finite(3));
assert_eq!(
repeat.total_duration(cycle_duration),
TotalDuration::Finite(cycle_duration * 3)
);
let duration = Duration::from_secs(5);
let repeat: RepeatCount = duration.into();
assert_eq!(repeat, RepeatCount::For(duration));
assert_eq!(
repeat.total_duration(cycle_duration),
TotalDuration::Finite(duration)
);
let repeat = RepeatCount::Infinite;
assert_eq!(
repeat.total_duration(cycle_duration),
TotalDuration::Infinite
);
}
#[test]
fn repeat_strategy() {
let strategy = RepeatStrategy::default();
assert_eq!(strategy, RepeatStrategy::Repeat);
}
#[test]
fn playback_direction() {
let tweening_direction = PlaybackDirection::default();
assert_eq!(tweening_direction, PlaybackDirection::Forward);
}
#[test]
fn playback_state() {
let mut state = PlaybackState::default();
assert_eq!(state, PlaybackState::Playing);
state = !state;
assert_eq!(state, PlaybackState::Paused);
state = !state;
assert_eq!(state, PlaybackState::Playing);
}
#[test]
fn ease_method() {
let ease = EaseMethod::default();
assert!(matches!(
ease,
EaseMethod::EaseFunction(EaseFunction::Linear)
));
let ease = EaseMethod::EaseFunction(EaseFunction::QuadraticIn);
assert_eq!(0., ease.sample(0.));
assert_eq!(0.25, ease.sample(0.5));
assert_eq!(1., ease.sample(1.));
let ease = EaseMethod::EaseFunction(EaseFunction::Linear);
assert_eq!(0., ease.sample(0.));
assert_eq!(0.5, ease.sample(0.5));
assert_eq!(1., ease.sample(1.));
let ease = EaseMethod::Discrete(0.3);
assert_eq!(0., ease.sample(0.));
assert_eq!(1., ease.sample(0.5));
assert_eq!(1., ease.sample(1.));
let ease = EaseMethod::CustomFunction(|f| 1. - f);
assert_eq!(0., ease.sample(1.));
assert_eq!(0.5, ease.sample(0.5));
assert_eq!(1., ease.sample(0.));
}
#[test]
fn animation_playback_state() {
for state in [PlaybackState::Playing, PlaybackState::Paused] {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(tween);
let mut anim = env.anim_mut().unwrap();
anim.playback_state = state;
anim.destroy_on_completion = false;
let dt = Duration::from_millis(100);
env.step_all(dt);
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active);
assert_eq!(env.anim().unwrap().playback_state, state);
let elapsed = match state {
PlaybackState::Playing => dt,
PlaybackState::Paused => Duration::ZERO,
};
assert_eq!(env.anim().unwrap().tweenable.elapsed(), elapsed);
env.anim_mut().unwrap().playback_state = PlaybackState::Playing;
env.step_all(Duration::from_secs(10) - elapsed);
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Completed);
assert_eq!(env.anim().unwrap().playback_state, PlaybackState::Playing);
}
}
#[test]
fn animation_events() {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
)
.with_repeat_count(2)
.with_cycle_completed_event(true);
let mut env = TestEnv::<DummyComponent>::new(tween);
let dt = Duration::from_millis(1200);
env.step_all(dt);
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active);
assert_eq!(env.event_count::<CycleCompletedEvent>(), 1);
assert_eq!(env.event_count::<AnimCompletedEvent>(), 0);
let dt = Duration::from_millis(1000);
env.step_all(dt);
assert!(env.anim().is_none());
assert_eq!(env.event_count::<CycleCompletedEvent>(), 1);
assert_eq!(env.event_count::<AnimCompletedEvent>(), 1);
}
#[derive(Debug, Resource)]
struct Count<E: Event, T = ()> {
pub count: i32,
pub phantom: PhantomData<E>,
pub phantom2: PhantomData<T>,
}
impl<E: Event, T> Default for Count<E, T> {
fn default() -> Self {
Self {
count: 0,
phantom: PhantomData,
phantom2: PhantomData,
}
}
}
struct GlobalMarker;
#[test]
fn animation_observe() {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
)
.with_repeat_count(2)
.with_cycle_completed_event(true);
let mut env = TestEnv::<DummyComponent>::new(tween);
env.world.init_resource::<Count<CycleCompletedEvent>>();
assert_eq!(env.world.resource::<Count<CycleCompletedEvent>>().count, 0);
env.world
.init_resource::<Count<CycleCompletedEvent, GlobalMarker>>();
assert_eq!(
env.world
.resource::<Count<CycleCompletedEvent, GlobalMarker>>()
.count,
0
);
fn observe_global(
_trigger: On<CycleCompletedEvent>,
mut count: ResMut<Count<CycleCompletedEvent, GlobalMarker>>,
) {
count.count += 1;
}
env.world.add_observer(observe_global);
fn observe_entity(
_trigger: On<CycleCompletedEvent>,
mut count: ResMut<Count<CycleCompletedEvent>>,
) {
count.count += 1;
}
env.world.entity_mut(env.entity).observe(observe_entity);
let dt = Duration::from_millis(1200);
env.step_all(dt);
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active);
assert_eq!(env.world.resource::<Count<CycleCompletedEvent>>().count, 1);
assert_eq!(
env.world
.resource::<Count<CycleCompletedEvent, GlobalMarker>>()
.count,
1
);
let dt = Duration::from_millis(1000);
env.step_all(dt);
assert!(env.anim().is_none());
assert_eq!(env.world.resource::<Count<CycleCompletedEvent>>().count, 2);
assert_eq!(
env.world
.resource::<Count<CycleCompletedEvent, GlobalMarker>>()
.count,
2
);
}
#[test]
fn animation_speed() {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(tween);
assert_approx_eq!(env.anim().unwrap().speed, 1.);
env.anim_mut().unwrap().speed = 2.4;
assert_approx_eq!(env.anim().unwrap().speed, 2.4);
env.step_all(Duration::from_millis(100));
assert_eq!(
env.anim().unwrap().tweenable.elapsed(),
Duration::from_millis(240)
);
env.anim_mut().unwrap().speed = -1.;
env.step_all(Duration::from_millis(100));
assert_eq!(env.anim().unwrap().speed, 0.);
assert_eq!(
env.anim().unwrap().tweenable.elapsed(),
Duration::from_millis(240)
);
}
#[test]
fn animator_set_tweenable() {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let tween2 = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::SmoothStep,
Duration::from_secs(2),
DummyLens { start: 2., end: 3. },
);
let mut env = TestEnv::<DummyComponent>::new(tween);
env.anim_mut().unwrap().destroy_on_completion = false;
let dt = Duration::from_millis(1500);
env.step_all(dt);
assert_eq!(env.component().value, 1.);
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Completed);
let old_tweenable = env.anim_mut().unwrap().set_tweenable(tween2).unwrap();
assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active);
assert_eq!(old_tweenable.elapsed(), Duration::from_secs(1)); assert_eq!(env.anim().unwrap().tweenable.elapsed(), Duration::ZERO);
env.step_all(dt);
assert!(env.component().value >= 2. && env.component().value <= 3.);
}
#[test]
#[should_panic(
expected = "TODO: Cannot use tweenable animations with different targets inside the same Sequence. Create separate animations for each target."
)]
fn seq_multi_target() {
let tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
)
.then(Tween::new::<DummyComponent2, DummyLens2>(
EaseFunction::SmoothStep,
Duration::from_secs(1),
DummyLens2 { start: -5, end: 5 },
));
let mut env = TestEnv::<DummyComponent>::new(tween);
let entity = env.entity;
env.world
.entity_mut(entity)
.insert(DummyComponent2 { value: -42 });
TweenAnim::step_one(&mut env.world, Duration::from_millis(1100), entity).unwrap();
}
#[test]
fn anim_target_component() {
let mut env = TestEnv::<Transform>::empty();
let entity = env.world.spawn(Transform::default()).id();
let tween = Tween::new::<Transform, TransformPositionLens>(
EaseFunction::Linear,
Duration::from_secs(1),
TransformPositionLens {
start: Vec3::ZERO,
end: Vec3::ONE,
},
);
let target = AnimTarget::component::<Transform>(entity);
let anim_entity = env
.world
.spawn((
TweenAnim::new(tween)
.with_speed(2.)
.with_destroy_on_completed(true),
target,
))
.id();
assert!(
TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok()
);
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.translation, Vec3::ONE * 0.2);
assert_eq!(
TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]),
1
);
assert!(env.world.entity(anim_entity).get::<TweenAnim>().is_none());
}
#[test]
fn anim_target_resource() {
let mut env = TestEnv::<Transform>::empty();
env.world.init_resource::<DummyResource>();
let tween = Tween::new::<DummyResource, DummyLens>(
EaseFunction::Linear,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let target = AnimTarget::resource::<DummyResource>();
let anim_entity = env
.world
.spawn((
TweenAnim::new(tween)
.with_speed(2.)
.with_destroy_on_completed(true),
target,
))
.id();
assert!(
TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok()
);
let res = env.world.resource::<DummyResource>();
assert_eq!(res.value, 0.2);
assert_eq!(
TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]),
1
);
assert!(env.world.entity(anim_entity).get::<TweenAnim>().is_none());
}
#[test]
fn anim_target_asset() {
let mut env = TestEnv::<Transform>::empty();
let mut assets = Assets::<DummyAsset>::default();
let handle = assets.add(DummyAsset::default());
env.world.insert_resource(assets);
let tween = Tween::new::<DummyAsset, DummyLens>(
EaseFunction::Linear,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let target = AnimTarget::asset::<DummyAsset>(&handle);
let anim_entity = env
.world
.spawn((
TweenAnim::new(tween)
.with_speed(2.)
.with_destroy_on_completed(true),
target,
))
.id();
assert!(
TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok()
);
let assets = env.world.resource::<Assets<DummyAsset>>();
let asset = assets.get(&handle).unwrap();
assert_eq!(asset.value, 0.2);
assert_eq!(
TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]),
1
);
assert!(env.world.entity(anim_entity).get::<TweenAnim>().is_none());
}
#[test]
fn animated_entity_commands_common() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear)
.with_repeat_count(4)
.with_repeat_strategy(RepeatStrategy::MirroredRepeat)
.id();
let entity2 = env
.world
.commands()
.spawn(Transform::default())
.move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear)
.with_repeat(4, RepeatStrategy::MirroredRepeat)
.into_inner()
.id();
env.world.flush();
env.step_all(Duration::from_millis(3300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.translation, Vec3::ONE * 0.7);
let tr = env.world.entity(entity2).get::<Transform>().unwrap();
assert_eq!(tr.translation, Vec3::ONE * 0.7);
}
#[test]
fn animated_entity_commands_move_to() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.translation, Vec3::ONE * 0.3);
}
#[test]
fn animated_entity_commands_move_from() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.move_from(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.translation, Vec3::ONE * 0.7);
}
#[test]
fn animated_entity_commands_scale_to() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.scale_to(Vec3::ONE * 2., Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.scale, Vec3::ONE * 1.3);
}
#[test]
fn animated_entity_commands_scale_from() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.scale_from(Vec3::ONE * 2., Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.scale, Vec3::ONE * 1.7);
}
#[test]
fn animated_entity_commands_rotate_x() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_x(Duration::from_secs(1))
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_x(TAU * 0.3));
}
#[test]
fn animated_entity_commands_rotate_y() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_y(Duration::from_secs(1))
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_y(TAU * 0.3));
}
#[test]
fn animated_entity_commands_rotate_z() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_z(Duration::from_secs(1))
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_z(TAU * 0.3));
}
#[test]
fn animated_entity_commands_rotate_x_by() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_x_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_x(FRAC_PI_2)); }
#[test]
fn animated_entity_commands_rotate_y_by() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_y_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_y(FRAC_PI_2)); }
#[test]
fn animated_entity_commands_rotate_z_by() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let entity = env
.world
.commands()
.spawn(Transform::default())
.rotate_z_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear)
.id();
env.world.flush();
env.step_all(Duration::from_millis(1300));
let tr = env.world.entity(entity).get::<Transform>().unwrap();
assert_eq!(tr.rotation, Quat::from_rotation_z(FRAC_PI_2)); }
#[test]
fn resolver_resource() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
env.world.init_resource::<DummyResource>();
let tween = Tween::new::<DummyResource, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let entity = env.world.commands().spawn(TweenAnim::new(tween)).id();
env.world.flush();
let delta_time = Duration::from_millis(200);
let resource_id = env.world.resource_id::<DummyResource>().unwrap();
env.world
.resource_scope(|world, resolver: Mut<TweenResolver>| {
world.resource_scope(
|world, mut cycle_events: Mut<Messages<CycleCompletedEvent>>| {
world.resource_scope(
|world, mut anim_events: Mut<Messages<AnimCompletedEvent>>| {
assert!(resolver
.resolve_resource(
world,
&TypeId::of::<DummyResource>(),
resource_id,
entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.is_err());
},
);
},
);
});
env.world
.resource_scope(|world, mut resolver: Mut<TweenResolver>| {
resolver.register_resource_resolver_for::<DummyResource>(world.components());
});
env.world
.resource_scope(|world, resolver: Mut<TweenResolver>| {
world.resource_scope(
|world, mut cycle_events: Mut<Messages<CycleCompletedEvent>>| {
world.resource_scope(
|world, mut anim_events: Mut<Messages<AnimCompletedEvent>>| {
assert!(resolver
.resolve_resource(
world,
&TypeId::of::<DummyResource>(),
resource_id,
entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.unwrap());
},
);
},
);
});
}
#[test]
fn resolver_asset() {
let dummy_tween = Tween::new::<DummyComponent, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut env = TestEnv::<DummyComponent>::new(dummy_tween);
let mut assets = Assets::<DummyAsset>::default();
let handle = assets.add(DummyAsset::default());
let untyped_asset_id = handle.id().untyped();
env.world.insert_resource(assets);
let tween = Tween::new::<DummyAsset, DummyLens>(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let entity = env.world.commands().spawn(TweenAnim::new(tween)).id();
env.world.flush();
let delta_time = Duration::from_millis(200);
let resource_id = env.world.resource_id::<Assets<DummyAsset>>().unwrap();
env.world
.resource_scope(|world, resolver: Mut<TweenResolver>| {
world.resource_scope(
|world, mut cycle_events: Mut<Messages<CycleCompletedEvent>>| {
world.resource_scope(
|world, mut anim_events: Mut<Messages<AnimCompletedEvent>>| {
assert!(resolver
.resolve_asset(
world,
&TypeId::of::<DummyAsset>(),
resource_id,
untyped_asset_id,
entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.is_err());
},
);
},
);
});
env.world
.resource_scope(|world, mut resolver: Mut<TweenResolver>| {
resolver.register_asset_resolver_for::<DummyAsset>(world.components());
});
env.world
.resource_scope(|world, resolver: Mut<TweenResolver>| {
world.resource_scope(
|world, mut cycle_events: Mut<Messages<CycleCompletedEvent>>| {
world.resource_scope(
|world, mut anim_events: Mut<Messages<AnimCompletedEvent>>| {
assert!(resolver
.resolve_asset(
world,
&TypeId::of::<DummyAsset>(),
resource_id,
untyped_asset_id,
entity,
delta_time,
cycle_events.reborrow(),
anim_events.reborrow(),
)
.unwrap());
},
);
},
);
});
}
}