#![deny(
warnings,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
missing_docs
)]
use std::time::Duration;
#[cfg(feature = "bevy_asset")]
use bevy::asset::Asset;
use bevy::prelude::*;
use interpolation::Ease as IEase;
pub use interpolation::{EaseFunction, Lerp};
pub use lens::Lens;
#[cfg(feature = "bevy_asset")]
pub use plugin::asset_animator_system;
pub use plugin::{component_animator_system, AnimationSystem, TweeningPlugin};
pub use tweenable::{
BoxedTweenable, Delay, Sequence, Targetable, TotalDuration, Tracks, Tween, TweenCompleted,
TweenState, Tweenable,
};
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)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepeatStrategy {
Repeat,
MirroredRepeat,
}
impl Default for RepeatStrategy {
fn default() -> Self {
Self::Repeat
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimatorState {
Playing,
Paused,
}
impl Default for AnimatorState {
fn default() -> Self {
Self::Playing
}
}
impl std::ops::Not for AnimatorState {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Self::Paused => Self::Playing,
Self::Playing => Self::Paused,
}
}
}
#[derive(Clone, Copy)]
pub enum EaseMethod {
EaseFunction(EaseFunction),
Linear,
Discrete(f32),
CustomFunction(fn(f32) -> f32),
}
impl EaseMethod {
#[must_use]
fn sample(self, x: f32) -> f32 {
match self {
Self::EaseFunction(function) => x.calc(function),
Self::Linear => x,
Self::Discrete(limit) => {
if x > limit {
1.
} else {
0.
}
}
Self::CustomFunction(function) => function(x),
}
}
}
impl Default for EaseMethod {
fn default() -> Self {
Self::Linear
}
}
impl From<EaseFunction> for EaseMethod {
fn from(ease_function: EaseFunction) -> Self {
Self::EaseFunction(ease_function)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TweeningDirection {
Forward,
Backward,
}
impl TweeningDirection {
#[must_use]
pub fn is_forward(&self) -> bool {
*self == Self::Forward
}
#[must_use]
pub fn is_backward(&self) -> bool {
*self == Self::Backward
}
}
impl Default for TweeningDirection {
fn default() -> Self {
Self::Forward
}
}
impl std::ops::Not for TweeningDirection {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Self::Forward => Self::Backward,
Self::Backward => Self::Forward,
}
}
}
macro_rules! animator_impl {
() => {
#[must_use]
pub fn with_state(mut self, state: AnimatorState) -> Self {
self.state = state;
self
}
#[must_use]
pub fn with_speed(mut self, speed: f32) -> Self {
self.speed = speed;
self
}
pub fn set_speed(&mut self, speed: f32) {
self.speed = speed;
}
pub fn speed(&self) -> f32 {
self.speed
}
pub fn set_tweenable(&mut self, tween: impl Tweenable<T> + 'static) {
self.tweenable = Box::new(tween);
}
#[must_use]
pub fn tweenable(&self) -> &dyn Tweenable<T> {
self.tweenable.as_ref()
}
#[must_use]
pub fn tweenable_mut(&mut self) -> &mut dyn Tweenable<T> {
self.tweenable.as_mut()
}
pub fn stop(&mut self) {
self.state = AnimatorState::Paused;
self.tweenable_mut().rewind();
}
};
}
#[derive(Component)]
pub struct Animator<T: Component> {
pub state: AnimatorState,
tweenable: BoxedTweenable<T>,
speed: f32,
}
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> Animator<T> {
#[must_use]
pub fn new(tween: impl Tweenable<T> + 'static) -> Self {
Self {
state: default(),
tweenable: Box::new(tween),
speed: 1.,
}
}
animator_impl!();
}
#[cfg(feature = "bevy_asset")]
#[derive(Component)]
pub struct AssetAnimator<T: Asset> {
pub state: AnimatorState,
tweenable: BoxedTweenable<T>,
handle: Handle<T>,
speed: f32,
}
#[cfg(feature = "bevy_asset")]
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()
}
}
#[cfg(feature = "bevy_asset")]
impl<T: Asset> AssetAnimator<T> {
#[must_use]
pub fn new(handle: Handle<T>, tween: impl Tweenable<T> + 'static) -> Self {
Self {
state: default(),
tweenable: Box::new(tween),
handle,
speed: 1.,
}
}
animator_impl!();
#[must_use]
fn handle(&self) -> Handle<T> {
self.handle.clone()
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "bevy_asset")]
use bevy::reflect::TypeUuid;
use super::*;
use crate::test_utils::*;
struct DummyLens {
start: f32,
end: f32,
}
#[derive(Debug, Default, Component)]
struct DummyComponent {
value: f32,
}
#[cfg(feature = "bevy_asset")]
#[derive(Debug, Default, Reflect, TypeUuid)]
#[uuid = "a33abc11-264e-4bbb-82e8-b87226bb4383"]
struct DummyAsset {
value: f32,
}
impl Lens<DummyComponent> for DummyLens {
fn lerp(&mut self, target: &mut DummyComponent, ratio: f32) {
target.value = self.start.lerp(&self.end, &ratio);
}
}
#[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.] {
l.lerp(&mut c, r);
assert_approx_eq!(c.value, r);
}
}
#[cfg(feature = "bevy_asset")]
impl Lens<DummyAsset> for DummyLens {
fn lerp(&mut self, target: &mut DummyAsset, ratio: f32) {
target.value = self.start.lerp(&self.end, &ratio);
}
}
#[cfg(feature = "bevy_asset")]
#[test]
fn dummy_lens_asset() {
let mut a = 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.] {
l.lerp(&mut a, r);
assert_approx_eq!(a.value, r);
}
}
#[test]
fn repeat_count() {
let count = RepeatCount::default();
assert_eq!(count, RepeatCount::Finite(1));
}
#[test]
fn repeat_strategy() {
let strategy = RepeatStrategy::default();
assert_eq!(strategy, RepeatStrategy::Repeat);
}
#[test]
fn tweening_direction() {
let tweening_direction = TweeningDirection::default();
assert_eq!(tweening_direction, TweeningDirection::Forward);
}
#[test]
fn animator_state() {
let mut state = AnimatorState::default();
assert_eq!(state, AnimatorState::Playing);
state = !state;
assert_eq!(state, AnimatorState::Paused);
state = !state;
assert_eq!(state, AnimatorState::Playing);
}
#[test]
fn ease_method() {
let ease = EaseMethod::default();
assert!(matches!(ease, EaseMethod::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::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 animator_new() {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator = Animator::<DummyComponent>::new(tween);
assert_eq!(animator.state, AnimatorState::default());
assert_eq!(animator.tweenable().progress(), 0.);
}
#[test]
fn animator_with_state() {
for state in [AnimatorState::Playing, AnimatorState::Paused] {
let tween = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator = Animator::new(tween).with_state(state);
assert_eq!(animator.state, state);
let debug_string = format!("{:?}", animator);
assert_eq!(
debug_string,
format!("Animator {{ state: {:?} }}", animator.state)
);
}
}
#[test]
fn animator_controls() {
let tween = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = Animator::new(tween);
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.stop();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.tweenable_mut().set_progress(0.5);
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.5);
animator.tweenable_mut().rewind();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.tweenable_mut().set_progress(0.5);
animator.state = AnimatorState::Playing;
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.5);
animator.tweenable_mut().rewind();
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.stop();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
}
#[test]
fn animator_speed() {
let tween = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = Animator::new(tween);
assert_approx_eq!(animator.speed(), 1.);
animator.set_speed(2.4);
assert_approx_eq!(animator.speed(), 2.4);
let tween = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator = Animator::new(tween).with_speed(3.5);
assert_approx_eq!(animator.speed(), 3.5);
}
#[test]
fn animator_set_tweenable() {
let tween = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = Animator::new(tween);
let tween2 = Tween::<DummyComponent>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(2),
DummyLens { start: 0., end: 1. },
);
animator.set_tweenable(tween2);
assert_eq!(animator.tweenable().duration(), Duration::from_secs(2));
}
#[cfg(feature = "bevy_asset")]
#[test]
fn asset_animator_new() {
let tween = Tween::<DummyAsset>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
assert_eq!(animator.state, AnimatorState::default());
assert_eq!(animator.handle(), Handle::<DummyAsset>::default());
let tween = animator;
assert_eq!(tween.tweenable().progress(), 0.);
}
#[cfg(feature = "bevy_asset")]
#[test]
fn asset_animator_with_state() {
for state in [AnimatorState::Playing, AnimatorState::Paused] {
let tween = Tween::<DummyAsset>::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator =
AssetAnimator::new(Handle::<DummyAsset>::default(), tween).with_state(state);
assert_eq!(animator.state, state);
let debug_string = format!("{:?}", animator);
assert_eq!(
debug_string,
format!("AssetAnimator {{ state: {:?} }}", animator.state)
);
}
}
#[cfg(feature = "bevy_asset")]
#[test]
fn asset_animator_controls() {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.stop();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.tweenable_mut().set_progress(0.5);
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.5);
animator.tweenable_mut().rewind();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.tweenable_mut().set_progress(0.5);
animator.state = AnimatorState::Playing;
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.5);
animator.tweenable_mut().rewind();
assert_eq!(animator.state, AnimatorState::Playing);
assert_approx_eq!(animator.tweenable().progress(), 0.);
animator.stop();
assert_eq!(animator.state, AnimatorState::Paused);
assert_approx_eq!(animator.tweenable().progress(), 0.);
}
#[cfg(feature = "bevy_asset")]
#[test]
fn asset_animator_speed() {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
assert_approx_eq!(animator.speed(), 1.);
animator.set_speed(2.4);
assert_approx_eq!(animator.speed(), 2.4);
let tween = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween).with_speed(3.5);
assert_approx_eq!(animator.speed(), 3.5);
}
#[cfg(feature = "bevy_asset")]
#[test]
fn asset_animator_set_tweenable() {
let tween = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(1),
DummyLens { start: 0., end: 1. },
);
let mut animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
let tween2 = Tween::new(
EaseFunction::QuadraticInOut,
Duration::from_secs(2),
DummyLens { start: 0., end: 1. },
);
animator.set_tweenable(tween2);
assert_eq!(animator.tweenable().duration(), Duration::from_secs(2));
}
}