From 07b94b749c2514a893c5279684ceb255ece2d990 Mon Sep 17 00:00:00 2001 From: Jerome Humbert <djeedai@gmail.com> Date: Fri, 28 Jan 2022 20:45:30 +0000 Subject: [PATCH] Add sequence and tracks for complex animations - Add `Sequence<T>` for chained tweens - Add `Tracks<T>` for tracks of sequences running in parallel - Move most animation-related properties to the new `Tweens<T>` struct - Add `sequence` example --- CHANGELOG | 13 +- README.md | 64 ++++++-- examples/sequence.gif | Bin 0 -> 59972 bytes examples/sequence.rs | 65 ++++++++ src/lens.rs | 3 +- src/lib.rs | 365 ++++++++++++++++++++++++++++++++++-------- src/plugin.rs | 106 ++---------- 7 files changed, 436 insertions(+), 180 deletions(-) create mode 100644 examples/sequence.gif create mode 100644 examples/sequence.rs diff --git a/CHANGELOG b/CHANGELOG index 2c999d7..e74da40 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `Animator<T>::is_paused()` and `AssetAnimator<T>::is_paused()` to query when a tweening animation is in its pause phase, if any. -- Add `Animator<T>::direction()` and `AssetAnimator<T>::direction()` to query the playback direction of a tweening animation (forward or backward). -- Add `Animator<T>::progress()` and `AssetAnimator<T>::progress()` to query the progres ratio in [0:1] of a tweening animation. +- Add `Tween<T>` describing a single tween animation, independently of its target (asset or component). +- Add `Tween<T>::is_paused()` to query when a tweening animation is in its pause phase, if any. +- Add `Tween<T>::direction()` to query the playback direction of a tweening animation (forward or backward). +- Add `Tween<T>::progress()` to query the progres ratio in [0:1] of a tweening animation. +- Enable multiple lenses per animator via "tracks", the ability to add multiple tween animations running in parallel on the same component. +- Enable sequences of tween animations running serially, one after the other, for each track of an animator, allowing to create more complex animations. + +### Fixed + +- Perform spherical linear interpolation (slerp) for `Quat` rotation of `Transform` animation via `TransformRotationLens`, instead of mere linear interpolation leaving the quaternion non-normalized. ## [0.2.0] - 2022-01-09 diff --git a/README.md b/README.md index d7d5f50..8a0e7f3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ Tweening animation plugin for the Bevy game engine. +## Features + +- [x] Versatile customizable lens system to animate any field of any component or asset. +- [x] Sequence of tweening animations chained together, running one after the other, to create complex animations. +- [x] Multiple tweening animations per component, running in parallel, to animate different fields with different parameters. + ## Usage ### System setup @@ -33,12 +39,13 @@ commands }, ..Default::default() }) - // Add an Animator component to perform the animation + // Add an Animator component to perform the animation. This is a shortcut to + // create both an Animator and a Tween, and assign the Tween to the Animator. .insert(Animator::new( // Use a quadratic easing on both endpoints EaseFunction::QuadraticInOut, // Loop animation back and forth over 1 second, with a 0.5 second - // pause after each cycle (start -> end -> start). + // pause after each cycle (start -> end -> start -> pause -> ...). TweeningType::PingPong { duration: Duration::from_secs(1), pause: Some(Duration::from_millis(500)), @@ -55,7 +62,9 @@ commands ## Predefined Lenses -The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, where `<TargetName>` is the name of the target component or asset which is queried, and `<FieldName>` is the field which is mutated in place. +A small number of predefined lenses are available for the most common use cases, which also serve as examples. Users are encouraged to write their own lens to tailor the animation to their use case. + +The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, where `<TargetName>` is the name of the target Bevy component or asset type which is queried by the internal animation system to be modified, and `<FieldName>` is the field which is mutated in place by the lens. All predefined lenses modify a single field. Custom lenses can be written which modify multiple fields at once. ### Bevy Components @@ -74,27 +83,26 @@ The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, wher |---|---|---| | [`ColorMaterial`](https://docs.rs/bevy/0.6.0/bevy/sprite/struct.ColorMaterial.html) | [`color`](https://docs.rs/bevy/0.6.0/bevy/sprite/struct.ColorMaterial.html#structfield.color) | [`ColorMaterialColorLens`](https://docs.rs/bevy_tweening/latest/bevy_tweening/struct.ColorMaterialColorLens.html) | -### Custom component support +### Custom lens -To be able to animate some fields of a custom component, a custom lens need to be implemented for that component, which **linearly** interpolates the field(s) of that component. +A custom lens allows animating any field or group of fields of a Bevy component or asset. A custom lens is a type implementing the `Lens` trait, which is generic over the type of component or asset. ```rust -#[derive(Component)] -struct CustomComponent(f32); - -struct CustomLens { +struct MyXAxisLens { start: f32, end: f32, } -impl Lens<CustomComponent> for CustomLens { - fn lerp(&self, target: &mut CustomComponent, ratio: f32) -> f32 { - target.0 = self.start + (self.end - self.start) * ratio; +impl Lens<Tranform> for MyXAxisLens { + fn lerp(&self, target: &mut Tranform, ratio: f32) -> f32 { + let start = Vec3::new(self.start, 0., 0.); + let end = Vec3::new(self.end, 0., 0.); + target.translation = start + (end - start) * ratio; } } ``` -This process can also be used to interpolate fields of existing Bevy built-in components for which a predfined lens is not provided. +Note that the lens always **linearly** interpolates the field(s) of the component or asset. The type of easing applied modifies the rate at which the `ratio` parameter evolves, and is applied before the `lerp()` function is invoked. The basic formula for lerp (linear interpolation) is either of: @@ -103,7 +111,27 @@ The basic formula for lerp (linear interpolation) is either of: The two formulations are mathematically equivalent, but one may be more suited than the other depending on the type interpolated and the operations available, and the potential floating-point precision errors. -Then, the system `component_animator_system::<CustomComponent>` needs to be added to the application. +### Custom component support + +Custom components are animated like built-in Bevy ones, via a lens. + +```rust +#[derive(Component)] +struct MyCustomComponent(f32); + +struct MyCustomLens { + start: f32, + end: f32, +} + +impl Lens<MyCustomComponent> for MyCustomLens { + fn lerp(&self, target: &mut MyCustomComponent, ratio: f32) -> f32 { + target.0 = self.start + (self.end - self.start) * ratio; + } +} +``` + +Then, in addition, the system `component_animator_system::<CustomComponent>` needs to be added to the application. This system will extract each frame all `CustomComponent` instances with an `Animator<CustomComponent>` on the same entity, and animate the component via its animator. ## Custom asset support @@ -153,6 +181,14 @@ cargo run --example ui_position --features="bevy/bevy_winit"  +### [`sequence`](examples/sequence.rs) + +```rust +cargo run --example sequence --features="bevy/bevy_winit" +``` + + + ## Ease Functions Many [ease functions](https://docs.rs/interpolation/0.2.0/interpolation/enum.EaseFunction.html) are available: diff --git a/examples/sequence.gif b/examples/sequence.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd5783c828fe7c4bb643c5eea33b04ff7e6c66c7 GIT binary patch literal 59972 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 Y$j$Xkbjf8jjkUB4pBf43X@hwF57{1{DgXcg literal 0 HcmV?d00001 diff --git a/examples/sequence.rs b/examples/sequence.rs new file mode 100644 index 0000000..3b72778 --- /dev/null +++ b/examples/sequence.rs @@ -0,0 +1,65 @@ +use bevy::prelude::*; +use bevy_tweening::*; +use std::time::Duration; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + App::default() + .insert_resource(WindowDescriptor { + title: "Sequence".to_string(), + width: 600., + height: 600., + vsync: true, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_plugin(TweeningPlugin) + .add_startup_system(setup) + .run(); + + Ok(()) +} + +fn setup(mut commands: Commands) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + let size = 25.; + + let margin = 40.; + let screen_x = 600.; + let screen_y = 600.; + let center = Vec3::new(screen_x / 2., screen_y / 2., 0.); + + let dests = &[ + Vec3::new(margin, margin, 0.), + Vec3::new(screen_x - margin, margin, 0.), + Vec3::new(screen_x - margin, screen_y - margin, 0.), + Vec3::new(margin, screen_y - margin, 0.), + Vec3::new(margin, margin, 0.), + ]; + let tweens = dests + .windows(2) + .map(|pair| { + Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once { + duration: Duration::from_secs(1), + }, + TransformPositionLens { + start: pair[0] - center, + end: pair[1] - center, + }, + ) + }) + .collect(); + + commands + .spawn_bundle(SpriteBundle { + sprite: Sprite { + color: Color::RED, + custom_size: Some(Vec2::new(size, size)), + ..Default::default() + }, + ..Default::default() + }) + .insert(Animator::new_seq(tweens)); +} diff --git a/src/lens.rs b/src/lens.rs index c5470fc..fa6056c 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -90,8 +90,7 @@ pub struct TransformRotationLens { impl Lens<Transform> for TransformRotationLens { fn lerp(&mut self, target: &mut Transform, ratio: f32) { - let value = self.start + (self.end - self.start) * ratio; - target.rotation = value; + target.rotation = self.start.slerp(self.end, ratio); // FIXME - This slerps the shortest path only! https://docs.rs/bevy/latest/bevy/math/struct.Quat.html#method.slerp } } diff --git a/src/lib.rs b/src/lib.rs index ec49d23..22b8728 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,30 +226,18 @@ impl std::ops::Not for TweeningDirection { } } -/// Component to control the animation of another component. -#[derive(Component)] -pub struct Animator<T> { +/// Single tweening animation instance. +pub struct Tween<T> { ease_function: EaseMethod, timer: Timer, - /// Control if this animation is played or not. - pub state: AnimatorState, paused: bool, tweening_type: TweeningType, direction: TweeningDirection, lens: Box<dyn Lens<T> + Send + Sync + 'static>, } -impl<T: 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> Animator<T> { - /// Create a new animator component from an easing function, tweening type, and a lens. - /// The type `T` of the component to animate can generally be deducted from the lens type itself. +impl<T> Tween<T> { + /// Create a new tween animation. pub fn new<L>( ease_function: impl Into<EaseMethod>, tweening_type: TweeningType, @@ -258,14 +246,13 @@ impl<T> Animator<T> { where L: Lens<T> + Send + Sync + 'static, { - Animator { + Tween { ease_function: ease_function.into(), timer: match tweening_type { TweeningType::Once { duration } => Timer::new(duration, false), TweeningType::Loop { duration, .. } => Timer::new(duration, false), TweeningType::PingPong { duration, .. } => Timer::new(duration, false), }, - state: AnimatorState::Playing, paused: false, tweening_type, direction: TweeningDirection::Forward, @@ -302,23 +289,175 @@ impl<T> Animator<T> { } } + fn tick(&mut self, delta: Duration, target: &mut T) { + self.timer.tick(delta); + if self.paused { + if self.timer.just_finished() { + match self.tweening_type { + TweeningType::Once { duration } => { + self.timer.set_duration(duration); + } + TweeningType::Loop { duration, .. } => { + self.timer.set_duration(duration); + } + TweeningType::PingPong { duration, .. } => { + self.timer.set_duration(duration); + } + } + self.timer.reset(); + self.paused = false; + } + } else { + if self.timer.duration().as_secs_f32() != 0. { + let progress = self.progress(); + let factor = self.ease_function.sample(progress); + self.apply(target, factor); + } + if self.timer.finished() { + match self.tweening_type { + TweeningType::Once { .. } => { + //commands.entity(entity).remove::<Animator>(); + } + TweeningType::Loop { pause, .. } => { + if let Some(pause) = pause { + self.timer.set_duration(pause); + self.paused = true; + } + self.timer.reset(); + } + TweeningType::PingPong { pause, .. } => { + if let Some(pause) = pause { + self.timer.set_duration(pause); + self.paused = true; + } + self.timer.reset(); + self.direction = !self.direction; + } + } + } + } + } + #[inline(always)] fn apply(&mut self, target: &mut T, ratio: f32) { self.lens.lerp(target, ratio); } } +struct Sequence<T> { + tweens: Vec<Tween<T>>, + index: usize, +} + +impl<T> Sequence<T> { + pub fn new<I>(tweens: I) -> Self + where + I: IntoIterator<Item = Tween<T>>, + { + Sequence { + tweens: tweens.into_iter().collect(), + index: 0, + } + } + + pub fn from_single(tween: Tween<T>) -> Self { + Sequence { + tweens: vec![tween], + index: 0, + } + } + fn tick(&mut self, delta: Duration, target: &mut T) { + if self.index < self.tweens.len() { + let tween = &mut self.tweens[self.index]; + tween.tick(delta, target); + if tween.progress() >= 1.0 { + self.index += 1; + } + } + } +} + +struct Tracks<T> { + tracks: Vec<Sequence<T>>, +} + +/// 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, + tracks: Tracks<T>, +} + +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> { + /// Create a new animator component from an easing function, tweening type, and a lens. + /// The type `T` of the component to animate can generally be deducted from the lens type itself. + /// This creates a new [`Tween`] instance then assign it to a newly created animator. + pub fn new<L>( + ease_function: impl Into<EaseMethod>, + tweening_type: TweeningType, + lens: L, + ) -> Self + where + L: Lens<T> + Send + Sync + 'static, + { + let tween = Tween::new(ease_function, tweening_type, lens); + Animator { + state: AnimatorState::Playing, + tracks: Tracks { + tracks: vec![Sequence::from_single(tween)], + }, + } + } + + /// Create a new animator component from a single tween instance. + pub fn new_single(tween: Tween<T>) -> Self { + Animator { + state: AnimatorState::Playing, + tracks: Tracks { + tracks: vec![Sequence::from_single(tween)], + }, + } + } + + /// Create a new animator component from a sequence of tween instances. + /// The tweens are played in order, one after the other. They all must be non-looping. + pub fn new_seq(tweens: Vec<Tween<T>>) -> Self { + for t in &tweens { + assert!(matches!(t.tweening_type, TweeningType::Once { .. })); + } + Animator { + state: AnimatorState::Playing, + tracks: Tracks { + tracks: vec![Sequence::new(tweens)], + }, + } + } + + #[allow(dead_code)] + fn tracks(&self) -> &Tracks<T> { + &self.tracks + } + + fn tracks_mut(&mut self) -> &mut Tracks<T> { + &mut self.tracks + } +} + /// Component to control the animation of an asset. #[derive(Component)] pub struct AssetAnimator<T: Asset> { - ease_function: EaseMethod, - timer: Timer, /// Control if this animation is played or not. pub state: AnimatorState, - paused: bool, - tweening_type: TweeningType, - direction: TweeningDirection, - lens: Box<dyn Lens<T> + Send + Sync + 'static>, + tracks: Tracks<T>, handle: Handle<T>, } @@ -343,48 +482,39 @@ impl<T: Asset> AssetAnimator<T> { where L: Lens<T> + Send + Sync + 'static, { + let tween = Tween::new(ease_function, tweening_type, lens); AssetAnimator { - ease_function: ease_function.into(), - timer: match tweening_type { - TweeningType::Once { duration } => Timer::new(duration, false), - TweeningType::Loop { duration, .. } => Timer::new(duration, false), - TweeningType::PingPong { duration, .. } => Timer::new(duration, false), - }, state: AnimatorState::Playing, - paused: false, - tweening_type, - direction: TweeningDirection::Forward, - lens: Box::new(lens), + tracks: Tracks { + tracks: vec![Sequence::from_single(tween)], + }, handle, } } - /// A boolean indicating whether the animation is currently in the pause phase of a loop. - /// - /// The [`TweeningType::Loop`] and [`TweeningType::PingPong`] tweening types are looping over - /// infinitely, with an optional pause between each loop. This function returns `true` if the - /// animation is currently under such pause. For [`TweeningType::Once`], which has no pause, - /// this always returns `false`. - pub fn is_paused(&self) -> bool { - self.paused - } - - /// The current animation direction. - /// - /// See [`TweeningDirection`] for details. - pub fn direction(&self) -> TweeningDirection { - self.direction + /// Create a new animator component from a single tween instance. + pub fn new_single(handle: Handle<T>, tween: Tween<T>) -> Self { + AssetAnimator { + state: AnimatorState::Playing, + tracks: Tracks { + tracks: vec![Sequence::from_single(tween)], + }, + handle, + } } - /// Current animation progress ratio between 0 and 1. - /// - /// For reversed playback ([`TweeningDirection::Backward`]), the ratio goes from 0 at the - /// end point (beginning of backward playback) to 1 at the start point (end of backward - /// playback). - pub fn progress(&self) -> f32 { - match self.direction { - TweeningDirection::Forward => self.timer.percent(), - TweeningDirection::Backward => self.timer.percent_left(), + /// Create a new animator component from a sequence of tween instances. + /// The tweens are played in order, one after the other. They all must be non-looping. + pub fn new_seq(handle: Handle<T>, tweens: Vec<Tween<T>>) -> Self { + for t in &tweens { + assert!(matches!(t.tweening_type, TweeningType::Once { .. })); + } + AssetAnimator { + state: AnimatorState::Playing, + tracks: Tracks { + tracks: vec![Sequence::new(tweens)], + }, + handle, } } @@ -392,15 +522,106 @@ impl<T: Asset> AssetAnimator<T> { self.handle.clone() } - #[inline(always)] - fn apply(&mut self, target: &mut T, ratio: f32) { - self.lens.lerp(target, ratio); + #[allow(dead_code)] + fn tracks(&self) -> &Tracks<T> { + &self.tracks + } + + fn tracks_mut(&mut self) -> &mut Tracks<T> { + &mut self.tracks } } #[cfg(test)] mod tests { use super::*; + #[test] + fn tween_tick() { + let mut tween = Tween { + ease_function: EaseMethod::Linear, + timer: Timer::from_seconds(1.0, false), + paused: false, + tweening_type: TweeningType::Once { + duration: Duration::from_secs_f32(1.0), + }, + direction: TweeningDirection::Forward, + lens: Box::new(TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }), + }; + let mut transform = Transform::default(); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2))); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(0.4))); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(0.6))); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(0.8))); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); + tween.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); + } + + #[test] + fn seq_tick() { + let tween1 = Tween { + ease_function: EaseMethod::Linear, + timer: Timer::from_seconds(1.0, false), + paused: false, + tweening_type: TweeningType::Once { + duration: Duration::from_secs_f32(1.0), + }, + direction: TweeningDirection::Forward, + lens: Box::new(TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }), + }; + let tween2 = Tween { + ease_function: EaseMethod::Linear, + timer: Timer::from_seconds(1.0, false), + paused: false, + tweening_type: TweeningType::Once { + duration: Duration::from_secs_f32(1.0), + }, + direction: TweeningDirection::Forward, + lens: Box::new(TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_x(180_f32.to_radians()), + }), + }; + let mut seq = Sequence::new([tween1, tween2]); + let mut transform = Transform::default(); + // First, translation alone (0->1) + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2))); + seq.tick(Duration::from_secs_f32(0.8), &mut transform); + assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0))); + // Then, rotation alone, on top of final translation (1->2) + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform.translation, Vec3::splat(1.0)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(36_f32.to_radians()), 1e-5)); + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform.translation, Vec3::splat(1.0)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(72_f32.to_radians()), 1e-5)); + seq.tick(Duration::from_secs_f32(0.6), &mut transform); + assert_eq!(transform.translation, Vec3::splat(1.0)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + assert_eq!(transform.translation, Vec3::splat(1.0)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + } #[test] fn animator_new() { @@ -415,9 +636,14 @@ mod tests { end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), }, ); - assert_eq!(animator.is_paused(), false); - assert_eq!(animator.direction(), TweeningDirection::Forward); - assert_eq!(animator.progress(), 0.); + let tracks = animator.tracks(); + assert_eq!(tracks.tracks.len(), 1); + let seq = &tracks.tracks[0]; + assert_eq!(seq.tweens.len(), 1); + let tween = &seq.tweens[0]; + assert_eq!(tween.is_paused(), false); + assert_eq!(tween.direction(), TweeningDirection::Forward); + assert_eq!(tween.progress(), 0.); } #[test] @@ -434,8 +660,13 @@ mod tests { end: Color::BLUE, }, ); - assert_eq!(animator.is_paused(), false); - assert_eq!(animator.direction(), TweeningDirection::Forward); - assert_eq!(animator.progress(), 0.); + let tracks = animator.tracks(); + assert_eq!(tracks.tracks.len(), 1); + let seq = &tracks.tracks[0]; + assert_eq!(seq.tweens.len(), 1); + let tween = &seq.tweens[0]; + assert_eq!(tween.is_paused(), false); + assert_eq!(tween.direction(), TweeningDirection::Forward); + assert_eq!(tween.progress(), 0.); } } diff --git a/src/plugin.rs b/src/plugin.rs index e477d19..3204862 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,6 +1,6 @@ use bevy::{asset::Asset, ecs::component::Component, prelude::*}; -use crate::{Animator, AnimatorState, AssetAnimator, TweeningType}; +use crate::{Animator, AnimatorState, AssetAnimator}; /// Plugin to add systems related to tweening #[derive(Debug, Clone, Copy)] @@ -21,53 +21,12 @@ pub fn component_animator_system<T: Component>( mut query: Query<(&mut T, &mut Animator<T>)>, ) { for (ref mut target, ref mut animator) in query.iter_mut() { - if animator.state == AnimatorState::Playing { - animator.timer.tick(time.delta()); + if animator.state == AnimatorState::Paused { + continue; } - if animator.paused { - if animator.timer.just_finished() { - match animator.tweening_type { - TweeningType::Once { duration } => { - animator.timer.set_duration(duration); - } - TweeningType::Loop { duration, .. } => { - animator.timer.set_duration(duration); - } - TweeningType::PingPong { duration, .. } => { - animator.timer.set_duration(duration); - } - } - animator.timer.reset(); - animator.paused = false; - } - } else { - if animator.timer.duration().as_secs_f32() != 0. { - let progress = animator.progress(); - let factor = animator.ease_function.sample(progress); - animator.apply(target, factor); - } - if animator.timer.finished() { - match animator.tweening_type { - TweeningType::Once { .. } => { - //commands.entity(entity).remove::<Animator>(); - } - TweeningType::Loop { pause, .. } => { - if let Some(pause) = pause { - animator.timer.set_duration(pause); - animator.paused = true; - } - animator.timer.reset(); - } - TweeningType::PingPong { pause, .. } => { - if let Some(pause) = pause { - animator.timer.set_duration(pause); - animator.paused = true; - } - animator.timer.reset(); - animator.direction = !animator.direction; - } - } - } + // Play all tracks in parallel + for seq in &mut animator.tracks_mut().tracks { + seq.tick(time.delta(), target); } } } @@ -78,54 +37,13 @@ pub fn asset_animator_system<T: Asset>( mut query: Query<&mut AssetAnimator<T>>, ) { for ref mut animator in query.iter_mut() { - if animator.state == AnimatorState::Playing { - animator.timer.tick(time.delta()); + if animator.state == AnimatorState::Paused { + continue; } - if animator.paused { - if animator.timer.just_finished() { - match animator.tweening_type { - TweeningType::Once { duration } => { - animator.timer.set_duration(duration); - } - TweeningType::Loop { duration, .. } => { - animator.timer.set_duration(duration); - } - TweeningType::PingPong { duration, .. } => { - animator.timer.set_duration(duration); - } - } - animator.timer.reset(); - animator.paused = false; - } - } else { - if animator.timer.duration().as_secs_f32() != 0. { - let progress = animator.progress(); - let factor = animator.ease_function.sample(progress); - if let Some(target) = assets.get_mut(animator.handle()) { - animator.apply(target, factor); - } - } - if animator.timer.finished() { - match animator.tweening_type { - TweeningType::Once { .. } => { - //commands.entity(entity).remove::<Animator>(); - } - TweeningType::Loop { pause, .. } => { - if let Some(pause) = pause { - animator.timer.set_duration(pause); - animator.paused = true; - } - animator.timer.reset(); - } - TweeningType::PingPong { pause, .. } => { - if let Some(pause) = pause { - animator.timer.set_duration(pause); - animator.paused = true; - } - animator.timer.reset(); - animator.direction = !animator.direction; - } - } + if let Some(target) = assets.get_mut(animator.handle()) { + // Play all tracks in parallel + for seq in &mut animator.tracks_mut().tracks { + seq.tick(time.delta(), target); } } } -- GitLab