From d3b5f1bee9ef6dfc1fda860f922410874af88f1f Mon Sep 17 00:00:00 2001 From: Jerome Humbert <djeedai@gmail.com> Date: Sun, 30 Jan 2022 20:47:14 +0000 Subject: [PATCH] `Tweenable`-based design Add a `Tweenable<T>` trait describing a generic animatable tween-like element. Expose the three types of tweenables and implement `Tweenable<T>` for them: - `Tween<T>`, a single animation - `Sequence<T>`, a sequence of consecutive animations - `Tracks<T>`, a batch of animations running in parallel Clean-up animators to hold a single top-level `Tweenable<T>` and let the user build any kind of animation hierarchy for themselves. --- CHANGELOG | 16 +- examples/colormaterial_color.rs | 85 +++--- examples/sequence.rs | 143 +++++++--- examples/sprite_color.rs | 84 +++--- examples/transform_rotation.rs | 86 +++--- examples/transform_translation.rs | 84 +++--- examples/ui_position.rs | 104 ++++---- src/lib.rs | 429 ++++++++++++++++++------------ src/plugin.rs | 20 +- 9 files changed, 618 insertions(+), 433 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 028c9ce..6834129 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,17 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement `Default` for `AnimatorState` as `AnimatorState::Playing`. - Added `Animator::with_state()` and `AssetAnimator::with_state()`, builder-like functions to override the default `AnimatorState`. - Added `Tween::is_looping()` returning true for all but `TweeningType::Once`. -- Publicly exposed `Sequence<T>`, a sequence of tweens running one after the other. -- Publicly exposed `Animator<T>::tracks()` and `Animator<T>::tracks_mut()` to access the animation sequences running in parallel on multiple animation tracks. +- Added the `Tweenable<T>` trait, implemented by the `Tween<T>` animation and the `Tracks<T>` and `Sequence<T>` animation collections. +- Added `IntoBoxDynTweenable<T>`, a trait to convert a `Tweenable<T>` trait object into a boxed variant. +- Publicly exposed `Sequence<T>`, a sequence of `Tweenable<T>` running one after the other. +- Publicly exposed `Tracks<T>`, a collection of `Tweenable<T>` running in parallel. +- Publicly exposed `TweenState`, the playback state of a single `Tweenable<T>` item. +- Added `Tween<T>::then()` and `Sequence<T>::then()` to append a `Tweenable<T>` to a sequence (creating a new sequence in the case of `Tween<T>::then()`). +- Added `tweenable()` and `tweenable_mut()` on the `Animator<T>` and `AssetAnimator<T>` to access their top-level `Tweenable<T>`. +- Implemented `Default` for `Animator<T>` and `AssetAnimator<T>`, creating an animator without any tweenable item (no-op). ### Changed - Moved tween duration out of the `TweeningType` enum, which combined with the removal of the "pause" feature in loops makes it a C-like enum. -- Updated the `sequence` example to add some text showing the current sequence active tween index and its progress. +- Updated the `sequence` example to add some text showing the current sequence progress. +- Modified the signature of `new()` for `Animator<T>` and `AssetAnimator<T>` to take a single `Tweenable<T>` instead of trying to build a `Tween<T>` internally. This allows passing any `Tweenable<T>` as the top-level animatable item of an animator, and avoids the overhead of maintaining a `Tracks<T>` internally in each animator when the most common use case is likely to use a single `Tween<T>` or a `Sequence<T>` without parallelism. ### Removed -- Removed the "pause" feature in-between loops of `TweeningType::Loop` and `TweeningType::PingPong`, which can be replaced if needed with a sequence including a no-op tween of the desired duration. Removed `Tween::is_paused()`. +- Removed the "pause" feature in-between loops of `TweeningType::Loop` and `TweeningType::PingPong`, which can be replaced if needed by a sequence including a no-op tween of the desired duration. Removed `Tween::is_paused()`. +- Removed `new_single()` and `new_seq()` on the `Animator<T>` and `AssetAnimator<T>`. Users should explicitly create a `Tween<T>` or `Sequence<T>` instead, and use `new()`. ### Fixed diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs index 51c7fa5..b7e850f 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -2,6 +2,8 @@ use bevy::{ prelude::*, sprite::{MaterialMesh2dBundle, Mesh2dHandle}, }; +use bevy_tweening::*; +use std::time::Duration; fn main() -> Result<(), Box<dyn std::error::Error>> { App::default() @@ -13,7 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(TweeningPlugin) .add_startup_system(setup) .run(); @@ -38,36 +40,36 @@ fn setup( let quad_mesh: Mesh2dHandle = meshes.add(Mesh::from(shape::Quad::default())).into(); for ease_function in &[ - bevy_tweening::EaseFunction::QuadraticIn, - bevy_tweening::EaseFunction::QuadraticOut, - bevy_tweening::EaseFunction::QuadraticInOut, - bevy_tweening::EaseFunction::CubicIn, - bevy_tweening::EaseFunction::CubicOut, - bevy_tweening::EaseFunction::CubicInOut, - bevy_tweening::EaseFunction::QuarticIn, - bevy_tweening::EaseFunction::QuarticOut, - bevy_tweening::EaseFunction::QuarticInOut, - bevy_tweening::EaseFunction::QuinticIn, - bevy_tweening::EaseFunction::QuinticOut, - bevy_tweening::EaseFunction::QuinticInOut, - bevy_tweening::EaseFunction::SineIn, - bevy_tweening::EaseFunction::SineOut, - bevy_tweening::EaseFunction::SineInOut, - bevy_tweening::EaseFunction::CircularIn, - bevy_tweening::EaseFunction::CircularOut, - bevy_tweening::EaseFunction::CircularInOut, - bevy_tweening::EaseFunction::ExponentialIn, - bevy_tweening::EaseFunction::ExponentialOut, - bevy_tweening::EaseFunction::ExponentialInOut, - bevy_tweening::EaseFunction::ElasticIn, - bevy_tweening::EaseFunction::ElasticOut, - bevy_tweening::EaseFunction::ElasticInOut, - bevy_tweening::EaseFunction::BackIn, - bevy_tweening::EaseFunction::BackOut, - bevy_tweening::EaseFunction::BackInOut, - bevy_tweening::EaseFunction::BounceIn, - bevy_tweening::EaseFunction::BounceOut, - bevy_tweening::EaseFunction::BounceInOut, + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, ] { // Create a unique material per entity, so that it can be animated // without affecting the other entities. Note that we could share @@ -75,6 +77,16 @@ fn setup( // asset would change the color of all entities using that material. let unique_material = materials.add(Color::BLACK.into()); + let tween = Tween::new( + *ease_function, + TweeningType::PingPong, + Duration::from_secs(1), + ColorMaterialColorLens { + start: Color::RED, + end: Color::BLUE, + }, + ); + commands .spawn_bundle(MaterialMesh2dBundle { mesh: quad_mesh.clone(), @@ -83,16 +95,7 @@ fn setup( material: unique_material.clone(), ..Default::default() }) - .insert(bevy_tweening::AssetAnimator::new( - unique_material.clone(), - *ease_function, - bevy_tweening::TweeningType::PingPong, - std::time::Duration::from_secs(1), - bevy_tweening::ColorMaterialColorLens { - start: Color::RED, - end: Color::BLUE, - }, - )); + .insert(AssetAnimator::new(unique_material.clone(), tween)); y -= size * spacing; if y < -screen_y { x += size * spacing; diff --git a/examples/sequence.rs b/examples/sequence.rs index 04b6eb0..d5b19cf 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -21,19 +21,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { } #[derive(Component)] -struct IndexText; +struct RedProgress; #[derive(Component)] -struct ProgressText; +struct BlueProgress; + +#[derive(Component)] +struct RedSprite; + +#[derive(Component)] +struct BlueSprite; fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn_bundle(OrthographicCameraBundle::new_2d()); let font = asset_server.load("fonts/FiraMono-Regular.ttf"); - let text_style = TextStyle { - font, + let text_style_red = TextStyle { + font: font.clone(), + font_size: 50.0, + color: Color::RED, + }; + let text_style_blue = TextStyle { + font: font.clone(), font_size: 50.0, - color: Color::WHITE, + color: Color::BLUE, }; let text_alignment = TextAlignment { @@ -47,12 +58,12 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { text: Text { sections: vec![ TextSection { - value: "index: ".to_owned(), - style: text_style.clone(), + value: "progress: ".to_owned(), + style: text_style_red.clone(), }, TextSection { - value: "0".to_owned(), - style: text_style.clone(), + value: "0%".to_owned(), + style: text_style_red.clone(), }, ], alignment: text_alignment, @@ -60,7 +71,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { transform: Transform::from_translation(Vec3::new(0., 40., 0.)), ..Default::default() }) - .insert(IndexText); + .insert(RedProgress); // Text with progress of the active tween in the sequence commands @@ -69,11 +80,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { sections: vec![ TextSection { value: "progress: ".to_owned(), - style: text_style.clone(), + style: text_style_blue.clone(), }, TextSection { value: "0%".to_owned(), - style: text_style.clone(), + style: text_style_blue.clone(), }, ], alignment: text_alignment, @@ -81,7 +92,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { transform: Transform::from_translation(Vec3::new(0., -40., 0.)), ..Default::default() }) - .insert(ProgressText); + .insert(BlueProgress); let size = 25.; @@ -98,20 +109,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { 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::from_secs(1), - TransformPositionLens { - start: pair[0] - center, - end: pair[1] - center, - }, - ) - }) - .collect(); + // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>) + let seq = Sequence::new(dests.windows(2).map(|pair| { + Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformPositionLens { + start: pair[0] - center, + end: pair[1] - center, + }, + ) + })); commands .spawn_bundle(SpriteBundle { @@ -122,32 +131,86 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { }, ..Default::default() }) - .insert(Animator::new_seq(tweens)); + .insert(RedSprite) + .insert(Animator::new(seq)); + + // First move from left to right, then rotate around self 180 degrees while scaling + // size at the same time. + let tween_move = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformPositionLens { + start: Vec3::new(-200., 100., 0.), + end: Vec3::new(200., 100., 0.), + }, + ); + let tween_rotate = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_z(180_f32.to_radians()), + }, + ); + let tween_scale = Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs(1), + TransformScaleLens { + start: Vec3::ONE, + end: Vec3::splat(2.0), + }, + ); + // Build parallel tracks executing two tweens at the same time : rotate and scale. + let tracks = Tracks::new([tween_rotate, tween_scale]); + // Build a sequence from an heterogeneous list of tweenables by casting them manually + // to a boxed Tweenable<Transform> : first move, then { rotate + scale }. + let seq2 = Sequence::new([ + Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, + Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>, + ]); + + commands + .spawn_bundle(SpriteBundle { + sprite: Sprite { + color: Color::BLUE, + custom_size: Some(Vec2::new(size * 3., size)), + ..Default::default() + }, + ..Default::default() + }) + .insert(BlueSprite) + .insert(Animator::new(seq2)); } fn update_text( // Note: need a QuerySet<> due to the "&mut Text" in both queries mut query_text: QuerySet<( - QueryState<&mut Text, With<IndexText>>, - QueryState<&mut Text, With<ProgressText>>, + QueryState<&mut Text, With<RedProgress>>, + QueryState<&mut Text, With<BlueProgress>>, )>, - query_anim: Query<&Animator<Transform>>, + query_anim_red: Query<&Animator<Transform>, With<RedSprite>>, + query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>, ) { - let anim = query_anim.single(); - let seq = &anim.tracks()[0]; - let index = seq.index(); - let tween = seq.current(); - let progress = tween.progress(); + let anim_red = query_anim_red.single(); + let tween_red = anim_red.tweenable().unwrap(); + let progress_red = tween_red.progress(); + + let anim_blue = query_anim_blue.single(); + let tween_blue = anim_blue.tweenable().unwrap(); + let progress_blue = tween_blue.progress(); // Use scopes to force-drop the mutable context before opening the next one { let mut q0 = query_text.q0(); - let mut index_text = q0.single_mut(); - index_text.sections[1].value = format!("{:1}", index).to_string(); + let mut red_text = q0.single_mut(); + red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string(); } { let mut q1 = query_text.q1(); - let mut progress_text = q1.single_mut(); - progress_text.sections[1].value = format!("{:5.1}%", progress * 100.).to_string(); + let mut blue_text = q1.single_mut(); + blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string(); } } diff --git a/examples/sprite_color.rs b/examples/sprite_color.rs index 4878509..475573a 100644 --- a/examples/sprite_color.rs +++ b/examples/sprite_color.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_tweening::*; fn main() -> Result<(), Box<dyn std::error::Error>> { App::default() @@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(TweeningPlugin) .add_startup_system(setup) .run(); @@ -29,37 +30,47 @@ fn setup(mut commands: Commands) { let mut y = screen_y; for ease_function in &[ - bevy_tweening::EaseFunction::QuadraticIn, - bevy_tweening::EaseFunction::QuadraticOut, - bevy_tweening::EaseFunction::QuadraticInOut, - bevy_tweening::EaseFunction::CubicIn, - bevy_tweening::EaseFunction::CubicOut, - bevy_tweening::EaseFunction::CubicInOut, - bevy_tweening::EaseFunction::QuarticIn, - bevy_tweening::EaseFunction::QuarticOut, - bevy_tweening::EaseFunction::QuarticInOut, - bevy_tweening::EaseFunction::QuinticIn, - bevy_tweening::EaseFunction::QuinticOut, - bevy_tweening::EaseFunction::QuinticInOut, - bevy_tweening::EaseFunction::SineIn, - bevy_tweening::EaseFunction::SineOut, - bevy_tweening::EaseFunction::SineInOut, - bevy_tweening::EaseFunction::CircularIn, - bevy_tweening::EaseFunction::CircularOut, - bevy_tweening::EaseFunction::CircularInOut, - bevy_tweening::EaseFunction::ExponentialIn, - bevy_tweening::EaseFunction::ExponentialOut, - bevy_tweening::EaseFunction::ExponentialInOut, - bevy_tweening::EaseFunction::ElasticIn, - bevy_tweening::EaseFunction::ElasticOut, - bevy_tweening::EaseFunction::ElasticInOut, - bevy_tweening::EaseFunction::BackIn, - bevy_tweening::EaseFunction::BackOut, - bevy_tweening::EaseFunction::BackInOut, - bevy_tweening::EaseFunction::BounceIn, - bevy_tweening::EaseFunction::BounceOut, - bevy_tweening::EaseFunction::BounceInOut, + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, ] { + let tween = Tween::new( + *ease_function, + TweeningType::PingPong, + std::time::Duration::from_secs(1), + SpriteColorLens { + start: Color::RED, + end: Color::BLUE, + }, + ); + commands .spawn_bundle(SpriteBundle { transform: Transform::from_translation(Vec3::new(x, y, 0.)), @@ -70,15 +81,8 @@ fn setup(mut commands: Commands) { }, ..Default::default() }) - .insert(bevy_tweening::Animator::new( - *ease_function, - bevy_tweening::TweeningType::PingPong, - std::time::Duration::from_secs(1), - bevy_tweening::SpriteColorLens { - start: Color::RED, - end: Color::BLUE, - }, - )); + .insert(Animator::new(tween)); + y -= size * spacing; if y < -screen_y { x += size * spacing; diff --git a/examples/transform_rotation.rs b/examples/transform_rotation.rs index 5bf6749..611535f 100644 --- a/examples/transform_rotation.rs +++ b/examples/transform_rotation.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_tweening::*; fn main() -> Result<(), Box<dyn std::error::Error>> { App::default() @@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(TweeningPlugin) .add_startup_system(setup) .run(); @@ -29,37 +30,47 @@ fn setup(mut commands: Commands) { let mut y = screen_y; for ease_function in &[ - bevy_tweening::EaseFunction::QuadraticIn, - bevy_tweening::EaseFunction::QuadraticOut, - bevy_tweening::EaseFunction::QuadraticInOut, - bevy_tweening::EaseFunction::CubicIn, - bevy_tweening::EaseFunction::CubicOut, - bevy_tweening::EaseFunction::CubicInOut, - bevy_tweening::EaseFunction::QuarticIn, - bevy_tweening::EaseFunction::QuarticOut, - bevy_tweening::EaseFunction::QuarticInOut, - bevy_tweening::EaseFunction::QuinticIn, - bevy_tweening::EaseFunction::QuinticOut, - bevy_tweening::EaseFunction::QuinticInOut, - bevy_tweening::EaseFunction::SineIn, - bevy_tweening::EaseFunction::SineOut, - bevy_tweening::EaseFunction::SineInOut, - bevy_tweening::EaseFunction::CircularIn, - bevy_tweening::EaseFunction::CircularOut, - bevy_tweening::EaseFunction::CircularInOut, - bevy_tweening::EaseFunction::ExponentialIn, - bevy_tweening::EaseFunction::ExponentialOut, - bevy_tweening::EaseFunction::ExponentialInOut, - bevy_tweening::EaseFunction::ElasticIn, - bevy_tweening::EaseFunction::ElasticOut, - bevy_tweening::EaseFunction::ElasticInOut, - bevy_tweening::EaseFunction::BackIn, - bevy_tweening::EaseFunction::BackOut, - bevy_tweening::EaseFunction::BackInOut, - bevy_tweening::EaseFunction::BounceIn, - bevy_tweening::EaseFunction::BounceOut, - bevy_tweening::EaseFunction::BounceInOut, + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, ] { + let tween = Tween::new( + *ease_function, + TweeningType::PingPong, + std::time::Duration::from_secs(1), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), + }, + ); + commands .spawn_bundle(( Transform::from_translation(Vec3::new(x, y, 0.)), @@ -70,21 +81,14 @@ fn setup(mut commands: Commands) { .spawn_bundle(SpriteBundle { sprite: Sprite { color: Color::RED, - custom_size: Some(Vec2::new(size, size)), + custom_size: Some(Vec2::new(size, size * 0.5)), ..Default::default() }, ..Default::default() }) - .insert(bevy_tweening::Animator::new( - *ease_function, - bevy_tweening::TweeningType::PingPong, - std::time::Duration::from_secs(1), - bevy_tweening::TransformRotationLens { - start: Quat::IDENTITY, - end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), - }, - )); + .insert(Animator::new(tween)); }); + y -= size * spacing; if y < -screen_y { x += size * spacing; diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs index 106c3d1..73a7ff3 100644 --- a/examples/transform_translation.rs +++ b/examples/transform_translation.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_tweening::*; fn main() -> Result<(), Box<dyn std::error::Error>> { App::default() @@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(TweeningPlugin) .add_startup_system(setup) .run(); @@ -28,37 +29,47 @@ fn setup(mut commands: Commands) { let mut x = -screen_x; for ease_function in &[ - bevy_tweening::EaseFunction::QuadraticIn, - bevy_tweening::EaseFunction::QuadraticOut, - bevy_tweening::EaseFunction::QuadraticInOut, - bevy_tweening::EaseFunction::CubicIn, - bevy_tweening::EaseFunction::CubicOut, - bevy_tweening::EaseFunction::CubicInOut, - bevy_tweening::EaseFunction::QuarticIn, - bevy_tweening::EaseFunction::QuarticOut, - bevy_tweening::EaseFunction::QuarticInOut, - bevy_tweening::EaseFunction::QuinticIn, - bevy_tweening::EaseFunction::QuinticOut, - bevy_tweening::EaseFunction::QuinticInOut, - bevy_tweening::EaseFunction::SineIn, - bevy_tweening::EaseFunction::SineOut, - bevy_tweening::EaseFunction::SineInOut, - bevy_tweening::EaseFunction::CircularIn, - bevy_tweening::EaseFunction::CircularOut, - bevy_tweening::EaseFunction::CircularInOut, - bevy_tweening::EaseFunction::ExponentialIn, - bevy_tweening::EaseFunction::ExponentialOut, - bevy_tweening::EaseFunction::ExponentialInOut, - bevy_tweening::EaseFunction::ElasticIn, - bevy_tweening::EaseFunction::ElasticOut, - bevy_tweening::EaseFunction::ElasticInOut, - bevy_tweening::EaseFunction::BackIn, - bevy_tweening::EaseFunction::BackOut, - bevy_tweening::EaseFunction::BackInOut, - bevy_tweening::EaseFunction::BounceIn, - bevy_tweening::EaseFunction::BounceOut, - bevy_tweening::EaseFunction::BounceInOut, + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, ] { + let tween = Tween::new( + *ease_function, + TweeningType::PingPong, + std::time::Duration::from_secs(1), + TransformPositionLens { + start: Vec3::new(x, screen_y, 0.), + end: Vec3::new(x, -screen_y, 0.), + }, + ); + commands .spawn_bundle(SpriteBundle { sprite: Sprite { @@ -68,15 +79,8 @@ fn setup(mut commands: Commands) { }, ..Default::default() }) - .insert(bevy_tweening::Animator::new( - *ease_function, - bevy_tweening::TweeningType::PingPong, - std::time::Duration::from_secs(1), - bevy_tweening::TransformPositionLens { - start: Vec3::new(x, screen_y, 0.), - end: Vec3::new(x, -screen_y, 0.), - }, - )); + .insert(Animator::new(tween)); + x += size * spacing; } } diff --git a/examples/ui_position.rs b/examples/ui_position.rs index 32a9ec5..dd221f0 100644 --- a/examples/ui_position.rs +++ b/examples/ui_position.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_tweening::*; fn main() -> Result<(), Box<dyn std::error::Error>> { App::default() @@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(TweeningPlugin) .add_startup_system(setup) .run(); @@ -28,37 +29,57 @@ fn setup(mut commands: Commands) { let mut x = 10.; for ease_function in &[ - bevy_tweening::EaseFunction::QuadraticIn, - bevy_tweening::EaseFunction::QuadraticOut, - bevy_tweening::EaseFunction::QuadraticInOut, - bevy_tweening::EaseFunction::CubicIn, - bevy_tweening::EaseFunction::CubicOut, - bevy_tweening::EaseFunction::CubicInOut, - bevy_tweening::EaseFunction::QuarticIn, - bevy_tweening::EaseFunction::QuarticOut, - bevy_tweening::EaseFunction::QuarticInOut, - bevy_tweening::EaseFunction::QuinticIn, - bevy_tweening::EaseFunction::QuinticOut, - bevy_tweening::EaseFunction::QuinticInOut, - bevy_tweening::EaseFunction::SineIn, - bevy_tweening::EaseFunction::SineOut, - bevy_tweening::EaseFunction::SineInOut, - bevy_tweening::EaseFunction::CircularIn, - bevy_tweening::EaseFunction::CircularOut, - bevy_tweening::EaseFunction::CircularInOut, - bevy_tweening::EaseFunction::ExponentialIn, - bevy_tweening::EaseFunction::ExponentialOut, - bevy_tweening::EaseFunction::ExponentialInOut, - bevy_tweening::EaseFunction::ElasticIn, - bevy_tweening::EaseFunction::ElasticOut, - bevy_tweening::EaseFunction::ElasticInOut, - bevy_tweening::EaseFunction::BackIn, - bevy_tweening::EaseFunction::BackOut, - bevy_tweening::EaseFunction::BackInOut, - bevy_tweening::EaseFunction::BounceIn, - bevy_tweening::EaseFunction::BounceOut, - bevy_tweening::EaseFunction::BounceInOut, + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, ] { + let tween = Tween::new( + *ease_function, + TweeningType::PingPong, + std::time::Duration::from_secs(1), + UiPositionLens { + start: Rect { + left: Val::Px(x), + top: Val::Px(10.), + right: Val::Auto, + bottom: Val::Auto, + }, + end: Rect { + left: Val::Px(x), + top: Val::Px(screen_y - 10. - size), + right: Val::Auto, + bottom: Val::Auto, + }, + }, + ); + commands .spawn_bundle(NodeBundle { style: Style { @@ -79,25 +100,8 @@ fn setup(mut commands: Commands) { color: UiColor(Color::RED), ..Default::default() }) - .insert(bevy_tweening::Animator::new( - *ease_function, - bevy_tweening::TweeningType::PingPong, - std::time::Duration::from_secs(1), - bevy_tweening::UiPositionLens { - start: Rect { - left: Val::Px(x), - top: Val::Px(10.), - right: Val::Auto, - bottom: Val::Auto, - }, - end: Rect { - left: Val::Px(x), - top: Val::Px(screen_y - 10. - size), - right: Val::Auto, - bottom: Val::Auto, - }, - }, - )); + .insert(Animator::new(tween)); + x += offset_x; } } diff --git a/src/lib.rs b/src/lib.rs index 78e82e7..5ee6767 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,8 +37,26 @@ //! # 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 Sprite, +//! // for the Animator to animate it. It also contains the start and +//! // end values associated with the animation ratios 0. and 1. +//! TransformPositionLens { +//! start: Vec3::new(0., 0., 0.), +//! end: Vec3::new(1., 2., -4.), +//! }, +//! ); +//! //! commands -//! // Spawn a Sprite entity to animate the position of +//! // Spawn a Sprite entity to animate the position of. //! .spawn_bundle(SpriteBundle { //! sprite: Sprite { //! color: Color::RED, @@ -47,23 +65,8 @@ //! }, //! ..Default::default() //! }) -//! // Add an Animator component to perform the animation -//! .insert(Animator::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 Sprite, -//! // for the Animator to animate it. It also contains the start and -//! // end values associated with the animation ratios 0. and 1. -//! TransformPositionLens { -//! start: Vec3::new(0., 0., 0.), -//! end: Vec3::new(1., 2., -4.), -//! }, -//! )); +//! // Add an Animator component to control and execute the animation. +//! .insert(Animator::new(tween)); //! # } //! ``` //! @@ -218,12 +221,58 @@ impl std::ops::Not for TweeningDirection { } } +/// Playback state of a [`Tweenable`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TweenState { +pub enum TweenState { /// Not animated. Stopped, /// Animating. Running, + /// Animation ended (but stop not called). + Ended, +} + +/// An animatable entity, either a single [`Tween`] or a collection of them. +pub trait Tweenable<T>: Send + Sync { + /// Get the total duration of the animation. + fn duration(&self) -> Duration; + + /// Get the current progress in \[0:1\] of the animation. + fn progress(&self) -> f32; + + /// Tick the animation, advancing it by the given delta time and mutating the + /// given target component or asset. + fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState; + + /// Stop the animation. + fn stop(&mut self); +} + +impl<T> Tweenable<T> for Box<dyn Tweenable<T> + Send + Sync + 'static> { + fn duration(&self) -> Duration { + self.as_ref().duration() + } + fn progress(&self) -> f32 { + self.as_ref().progress() + } + fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState { + self.as_mut().tick(delta, target) + } + fn stop(&mut self) { + self.as_mut().stop() + } +} + +/// Trait for boxing a [`Tweenable`] trait object. +pub trait IntoBoxDynTweenable<T> { + /// Convert the current object into a boxed [`Tweenable`]. + fn into_box_dyn(this: Self) -> Box<dyn Tweenable<T> + Send + Sync + 'static>; +} + +impl<T, U: Tweenable<T> + Send + Sync + 'static> IntoBoxDynTweenable<T> for U { + fn into_box_dyn(this: U) -> Box<dyn Tweenable<T> + Send + Sync + 'static> { + Box::new(this) + } } /// Single tweening animation instance. @@ -238,6 +287,13 @@ pub struct Tween<T> { on_ended: Option<Box<dyn FnMut() + Send + Sync + 'static>>, } +impl<T: 'static> Tween<T> { + /// Chain another [`Tweenable`] after this tween, making a sequence with the two. + pub fn then(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> { + Sequence::from_single(self).then(tween) + } +} + impl<T> Tween<T> { /// Create a new tween animation. pub fn new<L>( @@ -268,18 +324,6 @@ impl<T> Tween<T> { self.direction } - /// 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(), - } - } - /// Set a callback invoked when the animation starts. pub fn set_started<C>(&mut self, callback: C) where @@ -310,8 +354,26 @@ impl<T> Tween<T> { pub fn is_looping(&self) -> bool { self.tweening_type != TweeningType::Once } +} - fn tick(&mut self, delta: Duration, target: &mut T) { +impl<T> Tweenable<T> for Tween<T> { + fn duration(&self) -> Duration { + self.timer.duration() + } + + /// 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). + fn progress(&self) -> f32 { + match self.direction { + TweeningDirection::Forward => self.timer.percent(), + TweeningDirection::Backward => self.timer.percent_left(), + } + } + + fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState { let old_state = self.state; if old_state == TweenState::Stopped { self.state = TweenState::Running; @@ -332,6 +394,7 @@ impl<T> Tween<T> { self.lens.lerp(target, factor); if self.timer.just_finished() { + self.state = TweenState::Ended; // This is always true for non ping-pong, and is true for ping-pong when // coming back to start after a full cycle start -> end -> start. if self.direction == TweeningDirection::Forward { @@ -340,67 +403,99 @@ impl<T> Tween<T> { } } } + + self.state } fn stop(&mut self) { - if self.state == TweenState::Running { - self.state = TweenState::Stopped; - self.timer.reset(); - } + self.state = TweenState::Stopped; + self.timer.reset(); } } /// A sequence of tweens played back in order one after the other. pub struct Sequence<T> { - tweens: Vec<Tween<T>>, + tweens: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>, index: usize, state: TweenState, + duration: Duration, + time: Duration, } impl<T> Sequence<T> { /// Create a new sequence of tweens. - pub fn new<I>(tweens: I) -> Self - where - I: IntoIterator<Item = Tween<T>>, - { + pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self { + let tweens: Vec<_> = items + .into_iter() + .map(IntoBoxDynTweenable::into_box_dyn) + .collect(); + let duration = tweens.iter().map(|t| t.duration()).sum(); Sequence { - tweens: tweens.into_iter().collect(), + tweens, index: 0, state: TweenState::Stopped, + duration, + time: Duration::from_secs(0), } } /// Create a new sequence containing a single tween. - pub fn from_single(tween: Tween<T>) -> Self { + pub fn from_single(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self { + let duration = tween.duration(); Sequence { - tweens: vec![tween], + tweens: vec![Box::new(tween)], index: 0, state: TweenState::Stopped, + duration, + time: Duration::from_secs(0), } } + /// Append a [`Tweenable`] to this sequence. + pub fn then(mut self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self { + self.duration += tween.duration(); + self.tweens.push(Box::new(tween)); + self + } + /// Index of the current active tween in the sequence. pub fn index(&self) -> usize { self.index.min(self.tweens.len() - 1) } /// Get the current active tween in the sequence. - pub fn current(&self) -> &Tween<T> { - &self.tweens[self.index()] + pub fn current(&self) -> &dyn Tweenable<T> { + self.tweens[self.index()].as_ref() + } +} + +impl<T> Tweenable<T> for Sequence<T> { + fn duration(&self) -> Duration { + self.duration + } + + fn progress(&self) -> f32 { + self.time.as_secs_f32() / self.duration.as_secs_f32() } - fn tick(&mut self, delta: Duration, target: &mut T) { + fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState { if self.index < self.tweens.len() { + self.time = (self.time + delta).min(self.duration); let tween = &mut self.tweens[self.index]; - tween.tick(delta, target); - if tween.progress() >= 1.0 { + let state = tween.tick(delta, target); + if state == TweenState::Ended { + tween.stop(); self.index += 1; + if self.index >= self.tweens.len() { + self.state = TweenState::Ended; + } } } + self.state } fn stop(&mut self) { - if self.state == TweenState::Running { + if self.state != TweenState::Stopped { self.state = TweenState::Stopped; if self.index < self.tweens.len() { let tween = &mut self.tweens[self.index]; @@ -410,8 +505,56 @@ impl<T> Sequence<T> { } } -struct Tracks<T> { - tracks: Vec<Sequence<T>>, +/// A collection of [`Tweenable`] executing in parallel. +pub struct Tracks<T> { + tracks: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>, + duration: Duration, + time: Duration, +} + +impl<T> Tracks<T> { + /// Create a new [`Tracks`] from an iterator over a collection of [`Tweenable`]. + pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self { + let tracks: Vec<_> = items + .into_iter() + .map(IntoBoxDynTweenable::into_box_dyn) + .collect(); + let duration = tracks.iter().map(|t| t.duration()).max().unwrap(); + Tracks { + tracks, + duration, + time: Duration::from_secs(0), + } + } +} + +impl<T> Tweenable<T> for Tracks<T> { + fn duration(&self) -> Duration { + self.duration + } + + fn progress(&self) -> f32 { + self.time.as_secs_f32() / self.duration.as_secs_f32() + } + + fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState { + let mut any_running = true; + for tweenable in &mut self.tracks { + any_running = any_running && (tweenable.tick(delta, target) == TweenState::Running); + } + if any_running { + self.time = (self.time + delta).min(self.duration); + TweenState::Running + } else { + TweenState::Ended + } + } + + fn stop(&mut self) { + for seq in &mut self.tracks { + seq.stop(); + } + } } /// Component to control the animation of another component. @@ -420,7 +563,7 @@ pub struct Animator<T: Component> { /// Control if this animation is played or not. pub state: AnimatorState, prev_state: AnimatorState, - tracks: Tracks<T>, + tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>, } impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> { @@ -431,52 +574,22 @@ impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> { } } -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, - duration: Duration, - lens: L, - ) -> Self - where - L: Lens<T> + Send + Sync + 'static, - { - let tween = Tween::new(ease_function, tweening_type, duration, lens); - Animator { - state: AnimatorState::default(), - prev_state: AnimatorState::default(), - 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 { +impl<T: Component> Default for Animator<T> { + fn default() -> Self { Animator { - state: AnimatorState::default(), - prev_state: AnimatorState::default(), - tracks: Tracks { - tracks: vec![Sequence::from_single(tween)], - }, + state: Default::default(), + prev_state: Default::default(), + tweenable: None, } } +} - /// 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 { .. })); - } +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 { Animator { - state: AnimatorState::Playing, - prev_state: AnimatorState::Playing, - tracks: Tracks { - tracks: vec![Sequence::new(tweens)], - }, + tweenable: Some(Box::new(tween)), + ..Default::default() } } @@ -487,14 +600,27 @@ impl<T: Component> Animator<T> { 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 tracks(&self) -> &[Sequence<T>] { - &self.tracks.tracks + 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 tracks_mut(&mut self) -> &mut [Sequence<T>] { - &mut self.tracks.tracks + 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 + } } } @@ -504,7 +630,7 @@ pub struct AssetAnimator<T: Asset> { /// Control if this animation is played or not. pub state: AnimatorState, prev_state: AnimatorState, - tracks: Tracks<T>, + tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>, handle: Handle<T>, } @@ -516,56 +642,24 @@ impl<T: Asset + std::fmt::Debug> std::fmt::Debug for AssetAnimator<T> { } } -impl<T: Asset> AssetAnimator<T> { - /// Create a new asset animator component from an easing function, tweening type, and a lens. - /// The type `T` of the asset to animate can generally be deducted from the lens type itself. - /// The component can be attached on any entity. - pub fn new<L>( - handle: Handle<T>, - ease_function: impl Into<EaseMethod>, - tweening_type: TweeningType, - duration: Duration, - lens: L, - ) -> Self - where - L: Lens<T> + Send + Sync + 'static, - { - let tween = Tween::new(ease_function, tweening_type, duration, lens); - AssetAnimator { - state: AnimatorState::Playing, - prev_state: AnimatorState::Playing, - tracks: Tracks { - tracks: vec![Sequence::from_single(tween)], - }, - handle, - } - } - - /// Create a new animator component from a single tween instance. - pub fn new_single(handle: Handle<T>, tween: Tween<T>) -> Self { +impl<T: Asset> Default for AssetAnimator<T> { + fn default() -> Self { AssetAnimator { - state: AnimatorState::Playing, - prev_state: AnimatorState::Playing, - tracks: Tracks { - tracks: vec![Sequence::from_single(tween)], - }, - handle, + state: Default::default(), + prev_state: Default::default(), + tweenable: None, + handle: Default::default(), } } +} - /// 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 { .. })); - } +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 { AssetAnimator { - state: AnimatorState::Playing, - prev_state: AnimatorState::Playing, - tracks: Tracks { - tracks: vec![Sequence::new(tweens)], - }, + tweenable: Some(Box::new(tween)), handle, + ..Default::default() } } @@ -576,18 +670,31 @@ impl<T: Asset> AssetAnimator<T> { self } - fn handle(&self) -> Handle<T> { - self.handle.clone() + /// 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 tracks(&self) -> &[Sequence<T>] { - &self.tracks.tracks + 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 tracks_mut(&mut self) -> &mut [Sequence<T>] { - &mut self.tracks.tracks + 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() } } @@ -668,6 +775,7 @@ mod tests { (r, ec, dir) } }; + println!("Expected; r={} ec={} dir={:?}", ratio, ec, dir); // Tick the tween tween.tick(tick_duration, &mut transform); @@ -704,7 +812,7 @@ mod tests { end: Quat::from_rotation_x(180_f32.to_radians()), }, ); - let mut seq = Sequence::new([tween1, tween2]); + let mut seq = Sequence::from_single(tween1).then(tween2); let mut transform = Transform::default(); for i in 1..=11 { seq.tick(Duration::from_secs_f32(0.2), &mut transform); @@ -729,7 +837,7 @@ mod tests { /// Animator::new() #[test] fn animator_new() { - let animator = Animator::new( + let tween = Tween::new( EaseFunction::QuadraticInOut, TweeningType::PingPong, std::time::Duration::from_secs(1), @@ -738,21 +846,16 @@ mod tests { end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), }, ); + let animator = Animator::new(tween); assert_eq!(animator.state, AnimatorState::default()); - let tracks = animator.tracks(); - assert_eq!(tracks.len(), 1); - let seq = &tracks[0]; - assert_eq!(seq.tweens.len(), 1); - let tween = &seq.tweens[0]; - assert_eq!(tween.direction(), TweeningDirection::Forward); + let tween = animator.tweenable().unwrap(); assert_eq!(tween.progress(), 0.); } /// AssetAnimator::new() #[test] fn asset_animator_new() { - let animator = AssetAnimator::new( - Handle::<ColorMaterial>::default(), + let tween = Tween::new( EaseFunction::QuadraticInOut, TweeningType::PingPong, std::time::Duration::from_secs(1), @@ -761,13 +864,9 @@ mod tests { end: Color::BLUE, }, ); + let animator = AssetAnimator::new(Handle::<ColorMaterial>::default(), tween); assert_eq!(animator.state, AnimatorState::default()); - let tracks = animator.tracks(); - assert_eq!(tracks.len(), 1); - let seq = &tracks[0]; - assert_eq!(seq.tweens.len(), 1); - let tween = &seq.tweens[0]; - assert_eq!(tween.direction(), TweeningDirection::Forward); + let tween = animator.tweenable().unwrap(); assert_eq!(tween.progress(), 0.); } } diff --git a/src/plugin.rs b/src/plugin.rs index 6b91506..f344d7c 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -54,15 +54,12 @@ pub fn component_animator_system<T: Component>( animator.prev_state = animator.state; if animator.state == AnimatorState::Paused { if state_changed { - for seq in animator.tracks_mut() { - seq.stop(); + if let Some(tweenable) = animator.tweenable_mut() { + tweenable.stop(); } } - } else { - // Play all tracks in parallel - for seq in animator.tracks_mut() { - seq.tick(time.delta(), target); - } + } else if let Some(tweenable) = animator.tweenable_mut() { + tweenable.tick(time.delta(), target); } } } @@ -80,14 +77,13 @@ pub fn asset_animator_system<T: Asset>( animator.prev_state = animator.state; if animator.state == AnimatorState::Paused { if state_changed { - for seq in animator.tracks_mut() { - seq.stop(); + if let Some(tweenable) = animator.tweenable_mut() { + tweenable.stop(); } } } else if let Some(target) = assets.get_mut(animator.handle()) { - // Play all tracks in parallel - for seq in animator.tracks_mut() { - seq.tick(time.delta(), target); + if let Some(tweenable) = animator.tweenable_mut() { + tweenable.tick(time.delta(), target); } } } -- GitLab