Newer
Older
#![deny(
warnings,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
missing_docs
)]
//! Tweening animation plugin for the Bevy game engine
//!
//! 🍃 Bevy Tweening provides interpolation-based animation between ("tweening") two values, for Bevy components
//! and assets. Each field of a component or asset can be animated via a collection or predefined easing functions,
//! or providing a custom animation curve. Custom components and assets are also supported.
//!
//! # Example
//!
//! Add the tweening plugin to your app:
//!
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_tweening::*;
//!
//! App::default()
//! .add_plugins(DefaultPlugins)
//! .add_plugin(TweeningPlugin)
//! .run();
//! ```
//!
//! Animate the position ([`Transform::translation`]) of an [`Entity`]:
//!
//! # use std::time::Duration;
//! # fn system(mut commands: Commands) {
//! # let size = 16.;
//! // Create a single animation (tween) to move an entity.
//! let tween = Tween::new(
//! // Use a quadratic easing on both endpoints.
//! EaseFunction::QuadraticInOut,
//! // Loop animation back and forth.
//! TweeningType::PingPong,
//! // Animation time (one way only; for ping-pong it takes 2 seconds
//! // to come back to start).
//! Duration::from_secs(1),
//! // The lens gives access to the Transform component of the Entity,
//! // for the Animator to animate it. It also contains the start and
//! // end values respectively associated with the progress ratios 0. and 1.
//! end: Vec3::new(1., 2., -4.),
//! },
//! );
//! // Spawn a Sprite entity to animate the position of.
//! .spawn_bundle(SpriteBundle {
//! sprite: Sprite {
//! color: Color::RED,
//! custom_size: Some(Vec2::new(size, size)),
//! ..Default::default()
//! },
//! ..Default::default()
//! })
//! // Add an Animator component to control and execute the animation.
//! .insert(Animator::new(tween));
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! # Tweenables
//!
//! 🍃 Bevy Tweening supports several types of _tweenables_, building blocks that can be combined to form complex
//! animations. A tweenable is a type implementing the [`Tweenable`] trait.
//!
//! - [`Tween`] - A simple tween (easing) animation between two values.
//! - [`Sequence`] - A series of tweenables executing in series, one after the other.
//! - [`Tracks`] - A collection of tweenables executing in parallel.
//! - [`Delay`] - A time delay.
//!
//! ## Chaining animations
//!
//! Most tweenables can be chained with the `then()` operator to produce a [`Sequence`] tweenable:
//!
//! ```
//! # use bevy::prelude::*;
//! # use bevy_tweening::{lens::*, *};
//! # use std::time::Duration;
//! let tween1 = Tween::new(
//! // [...]
//! # EaseFunction::BounceOut,
//! # TweeningType::Once,
//! # Duration::from_secs(2),
//! # TransformScaleLens {
//! # start: Vec3::ZERO,
//! # end: Vec3::ONE,
//! # },
//! );
//! let tween2 = Tween::new(
//! // [...]
//! # EaseFunction::QuadraticInOut,
//! # TweeningType::Once,
//! # Duration::from_secs(1),
//! # TransformPositionLens {
//! # start: Vec3::ZERO,
//! # end: Vec3::new(3.5, 0., 0.),
//! # },
//! );
//! // Produce a Sequence executing first 'tween1' then 'tween2'
//! let seq = tween1.then(tween2);
//! ```
//!
//! # Animators and lenses
//!
//! Bevy components and assets are animated with tweening _animator_ components, which take a tweenable and
//! apply it to another component on the same [`Entity`]. Those animators determine that other component and
//! its fields to animate using a _lens_.
//!
//! ## Components animation
//!
//! Components are animated with the [`Animator`] component, which is generic over the type of component
//! it animates. This is a restriction imposed by Bevy, to access the animated component as a mutable
//! reference via a [`Query`] and comply with the ECS rules.
//!
//! The [`Animator`] itself is not generic over the subset of fields of the components it animates.
//! This limits the proliferation of generic types when animating e.g. both the position and rotation
//! of an entity.
//!
//! ## Assets animation
//!
//! Assets are animated in a similar way to component, via the [`AssetAnimator`] component.
//!
//! Because assets are typically shared, and the animation applies to the asset itself, all users of the asset
//! see the animation. For example, animating the color of a [`ColorMaterial`] will change the color of all the
//! 2D meshes using that material.
//!
//! ## Lenses
//!
//! Both [`Animator`] and [`AssetAnimator`] access the field(s) to animate via a lens, a type that implements
//! the [`Lens`] trait.
//!
//! Several predefined lenses are provided in the [`lens`] module for the most commonly animated fields, like the
//! components of a [`Transform`]. A custom lens can also be created by implementing the trait, allowing to animate
//! virtually any field of any Bevy component or asset.
//!
//! [`Transform::translation`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html#structfield.translation
//! [`Entity`]: https://docs.rs/bevy/0.6.0/bevy/ecs/entity/struct.Entity.html
//! [`Query`]: https://docs.rs/bevy/0.6.0/bevy/ecs/system/struct.Query.html
//! [`ColorMaterial`]: https://docs.rs/bevy/0.6.0/bevy/sprite/struct.ColorMaterial.html
//! [`Sprite`]: https://docs.rs/bevy/0.6.0/bevy/sprite/struct.Sprite.html
//! [`Transform`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html
use bevy::{asset::Asset, prelude::*};
use interpolation::Ease as IEase;
pub use interpolation::EaseFunction;
pub use interpolation::Lerp;
pub use plugin::{asset_animator_system, component_animator_system, TweeningPlugin};
pub use tweenable::{Delay, Sequence, Tracks, Tween, Tweenable};
/// Type of looping for a tween animation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Run the animation once from state to end only.
Once,
/// Looping, restarting from the start once finished.
Loop,
/// Repeat the animation back and forth.
PingPong,
}
/// Playback state of an animator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The animation is playing. This is the default state.
Playing,
/// The animation is paused/stopped.
Paused,
}
impl Default for AnimatorState {
fn default() -> Self {
AnimatorState::Playing
}
}
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
impl std::ops::Not for AnimatorState {
type Output = AnimatorState;
fn not(self) -> Self::Output {
match self {
AnimatorState::Paused => AnimatorState::Playing,
AnimatorState::Playing => AnimatorState::Paused,
}
}
}
/// Describe how eased value should be computed.
#[derive(Clone, Copy)]
pub enum EaseMethod {
/// Follow `EaseFunction`.
EaseFunction(EaseFunction),
/// Linear interpolation, with no function.
Linear,
/// Discrete interpolation, eased value will jump from start to end when
/// stepping over the discrete limit.
Discrete(f32),
/// Use a custom function to interpolate the value.
CustomFunction(fn(f32) -> f32),
}
impl EaseMethod {
fn sample(self, x: f32) -> f32 {
match self {
EaseMethod::EaseFunction(function) => x.calc(function),
EaseMethod::Linear => x,
EaseMethod::Discrete(limit) => {
if x > limit {
1.
} else {
0.
}
}
EaseMethod::CustomFunction(function) => function(x),
}
}
}
impl Into<EaseMethod> for EaseFunction {
fn into(self) -> EaseMethod {
EaseMethod::EaseFunction(self)
}
}
/// Direction a tweening animation is playing.
///
/// For all but [`TweeningType::PingPong`] this is always [`TweeningDirection::Forward`]. For the
/// [`TweeningType::PingPong`] tweening type, this is either forward (from start to end; ping) or
/// backward (from end to start; pong).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TweeningDirection {
/// Animation playing from start to end.
Forward,
/// Animation playing from end to start.
Backward,
}
impl std::ops::Not for TweeningDirection {
type Output = TweeningDirection;
fn not(self) -> Self::Output {
match self {
TweeningDirection::Forward => TweeningDirection::Backward,
TweeningDirection::Backward => TweeningDirection::Forward,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Not animated. If controlled by an [`Animator`] or [`AssetAnimator`], that animator is paused.
/// Actively animating. The tweenable did not reach its end state yet.
/// Animation ended but [`Tweenable::stop()`] was not called. The tweenable is idling at its latest
/// time. This can only happen for [`TweeningType::Once`], since other types loop indefinitely
/// until they're stopped.
/// Component to control the animation of another component.
#[derive(Component)]
pub struct Animator<T: Component> {
/// Control if this animation is played or not.
pub state: AnimatorState,
tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
}
impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Animator")
.field("state", &self.state)
.finish()
}
}
impl<T: Component> Default for Animator<T> {
fn default() -> Self {
state: Default::default(),
prev_state: Default::default(),
tweenable: None,
impl<T: Component> Animator<T> {
/// Create a new animator component from a single [`Tween`] or [`Sequence`].
pub fn new(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
tweenable: Some(Box::new(tween)),
..Default::default()
/// Set the initial state of the animator.
pub fn with_state(mut self, state: AnimatorState) -> Self {
self.state = state;
self.prev_state = state;
self
}
/// Set the top-level tweenable item this animator controls.
pub fn set_tweenable(&mut self, tween: impl Tweenable<T> + Send + Sync + 'static) {
self.tweenable = Some(Box::new(tween));
}
/// Get the collection of sequences forming the parallel tracks of animation.
pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
if let Some(tweenable) = &self.tweenable {
Some(tweenable.as_ref())
} else {
None
}
/// Get the mutable collection of sequences forming the parallel tracks of animation.
pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
if let Some(tweenable) = &mut self.tweenable {
Some(tweenable.as_mut())
} else {
None
}
/// Component to control the animation of an asset.
#[derive(Component)]
pub struct AssetAnimator<T: Asset> {
/// Control if this animation is played or not.
pub state: AnimatorState,
tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
handle: Handle<T>,
}
impl<T: Asset + std::fmt::Debug> std::fmt::Debug for AssetAnimator<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AssetAnimator")
.field("state", &self.state)
.finish()
}
}
impl<T: Asset> Default for AssetAnimator<T> {
fn default() -> Self {
state: Default::default(),
prev_state: Default::default(),
tweenable: None,
handle: Default::default(),
impl<T: Asset> AssetAnimator<T> {
/// Create a new animator component from a single [`Tween`] or [`Sequence`].
pub fn new(handle: Handle<T>, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
/// Set the initial state of the animator.
pub fn with_state(mut self, state: AnimatorState) -> Self {
self.state = state;
self.prev_state = state;
self
}
/// Set the top-level tweenable item this animator controls.
pub fn set_tweenable(&mut self, tween: impl Tweenable<T> + Send + Sync + 'static) {
self.tweenable = Some(Box::new(tween));
/// Get the collection of sequences forming the parallel tracks of animation.
pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
if let Some(tweenable) = &self.tweenable {
Some(tweenable.as_ref())
} else {
None
}
/// Get the mutable collection of sequences forming the parallel tracks of animation.
pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
if let Some(tweenable) = &mut self.tweenable {
Some(tweenable.as_mut())
} else {
None
}
}
fn handle(&self) -> Handle<T> {
self.handle.clone()
}
}
#[cfg(test)]
mod tests {
use super::{lens::*, *};
fn animator_new() {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
let animator = Animator::new(tween);
assert_eq!(animator.state, AnimatorState::default());
let tween = animator.tweenable().unwrap();
assert_eq!(tween.progress(), 0.);
}
/// Animator::with_state()
#[test]
fn animator_with_state() {
for state in [AnimatorState::Playing, AnimatorState::Paused] {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
TweeningType::PingPong,
std::time::Duration::from_secs(1),
TransformRotationLens {
start: Quat::IDENTITY,
end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
},
);
let animator = Animator::new(tween).with_state(state);
assert_eq!(animator.state, state);
/// Animator::default() + Animator::set_tweenable()
fn animator_default() {
let mut animator = Animator::<Transform>::default();
assert!(animator.tweenable().is_none());
assert!(animator.tweenable_mut().is_none());
TweeningType::PingPong,
std::time::Duration::from_secs(1),
TransformRotationLens {
start: Quat::IDENTITY,
end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
},
);
animator.set_tweenable(tween);
assert!(animator.tweenable().is_some());
assert!(animator.tweenable_mut().is_some());
#[test]
fn asset_animator_new() {
TweeningType::PingPong,
std::time::Duration::from_secs(1),
ColorMaterialColorLens {
start: Color::RED,
end: Color::BLUE,
},
);
let animator = AssetAnimator::new(Handle::<ColorMaterial>::default(), tween);
assert_eq!(animator.state, AnimatorState::default());
assert_eq!(tween.progress(), 0.);
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
/// AssetAnimator::with_state()
#[test]
fn asset_animator_with_state() {
for state in [AnimatorState::Playing, AnimatorState::Paused] {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
TweeningType::PingPong,
std::time::Duration::from_secs(1),
ColorMaterialColorLens {
start: Color::RED,
end: Color::BLUE,
},
);
let animator =
AssetAnimator::new(Handle::<ColorMaterial>::default(), tween).with_state(state);
assert_eq!(animator.state, state);
}
}
/// AssetAnimator::default() + AssetAnimator::set_tweenable()
#[test]
fn asset_animator_default() {
let mut animator = AssetAnimator::<ColorMaterial>::default();
assert!(animator.tweenable().is_none());
assert!(animator.tweenable_mut().is_none());
assert_eq!(animator.handle(), Handle::<ColorMaterial>::default());
let tween = Tween::new(
EaseFunction::QuadraticInOut,
TweeningType::PingPong,
std::time::Duration::from_secs(1),
ColorMaterialColorLens {
start: Color::RED,
end: Color::BLUE,
},
);
animator.set_tweenable(tween);
assert!(animator.tweenable().is_some());
assert!(animator.tweenable_mut().is_some());
assert_eq!(animator.handle(), Handle::<ColorMaterial>::default());
}