From 2f8d66b0d72e46aa9313149b5ca46a5aeefbb226 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Wed, 21 Feb 2024 01:32:59 +0100 Subject: [PATCH] Added a duration for Tweenables and a Sequence of Tweenables (#19) Tweenables now have a duration, which makes sense when using e.g. in a Sequence. The Sequence is currently modeled via tuples. Currently just for `(impl Tweenable, impl Tweenable)` but this may be extended (via macros) to more than just 2 element tuples. This also adds a trait `AnyTweenableElement` which is necessary for the Sequence. --- examples/animatables.rs | 53 +++++++++----- src/view/animatables.rs | 143 ++++++++++++++++++++++++++++++++---- src/widget/animatables.rs | 147 +++++++++++++++++++++++++++++++++++--- 3 files changed, 304 insertions(+), 39 deletions(-) diff --git a/examples/animatables.rs b/examples/animatables.rs index a335f71..e21b21e 100644 --- a/examples/animatables.rs +++ b/examples/animatables.rs @@ -40,6 +40,8 @@ fn main() -> Result<()> { if state.selected_tab == i { 2.0 } else { 1.0 }, )) }; + + let play_speed = if state.maximize { 1.0 } else { -1.0 }; v_stack(( button( "Click this button to animate!".fg(Color::Green), @@ -48,33 +50,52 @@ fn main() -> Result<()> { "Click these tabs to maximize each", weighted_h_stack((tab(0), tab(1), tab(2), tab(3), tab(4))), "Elastic Title" - .fill_max_width((0.2..1.0).elastic_in_out_ease().tween( - Duration::from_secs_f64(3.5), - if state.maximize { 1.0 } else { -1.0 }, - )) + .fill_max_width( + (0.2..1.0) + .duration(Duration::from_secs(3)) + .elastic_in_out_ease() + .play(play_speed), + ) .border((Borders::HORIZONTAL, BorderKind::ThickStraight)), "Quadratic Title" - .fill_max_width((0.2..1.0).quadratic_in_out_ease().tween( - Duration::from_secs_f64(1.0), - if state.maximize { 1.0 } else { -1.0 }, - )) + .fill_max_width((0.2..1.0).quadratic_in_out_ease().play(play_speed)) .border((Borders::HORIZONTAL, BorderKind::ThickStraight)), + "This does some weird stuff" + .fill_max_width( + ( + (0.2..1.0).duration(Duration::from_secs(1)), + (1.0..0.5) + .duration(Duration::from_secs(2)) + .quadratic_in_out_ease(), + ) + .duration(Duration::from_secs(2)) // Note the ratio of the durations specified above stays the same + .play(play_speed), + ) + .border(( + Borders::HORIZONTAL, + BorderKind::ThickStraight, + Style::default().fg(Color::Blue), + )), h_stack(( "This box resizes" .fill_max_size(low_pass(0.05, if state.maximize { 0.2 } else { 0.8 })) .border(Style::default().fg(Color::Red)), "same, but different" .border(BorderKind::Rounded) - .fill_max_height(lerp( - (0.1..1.0).quadratic_in_out_ease(), - low_pass(0.05, if state.maximize { 0.7 } else { 0.1 }), - )), + .fill_max_height( + (0.1..1.0) + .quadratic_in_out_ease() + .lerp(low_pass(0.05, if state.maximize { 0.7 } else { 0.1 })), + ), )), "Expanding Title 2" - .fill_max_width((0.2..1.0).reverse().quadratic_out_ease().tween( - Duration::from_secs_f64(1.5), - if state.maximize { 1.0 } else { -1.0 }, - )) + .fill_max_width( + (0.2..1.0) + .reverse() + .duration(Duration::from_millis(500)) + .quadratic_out_ease() + .play(play_speed), + ) .border((Borders::HORIZONTAL, BorderKind::DoubleStraight)), )) }, diff --git a/src/view/animatables.rs b/src/view/animatables.rs index 3a5d8b8..d2da351 100644 --- a/src/view/animatables.rs +++ b/src/view/animatables.rs @@ -258,15 +258,25 @@ pub trait Tweenable: Send + Sync { message: Box, ) -> MessageResult<()>; - fn tween(self, duration: Duration, play_speed: PS) -> Tween + /// Overrides the duration of any tweenable it composes + fn duration(self, duration: Duration) -> WithDuration + where + Self: Sized, + { + WithDuration { + tweenable: self, + duration, + } + } + + fn play(self, play_speed: PS) -> PlayTween where Self: Sized, PS: Animatable, { - Tween { + PlayTween { play_speed, tweenable: self, - duration, } } @@ -372,15 +382,127 @@ impl> Tweenable for Range { } } +// Sequence of multiple tweenables, not sure yet whether this should be done via tuples (as that syntax is already used by ViewSequences) +impl, T2: Tweenable> Tweenable for (T1, T2) { + type State = ((Id, T1::State), (Id, T2::State)); + + type Element = widget::animatables::Sequence; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, (state, element)) = cx.with_new_id(|cx| { + let (id0, state0, element0) = self.0.build(cx); + let (id1, state1, element1) = self.1.build(cx); + let element = + widget::animatables::Sequence::new(vec![Box::new(element0), Box::new(element1)]); + (((id0, state0), (id1, state1)), element) + }); + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + ((id0, state0), (id1, state1)): &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.with_id(*id, |cx| { + self.0.rebuild( + cx, + &prev.0, + id0, + state0, + (*element.tweenables[0]) + .as_any_mut() + .downcast_mut() + .unwrap(), + ) | self.1.rebuild( + cx, + &prev.1, + id1, + state1, + (*element.tweenables[1]) + .as_any_mut() + .downcast_mut() + .unwrap(), + ) + }) + } + + fn message( + &self, + id_path: &[Id], + ((id0, state0), (id1, state1)): &mut Self::State, + message: Box, + ) -> MessageResult<()> { + match id_path { + [id, rest_path @ ..] if id == id0 => self.0.message(rest_path, state0, message), + [id, rest_path @ ..] if id == id1 => self.1.message(rest_path, state1, message), + [..] => MessageResult::Stale(message), + } + } +} + +// TODO should this also be used within other animatables directly (not just Tweenable)? +// TODO Duration could be animated too +/// Overrides the duration of any tweenable it composes +pub struct WithDuration { + pub(crate) tweenable: T, + pub(crate) duration: Duration, +} + +impl> Tweenable for WithDuration { + type State = T::State; + + type Element = widget::animatables::WithDuration; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, state, element) = self.tweenable.build(cx); + ( + id, + state, + widget::animatables::WithDuration::new(element, self.duration), + ) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut changeflags = ChangeFlags::empty(); + if self.duration != prev.duration { + element.duration = self.duration; + changeflags |= ChangeFlags::ANIMATION; + } + changeflags + | self + .tweenable + .rebuild(cx, &prev.tweenable, id, state, &mut element.tweenable) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + ) -> MessageResult<()> { + self.tweenable.message(id_path, state, message) + } +} + // TODO Duration could also be animated, but I'm not sure it's worth the complexity (vs benefit)... #[derive(Clone, Debug)] -pub struct Tween { +pub struct PlayTween { play_speed: PS, tweenable: TW, - duration: Duration, } -impl Animatable for Tween +impl Animatable for PlayTween where V: 'static, PS: Animatable, @@ -388,18 +510,15 @@ where { type State = (Id, PS::State, Id, TW::State); - type Element = widget::animatables::Tween; + type Element = widget::animatables::PlayTween; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let (id, (state, element)) = cx.with_new_id(|cx| { let (play_speed_id, play_speed_state, play_speed_element) = self.play_speed.build(cx); let (tweenable_id, tweenable_state, tweenable_element) = self.tweenable.build(cx); - let element = widget::animatables::Tween::new( - play_speed_element, - tweenable_element, - self.duration, - ); + let element = + widget::animatables::PlayTween::new(play_speed_element, tweenable_element); ( ( play_speed_id, diff --git a/src/widget/animatables.rs b/src/widget/animatables.rs index 9b26249..2918cf2 100644 --- a/src/widget/animatables.rs +++ b/src/widget/animatables.rs @@ -129,30 +129,28 @@ impl, R: AnimatableElement> AnimatableElement // TODO Duration could also be animated, but I'm not sure it's worth the complexity (vs benefit)... #[derive(Clone, Debug)] -pub struct Tween { +pub struct PlayTween { pub(crate) play_speed: PS, current_time: Duration, pub(crate) tweenable: TW, - duration: Duration, } -impl Tween { - pub(crate) fn new(play_speed: PS, tweenable: TW, duration: Duration) -> Self { - Tween { +impl PlayTween { + pub(crate) fn new(play_speed: PS, tweenable: TW) -> Self { + PlayTween { play_speed, tweenable, current_time: Duration::ZERO, - duration, } } } impl, TW: TweenableElement> AnimatableElement - for Tween + for PlayTween { fn animate(&mut self, cx: &mut LifeCycleCx) -> &V { let play_speed = self.play_speed.animate(cx); - let duration_as_secs = self.duration.as_secs_f64(); + let duration_as_secs = self.tweenable.duration().as_secs_f64(); let current_time_as_secs = self.current_time.as_secs_f64(); let new_time = (current_time_as_secs + *play_speed * cx.time_since_last_render_request().as_secs_f64()) @@ -164,8 +162,8 @@ impl, TW: TweenableElement> Animatable current_time_as_secs / duration_as_secs }; - if !self.duration.is_zero() - && ((*play_speed > 0.0 && self.current_time != self.duration) + if !self.tweenable.duration().is_zero() + && ((*play_speed > 0.0 && self.current_time != self.tweenable.duration()) || (*play_speed < 0.0 && self.current_time != Duration::ZERO)) { self.current_time = Duration::from_secs_f64(new_time); @@ -177,8 +175,50 @@ impl, TW: TweenableElement> Animatable // ---------------------------------- TWEENABLE -pub trait TweenableElement: 'static { +pub trait TweenableElement: 'static + AnyTweenableElement { fn interpolate(&mut self, cx: &mut LifeCycleCx, ratio: f64) -> &V; + + // TODO &mut? + // Could also be collecting a default from some kind of context (LifeCycleCx?) + /// Default duration is 1 second + fn duration(&mut self) -> Duration { + Duration::from_secs(1) + } +} + +pub trait AnyTweenableElement { + fn as_any(&self) -> &dyn Any; + + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn type_name(&self) -> &'static str; +} + +impl> AnyTweenableElement for A { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } +} + +impl TweenableElement for Box> { + fn interpolate(&mut self, cx: &mut LifeCycleCx, ratio: f64) -> &V { + self.deref_mut().interpolate(cx, ratio) + } + + // TODO &mut? + // Could also be collecting a default from some kind of context (LifeCycleCx?) + /// Default duration is 1 second + fn duration(&mut self) -> Duration { + self.deref_mut().duration() + } } pub struct TweenableRange { @@ -206,8 +246,89 @@ impl> TweenableElement for TweenableRange &mut self.value } } + +/// Overrides the duration of any tweenable it composes +pub struct WithDuration { + pub(crate) tweenable: T, + pub(crate) duration: Duration, +} + +impl WithDuration { + pub(crate) fn new(tweenable: T, duration: Duration) -> Self { + Self { + tweenable, + duration, + } + } +} + +impl> TweenableElement for WithDuration { + fn interpolate(&mut self, cx: &mut LifeCycleCx, ratio: f64) -> &V { + self.tweenable.interpolate(cx, ratio) + } + + fn duration(&mut self) -> Duration { + self.duration + } +} + +pub struct Sequence { + pub(crate) tweenables: Vec>>, +} + +impl Sequence { + pub fn new(tweenables: Vec>>) -> Self { + Self { tweenables } + } +} + +impl TweenableElement for Sequence { + fn interpolate(&mut self, cx: &mut LifeCycleCx, mut ratio: f64) -> &V { + let total_duration = self + .tweenables + .iter_mut() + .fold(Duration::ZERO, |acc, tweenable| acc + tweenable.duration()); + let total_duration_f64 = total_duration.as_secs_f64(); + + if self.tweenables.is_empty() { + panic!("A Sequence should never be empty"); + } + + let mut duration_acc = Duration::ZERO; + let mut target_ix = None; + + for (ix, tweenable) in self.tweenables.iter_mut().enumerate() { + let tween_duration = tweenable.duration(); + let next_duration_acc = duration_acc + tween_duration; + let start_ratio = duration_acc.as_secs_f64() / total_duration_f64; + let end_ratio = next_duration_acc.as_secs_f64() / total_duration_f64; + + if ratio >= start_ratio && ratio < end_ratio { + target_ix = Some(ix); + ratio = (ratio - start_ratio) / (end_ratio - start_ratio); + break; + } + + duration_acc = next_duration_acc; + } + + if let Some(ix) = target_ix { + self.tweenables[ix].interpolate(cx, ratio) + } else { + self.tweenables.last_mut().unwrap().interpolate(cx, 1.0) + } + } + + fn duration(&mut self) -> Duration { + self.tweenables + .iter_mut() + .fold(Duration::ZERO, |acc, tweenable| acc + tweenable.duration()) + } +} + pub mod ease { use crate::widget::LifeCycleCx; + use std::time::Duration; use super::TweenableElement; @@ -221,6 +342,10 @@ pub mod ease { #[allow(clippy::redundant_closure_call)] self.0.interpolate(cx, $ease_fn(ratio)) } + + fn duration(&mut self) -> Duration { + self.0.duration() + } } }; }