diff --git a/CHANGELOG b/CHANGELOG index 6834129b3a6dd1cba0c8367ae1d380529a722e78..6ddcbbf9b41b4a3956e002690602d82273fa0026 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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). +- Added `Delay` tweenable for a time delay between other tweens. ### Changed diff --git a/Cargo.toml b/Cargo.toml index 9f0473ac1a1068cb9329f11065845d19c73a6c02..4fbc4d72b5515d463c3dc5d2a5b73cbfe5356d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ version = "0.6" default-features = false features = [ "render" ] +[dev-dependencies] +bevy-inspector-egui = "0.8" + [[example]] name = "colormaterial_color" required-features = [ "bevy/bevy_winit" ] diff --git a/README.md b/README.md index f71d9b9a745414954676ec9c8e4e9c68614a5cb7..c249c29c4b057878f3b5a59c550d778f09d7239d 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,14 @@ The process is similar to custom components, creating a custom lens for the cust See the [`examples/`](https://github.com/djeedai/bevy_tweening/tree/main/examples) folder. +### [`menu`](examples/menu.rs) + +```rust +cargo run --example menu --features="bevy/bevy_winit" +``` + + + ### [`sprite_color`](examples/sprite_color.rs) ```rust diff --git a/examples/menu.gif b/examples/menu.gif new file mode 100644 index 0000000000000000000000000000000000000000..9017c5932e79f71213d7ec0fdda4fb5ce1638e77 Binary files /dev/null and b/examples/menu.gif differ diff --git a/examples/menu.rs b/examples/menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f3dbf3072e0c9f06c7b853e80198c999065dcfb --- /dev/null +++ b/examples/menu.rs @@ -0,0 +1,103 @@ +use bevy::prelude::*; +use bevy_inspector_egui::WorldInspectorPlugin; +use bevy_tweening::*; +use std::time::Duration; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + App::default() + .insert_resource(WindowDescriptor { + title: "Menu".to_string(), + width: 800., + height: 400., + vsync: true, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_plugin(TweeningPlugin) + .add_plugin(WorldInspectorPlugin::new()) + .add_startup_system(setup) + .run(); + + Ok(()) +} + +fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { + commands.spawn_bundle(UiCameraBundle::default()); + + let font = asset_server.load("fonts/FiraMono-Regular.ttf"); + + let container = commands + .spawn_bundle(NodeBundle { + style: Style { + position_type: PositionType::Absolute, + position: Rect::all(Val::Px(0.)), + margin: Rect::all(Val::Px(16.)), + padding: Rect::all(Val::Px(16.)), + flex_direction: FlexDirection::ColumnReverse, + align_content: AlignContent::Center, + align_items: AlignItems::Center, + align_self: AlignSelf::Center, + justify_content: JustifyContent::Center, + ..Default::default() + }, + color: UiColor(Color::NONE), + ..Default::default() + }) + .insert(Name::new("container")) + .id(); + + let mut start_time_ms = 0; + for text in &["Continue", "New Game", "Settings", "Quit"] { + let delay = Delay::new(Duration::from_millis(start_time_ms)); + start_time_ms += 500; + let tween_scale = Tween::new( + EaseFunction::BounceOut, + TweeningType::Once, + Duration::from_secs(2), + TransformScaleLens { + start: Vec3::splat(0.01), + end: Vec3::ONE, + }, + ); + let seq = delay.then(tween_scale); + commands + .spawn_bundle(NodeBundle { + node: Node { + size: Vec2::new(300., 80.), + }, + style: Style { + min_size: Size::new(Val::Px(300.), Val::Px(80.)), + margin: Rect::all(Val::Px(8.)), + padding: Rect::all(Val::Px(8.)), + align_content: AlignContent::Center, + align_items: AlignItems::Center, + align_self: AlignSelf::Center, + justify_content: JustifyContent::Center, + ..Default::default() + }, + color: UiColor(Color::rgb_u8(162, 226, 95)), + transform: Transform::from_scale(Vec3::splat(0.01)), + ..Default::default() + }) + .insert(Name::new(format!("button:{}", text))) + .insert(Parent(container)) + .insert(Animator::new(seq)) + .with_children(|parent| { + parent.spawn_bundle(TextBundle { + text: Text::with_section( + text.to_string(), + TextStyle { + font: font.clone(), + font_size: 48.0, + color: Color::rgb_u8(83, 163, 130), + }, + TextAlignment { + vertical: VerticalAlign::Center, + horizontal: HorizontalAlign::Center, + }, + ), + ..Default::default() + }); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5ee676700fd9029caaaeeafb4a08d722ed7cbb18..29107f5bd46a26a60b903da40ee3a379ceb83d6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ //! end: Vec3::new(1., 2., -4.), //! }, //! ); -//! +//! //! commands //! // Spawn a Sprite entity to animate the position of. //! .spawn_bundle(SpriteBundle { @@ -106,8 +106,6 @@ //! [`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 std::time::Duration; - use bevy::{asset::Asset, prelude::*}; use interpolation::Ease as IEase; @@ -116,12 +114,14 @@ pub use interpolation::Lerp; mod lens; mod plugin; +mod tweenable; pub use lens::{ ColorMaterialColorLens, Lens, SpriteColorLens, TextColorLens, TransformPositionLens, TransformRotationLens, TransformScaleLens, UiPositionLens, }; 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)] @@ -232,331 +232,6 @@ pub enum TweenState { 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. -pub struct Tween<T> { - ease_function: EaseMethod, - timer: Timer, - state: TweenState, - tweening_type: TweeningType, - direction: TweeningDirection, - lens: Box<dyn Lens<T> + Send + Sync + 'static>, - on_started: Option<Box<dyn FnMut() + Send + Sync + 'static>>, - 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>( - ease_function: impl Into<EaseMethod>, - tweening_type: TweeningType, - duration: Duration, - lens: L, - ) -> Self - where - L: Lens<T> + Send + Sync + 'static, - { - Tween { - ease_function: ease_function.into(), - timer: Timer::new(duration, tweening_type != TweeningType::Once), - state: TweenState::Stopped, - tweening_type, - direction: TweeningDirection::Forward, - lens: Box::new(lens), - on_started: None, - on_ended: None, - } - } - - /// The current animation direction. - /// - /// See [`TweeningDirection`] for details. - pub fn direction(&self) -> TweeningDirection { - self.direction - } - - /// Set a callback invoked when the animation starts. - pub fn set_started<C>(&mut self, callback: C) - where - C: FnMut() + Send + Sync + 'static, - { - self.on_started = Some(Box::new(callback)); - } - - /// Clear the callback invoked when the animation starts. - pub fn clear_started(&mut self) { - self.on_started = None; - } - - /// Set a callback invoked when the animation ends. - pub fn set_ended<C>(&mut self, callback: C) - where - C: FnMut() + Send + Sync + 'static, - { - self.on_ended = Some(Box::new(callback)); - } - - /// Clear the callback invoked when the animation ends. - pub fn clear_ended(&mut self) { - self.on_ended = None; - } - - /// Is the animation playback looping? - pub fn is_looping(&self) -> bool { - self.tweening_type != TweeningType::Once - } -} - -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; - if let Some(cb) = &mut self.on_started { - cb(); - } - } - - self.timer.tick(delta); - - // Toggle direction immediately, so self.progress() returns the correct ratio - if self.timer.just_finished() && self.tweening_type == TweeningType::PingPong { - self.direction = !self.direction; - } - - let progress = self.progress(); - let factor = self.ease_function.sample(progress); - 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 { - if let Some(cb) = &mut self.on_ended { - cb(); - } - } - } - - self.state - } - - fn stop(&mut self) { - 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<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(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, - index: 0, - state: TweenState::Stopped, - duration, - time: Duration::from_secs(0), - } - } - - /// Create a new sequence containing a single tween. - pub fn from_single(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self { - let duration = tween.duration(); - Sequence { - 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) -> &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) -> TweenState { - if self.index < self.tweens.len() { - self.time = (self.time + delta).min(self.duration); - let tween = &mut self.tweens[self.index]; - 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::Stopped { - self.state = TweenState::Stopped; - if self.index < self.tweens.len() { - let tween = &mut self.tweens[self.index]; - tween.stop(); - } - } - } -} - -/// 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. #[derive(Component)] pub struct Animator<T: Component> { @@ -701,142 +376,50 @@ impl<T: Asset> AssetAnimator<T> { #[cfg(test)] mod tests { use super::*; - use std::sync::{Arc, Mutex}; - - /// Utility to compare floating-point values with a tolerance. - fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool { - (a - b).abs() < tol - } - /// Test ticking of a single tween in isolation. + /// Animator::new() #[test] - fn tween_tick() { - for tweening_type in &[ - TweeningType::Once, - TweeningType::Loop, + fn animator_new() { + let tween = Tween::new( + EaseFunction::QuadraticInOut, TweeningType::PingPong, - ] { - // Create a linear tween over 1 second - let mut tween = Tween::new( - EaseMethod::Linear, - *tweening_type, - Duration::from_secs_f32(1.0), - TransformPositionLens { - start: Vec3::ZERO, - end: Vec3::ONE, - }, - ); - - // Register callbacks to count started/ended events - let started_count = Arc::new(Mutex::new(0)); - let ended_count = Arc::new(Mutex::new(0)); - let sc = Arc::clone(&started_count); - let ec = Arc::clone(&ended_count); - tween.set_started(move || { - let mut sc = sc.lock().unwrap(); - *sc += 1; - }); - tween.set_ended(move || { - let mut ec = ec.lock().unwrap(); - *ec += 1; - }); - assert_eq!(*started_count.lock().unwrap(), 0); - assert_eq!(*ended_count.lock().unwrap(), 0); - - // Loop over 2.2 seconds, so greater than one ping-pong loop - let mut transform = Transform::default(); - let tick_duration = Duration::from_secs_f32(0.2); - for i in 1..=11 { - // Calculate expected values - let (ratio, ec, dir) = match tweening_type { - TweeningType::Once => { - let r = (i as f32 * 0.2).min(1.0); - let ec = if i >= 5 { 1 } else { 0 }; - (r, ec, TweeningDirection::Forward) - } - TweeningType::Loop => { - let r = (i as f32 * 0.2).fract(); - let ec = i / 5; - (r, ec, TweeningDirection::Forward) - } - TweeningType::PingPong => { - let i10 = i % 10; - let r = if i10 >= 5 { - (10 - i10) as f32 * 0.2 - } else { - i10 as f32 * 0.2 - }; - let ec = i / 10; - let dir = if i10 >= 5 { - TweeningDirection::Backward - } else { - TweeningDirection::Forward - }; - (r, ec, dir) - } - }; - println!("Expected; r={} ec={} dir={:?}", ratio, ec, dir); - - // Tick the tween - tween.tick(tick_duration, &mut transform); - - // Check actual values - assert_eq!(tween.direction(), dir); - assert!(abs_diff_eq(tween.progress(), ratio, 1e-5)); - assert!(transform.translation.abs_diff_eq(Vec3::splat(ratio), 1e-5)); - assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); - assert_eq!(*started_count.lock().unwrap(), 1); - assert_eq!(*ended_count.lock().unwrap(), ec); - } - } - } - - /// Test ticking a sequence of tweens. - #[test] - fn seq_tick() { - let tween1 = Tween::new( - EaseMethod::Linear, - TweeningType::Once, - Duration::from_secs_f32(1.0), - TransformPositionLens { - start: Vec3::ZERO, - end: Vec3::ONE, - }, - ); - let tween2 = Tween::new( - EaseMethod::Linear, - TweeningType::Once, - Duration::from_secs_f32(1.0), + std::time::Duration::from_secs(1), TransformRotationLens { start: Quat::IDENTITY, - end: Quat::from_rotation_x(180_f32.to_radians()), + end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.), }, ); - 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); - if i <= 5 { - let r = i as f32 * 0.2; - assert_eq!(transform, Transform::from_translation(Vec3::splat(r))); - } else if i <= 10 { - let alpha_deg = (36 * (i - 5)) as f32; - assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5)); - } else { - assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); - assert!(transform - .rotation - .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); - } + 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::new() + /// Animator::default() + Animator::set_tweenable() #[test] - fn animator_new() { + fn animator_default() { + let mut animator = Animator::<Transform>::default(); + assert!(animator.tweenable().is_none()); + assert!(animator.tweenable_mut().is_none()); + let tween = Tween::new( EaseFunction::QuadraticInOut, TweeningType::PingPong, @@ -846,10 +429,9 @@ 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 tween = animator.tweenable().unwrap(); - assert_eq!(tween.progress(), 0.); + animator.set_tweenable(tween); + assert!(animator.tweenable().is_some()); + assert!(animator.tweenable_mut().is_some()); } /// AssetAnimator::new() @@ -869,4 +451,46 @@ mod tests { let tween = animator.tweenable().unwrap(); assert_eq!(tween.progress(), 0.); } + + /// 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()); + } } diff --git a/src/tweenable.rs b/src/tweenable.rs new file mode 100644 index 0000000000000000000000000000000000000000..1341253a75c0405dbc512e67a575ae5605451d28 --- /dev/null +++ b/src/tweenable.rs @@ -0,0 +1,552 @@ +use bevy::prelude::*; +use std::time::Duration; + +use crate::{EaseMethod, Lens, TweenState, TweeningDirection, TweeningType}; + +/// 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. +pub struct Tween<T> { + ease_function: EaseMethod, + timer: Timer, + state: TweenState, + tweening_type: TweeningType, + direction: TweeningDirection, + lens: Box<dyn Lens<T> + Send + Sync + 'static>, + on_started: Option<Box<dyn FnMut() + Send + Sync + 'static>>, + 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>( + ease_function: impl Into<EaseMethod>, + tweening_type: TweeningType, + duration: Duration, + lens: L, + ) -> Self + where + L: Lens<T> + Send + Sync + 'static, + { + Tween { + ease_function: ease_function.into(), + timer: Timer::new(duration, tweening_type != TweeningType::Once), + state: TweenState::Stopped, + tweening_type, + direction: TweeningDirection::Forward, + lens: Box::new(lens), + on_started: None, + on_ended: None, + } + } + + /// The current animation direction. + /// + /// See [`TweeningDirection`] for details. + pub fn direction(&self) -> TweeningDirection { + self.direction + } + + /// Set a callback invoked when the animation starts. + pub fn set_started<C>(&mut self, callback: C) + where + C: FnMut() + Send + Sync + 'static, + { + self.on_started = Some(Box::new(callback)); + } + + /// Clear the callback invoked when the animation starts. + pub fn clear_started(&mut self) { + self.on_started = None; + } + + /// Set a callback invoked when the animation ends. + pub fn set_ended<C>(&mut self, callback: C) + where + C: FnMut() + Send + Sync + 'static, + { + self.on_ended = Some(Box::new(callback)); + } + + /// Clear the callback invoked when the animation ends. + pub fn clear_ended(&mut self) { + self.on_ended = None; + } + + /// Is the animation playback looping? + pub fn is_looping(&self) -> bool { + self.tweening_type != TweeningType::Once + } +} + +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; + if let Some(cb) = &mut self.on_started { + cb(); + } + } + + self.timer.tick(delta); + + // Toggle direction immediately, so self.progress() returns the correct ratio + if self.timer.just_finished() && self.tweening_type == TweeningType::PingPong { + self.direction = !self.direction; + } + + let progress = self.progress(); + let factor = self.ease_function.sample(progress); + 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 { + if let Some(cb) = &mut self.on_ended { + cb(); + } + } + } + + self.state + } + + fn stop(&mut self) { + 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<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(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, + index: 0, + state: TweenState::Stopped, + duration, + time: Duration::from_secs(0), + } + } + + /// Create a new sequence containing a single tween. + pub fn from_single(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self { + let duration = tween.duration(); + Sequence { + 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) -> &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) -> TweenState { + if self.index < self.tweens.len() { + self.time = (self.time + delta).min(self.duration); + let tween = &mut self.tweens[self.index]; + 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::Stopped { + self.state = TweenState::Stopped; + if self.index < self.tweens.len() { + let tween = &mut self.tweens[self.index]; + tween.stop(); + } + } + } +} + +/// 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(); + } + } +} + + +/// A collection of [`Tweenable`] executing in parallel. +pub struct Delay { + timer: Timer, +} + +impl Delay { + /// Create a new [`Tracks`] from an iterator over a collection of [`Tweenable`]. + pub fn new(duration: Duration) -> Self { + Delay { + timer: Timer::new(duration, false), + } + } + + /// Chain another [`Tweenable`] after this tween, making a sequence with the two. + pub fn then<T>(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> { + Sequence::from_single(self).then(tween) + } +} + +impl<T> Tweenable<T> for Delay { + fn duration(&self) -> Duration { + self.timer.duration() + } + + fn progress(&self) -> f32 { + self.timer.percent() + } + + fn tick(&mut self, delta: Duration, _: &mut T) -> TweenState { + self.timer.tick(delta); + if self.timer.finished() { + TweenState::Ended + } else { + TweenState::Running + } + } + + fn stop(&mut self) { + self.timer.reset(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{TransformPositionLens, TransformRotationLens}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + /// Utility to compare floating-point values with a tolerance. + fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool { + (a - b).abs() < tol + } + + /// Test ticking of a single tween in isolation. + #[test] + fn tween_tick() { + for tweening_type in &[ + TweeningType::Once, + TweeningType::Loop, + TweeningType::PingPong, + ] { + // Create a linear tween over 1 second + let mut tween = Tween::new( + EaseMethod::Linear, + *tweening_type, + Duration::from_secs_f32(1.0), + TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }, + ); + + // Register callbacks to count started/ended events + let started_count = Arc::new(Mutex::new(0)); + let ended_count = Arc::new(Mutex::new(0)); + let sc = Arc::clone(&started_count); + let ec = Arc::clone(&ended_count); + tween.set_started(move || { + let mut sc = sc.lock().unwrap(); + *sc += 1; + }); + tween.set_ended(move || { + let mut ec = ec.lock().unwrap(); + *ec += 1; + }); + assert_eq!(*started_count.lock().unwrap(), 0); + assert_eq!(*ended_count.lock().unwrap(), 0); + + // Loop over 2.2 seconds, so greater than one ping-pong loop + let mut transform = Transform::default(); + let tick_duration = Duration::from_secs_f32(0.2); + for i in 1..=11 { + // Calculate expected values + let (ratio, ec, dir) = match tweening_type { + TweeningType::Once => { + let r = (i as f32 * 0.2).min(1.0); + let ec = if i >= 5 { 1 } else { 0 }; + (r, ec, TweeningDirection::Forward) + } + TweeningType::Loop => { + let r = (i as f32 * 0.2).fract(); + let ec = i / 5; + (r, ec, TweeningDirection::Forward) + } + TweeningType::PingPong => { + let i10 = i % 10; + let r = if i10 >= 5 { + (10 - i10) as f32 * 0.2 + } else { + i10 as f32 * 0.2 + }; + let ec = i / 10; + let dir = if i10 >= 5 { + TweeningDirection::Backward + } else { + TweeningDirection::Forward + }; + (r, ec, dir) + } + }; + println!("Expected; r={} ec={} dir={:?}", ratio, ec, dir); + + // Tick the tween + tween.tick(tick_duration, &mut transform); + + // Check actual values + assert_eq!(tween.direction(), dir); + assert!(abs_diff_eq(tween.progress(), ratio, 1e-5)); + assert!(transform.translation.abs_diff_eq(Vec3::splat(ratio), 1e-5)); + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + assert_eq!(*started_count.lock().unwrap(), 1); + assert_eq!(*ended_count.lock().unwrap(), ec); + } + } + } + + /// Test ticking a sequence of tweens. + #[test] + fn seq_tick() { + let tween1 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }, + ); + let tween2 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_x(180_f32.to_radians()), + }, + ); + let mut seq = tween1.then(tween2); + let mut transform = Transform::default(); + for i in 1..=16 { + seq.tick(Duration::from_secs_f32(0.2), &mut transform); + if i <= 5 { + let r = i as f32 * 0.2; + assert_eq!(transform, Transform::from_translation(Vec3::splat(r))); + } else if i <= 10 { + let alpha_deg = (36 * (i - 5)) as f32; + assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5)); + } else { + assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + } + } + } + + /// Test ticking parallel tracks of tweens. + #[test] + fn tracks_tick() { + let tween1 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(1.0), + TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }, + ); + let tween2 = Tween::new( + EaseMethod::Linear, + TweeningType::Once, + Duration::from_secs_f32(0.8), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_x(180_f32.to_radians()), + }, + ); + let mut tracks = Tracks::new([tween1, tween2]); + let mut transform = Transform::default(); + for i in 1..=6 { + tracks.tick(Duration::from_secs_f32(0.2), &mut transform); + if i <= 5 { + let r = i as f32 * 0.2; + let alpha_deg = (45 * i.min(4)) as f32; + assert!(transform.translation.abs_diff_eq(Vec3::splat(r), 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5)); + } else { + assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5)); + } + } + } +}