From d0659812f5a7e6b9c5c3e33b158a4eaad7785383 Mon Sep 17 00:00:00 2001 From: Jared Moulton Date: Wed, 19 Feb 2025 01:24:53 -0700 Subject: [PATCH] Improve animation reverse (#780) Previously, animation in auto reverse would run at double time and when greater than 50% of the duration had passed, it would essentially run time backwards. This was fine with most easing functions but would lead to unnatural results when using Spring easing. I had previously built in a reverse_once that worked around this, when specifically opted into, but it makes sense for it to be how reversing is always done. Now time does not run backwards and animations don't run at double time. Both of these things are good changes. Now reversing all easing functions look natural. --- src/animate.rs | 97 ++++++++++++++++++++++++++------------ src/context.rs | 5 +- src/views/dyn_container.rs | 3 +- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/animate.rs b/src/animate.rs index 28b4cc3b..d57c12ad 100644 --- a/src/animate.rs +++ b/src/animate.rs @@ -310,6 +310,7 @@ pub(crate) enum AnimState { // NOTE: If animation has `RepeatMode::LoopForever`, this state will never be reached. Completed { elapsed: Option, + was_reversing: bool, }, } @@ -343,6 +344,8 @@ pub enum AnimStateCommand { Start, /// Stop the animation Stop, + /// Start the animation in reverse + Reverse, } type EffectStateVec = SmallVec<[RwSignal); 1]>>; 1]>; @@ -709,6 +712,19 @@ impl Animation { ) } + /// The animation will receive a reverse command any time the trigger function tracks any reactive updates. + /// + /// This will start the animation in reverse + pub fn reverse(self, trigger: impl Fn() + 'static) -> Self { + self.state( + move || { + trigger(); + AnimStateCommand::Reverse + }, + false, + ) + } + /// The animation will receive a stop command any time the trigger function tracks any reactive updates. pub fn stop(self, trigger: impl Fn() + 'static) -> Self { self.state( @@ -746,6 +762,10 @@ impl Animation { self.transition(AnimStateCommand::Start) } + pub(crate) fn reverse_mut(&mut self) { + self.transition(AnimStateCommand::Reverse) + } + #[allow(unused)] pub(crate) fn stop_mut(&mut self) { self.transition(AnimStateCommand::Stop) @@ -844,6 +864,11 @@ impl Animation { was_in_ext, } => match self.repeat_mode { RepeatMode::LoopForever => { + if self.reverse_once.is_rev() { + self.reverse_once.set(false); + } else if self.auto_reverse { + self.reverse_once.set(true); + } self.state = AnimState::PassInProgress { started_on: Instant::now(), elapsed: Duration::ZERO, @@ -852,6 +877,7 @@ impl Animation { RepeatMode::Times(times) => { self.repeat_count += 1; if self.repeat_count >= times { + let was_reversing = self.reverse_once.is_rev(); self.reverse_once.set(false); self.on_complete.notify(); if !*was_in_ext { @@ -859,6 +885,7 @@ impl Animation { } self.state = AnimState::Completed { elapsed: Some(*elapsed), + was_reversing, } } else { self.state = AnimState::PassInProgress { @@ -874,7 +901,13 @@ impl Animation { AnimState::Stopped => { debug_assert!(false, "Tried to advance a stopped animation") } - AnimState::Completed { .. } => {} + AnimState::Completed { was_reversing, .. } => { + if self.auto_reverse && !*was_reversing { + self.reverse_mut(); + } else { + self.state = AnimState::Stopped; + } + } } } @@ -894,6 +927,16 @@ impl Animation { } } AnimStateCommand::Start => { + self.reverse_once.set(false); + self.folded_style.map.clear(); + self.repeat_count = 0; + self.state = AnimState::PassInProgress { + started_on: Instant::now(), + elapsed: Duration::ZERO, + } + } + AnimStateCommand::Reverse => { + self.reverse_once.set(true); self.folded_style.map.clear(); self.repeat_count = 0; self.state = AnimState::PassInProgress { @@ -913,9 +956,7 @@ impl Animation { if self.duration == Duration::ZERO { return 0.; } - let mut elapsed = self.elapsed().unwrap_or(Duration::ZERO); - // don't account for delay when reversing if !self.reverse_once.is_rev() && elapsed < self.delay { // The animation hasn't started yet @@ -924,16 +965,7 @@ impl Animation { if !self.reverse_once.is_rev() { elapsed -= self.delay; } - - let mut percent = elapsed.as_secs_f64() / self.duration.as_secs_f64(); - - if self.auto_reverse { - // If the animation should auto-reverse, adjust the percent accordingly - if percent > 0.5 { - percent = 1.0 - percent; - } - percent *= 2.0; // Normalize to [0.0, 1.0] range after reversal adjustment - } + let percent = elapsed.as_secs_f64() / self.duration.as_secs_f64(); if self.reverse_once.is_rev() { 1. - percent @@ -942,6 +974,10 @@ impl Animation { } } + fn is_reversing(&self) -> bool { + self.reverse_once.is_rev() + } + /// Get the lower and upper keyframe ids from the cache for a prop and then resolve those id's into a pair of `KeyFrameProp`s that contain the prop value and easing function pub(crate) fn get_current_kf_props( &self, @@ -1019,7 +1055,7 @@ impl Animation { } }; - if self.reverse_once.is_rev() { + if self.is_reversing() { Some((upper, lower)) } else { Some((lower, upper)) @@ -1081,24 +1117,24 @@ impl Animation { if self.props_in_ext_progress.contains_key(prop) { continue; } - let Some((lower, upper)) = + let Some((prev, target)) = self.get_current_kf_props(*prop, frame_target, computed_style) else { continue; }; - let local_percent = self.get_local_percent(lower.id, upper.id); - let easing = upper.easing.clone(); + let local_percent = self.get_local_percent(prev.id, target.id); + let easing = target.easing.clone(); // TODO: Find a better way to find when an animation should enter ext mode rather than just starting to check after 97%. // this could miss getting a prop into ext mode if (local_percent > 0.97) && !easing.finished(local_percent) { self.props_in_ext_progress - .insert(*prop, (lower.clone(), upper.clone())); + .insert(*prop, (prev.clone(), target.clone())); } else { self.props_in_ext_progress.remove(prop); } let eased_time = easing.eval(local_percent); if let Some(interpolated) = - (prop.info().interpolate)(&*lower.val.clone(), &*upper.val.clone(), eased_time) + (prop.info().interpolate)(&*prev.val.clone(), &*target.val.clone(), eased_time) { self.folded_style.map.insert(prop.key, interpolated); } @@ -1115,21 +1151,21 @@ impl Animation { } /// For a given pair of frame ids, find where the full animation progress is within the subrange of the frame id pair. - pub(crate) fn get_local_percent(&self, low_frame: u16, high_frame: u16) -> f64 { - let (low_frame, high_frame) = if self.reverse_once.is_rev() { - (high_frame as f64, low_frame as f64) + pub(crate) fn get_local_percent(&self, prev_frame: u16, target_frame: u16) -> f64 { + // undo the frame change that get current key_frame props does so that low is actually lower + let (low_frame, high_frame) = if self.is_reversing() { + (target_frame as f64, prev_frame as f64) } else { - (low_frame as f64, high_frame as f64) + (prev_frame as f64, target_frame as f64) }; let total_num_frames = self.max_key_frame_num as f64; - let low_frame_percent = low_frame / total_num_frames; let high_frame_percent = high_frame / total_num_frames; let keyframe_range = (high_frame_percent.max(0.001) - low_frame_percent.max(0.001)).abs(); - let total_time_percent = self.total_time_percent(); let local = (total_time_percent - low_frame_percent) / keyframe_range; - if self.reverse_once.is_rev() { + + if self.is_reversing() { 1. - local } else { local @@ -1159,10 +1195,11 @@ impl Animation { /// returns true if the animation can advance, which either means the animation will transition states, or properties can be animated and updated pub const fn can_advance(&self) -> bool { match self.state_kind() { - AnimStateKind::PassFinished | AnimStateKind::PassInProgress | AnimStateKind::Idle => { - true - } - AnimStateKind::Paused | AnimStateKind::Stopped | AnimStateKind::Completed => false, + AnimStateKind::PassFinished + | AnimStateKind::PassInProgress + | AnimStateKind::Idle + | AnimStateKind::Completed => true, + AnimStateKind::Paused | AnimStateKind::Stopped => false, } } diff --git a/src/context.rs b/src/context.rs index 2d025462..c267f5d0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1322,8 +1322,7 @@ fn animations_on_remove(id: ViewId, scope: Scope) -> u16 { let mut request_style = false; for anim in animations { if anim.run_on_remove && !matches!(anim.repeat_mode, RepeatMode::LoopForever) { - anim.reverse_once.set(true); - anim.start_mut(); + anim.reverse_mut(); request_style = true; wait_for += 1; let trigger = anim.on_visual_complete; @@ -1354,7 +1353,6 @@ fn stop_reset_remove_animations(id: ViewId) { && anim.state_kind() == AnimStateKind::PassInProgress && !matches!(anim.repeat_mode, RepeatMode::LoopForever) { - anim.reverse_once.set(false); anim.start_mut(); request_style = true; } @@ -1377,7 +1375,6 @@ fn animations_on_create(id: ViewId) { let mut request_style = false; for anim in animations { if anim.run_on_create && !matches!(anim.repeat_mode, RepeatMode::LoopForever) { - anim.reverse_once.set(false); anim.start_mut(); request_style = true; } diff --git a/src/views/dyn_container.rs b/src/views/dyn_container.rs index 7273f7ae..5d0d9723 100644 --- a/src/views/dyn_container.rs +++ b/src/views/dyn_container.rs @@ -199,8 +199,7 @@ fn animations_recursive_on_remove(id: ViewId, child_id: ViewId, child_scope: Sco let mut request_style = false; for anim in animations { if anim.run_on_remove && !matches!(anim.repeat_mode, RepeatMode::LoopForever) { - anim.reverse_once.set(true); - anim.start_mut(); + anim.reverse_mut(); request_style = true; wait_for += 1; let trigger = anim.on_visual_complete;