diff --git a/style/animation.rs b/style/animation.rs index c90e2327c5..9520810a98 100644 --- a/style/animation.rs +++ b/style/animation.rs @@ -25,7 +25,7 @@ use crate::style_resolver::StyleResolverForElement; use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; use crate::stylesheets::layer_rule::LayerOrder; use crate::values::animated::{Animate, Procedure}; -use crate::values::computed::{Time, TimingFunction}; +use crate::values::computed::TimingFunction; use crate::values::generics::easing::BeforeFlag; use crate::values::specified::TransitionBehavior; use crate::Atom; @@ -57,31 +57,6 @@ impl PropertyAnimation { self.from.id() } - fn from_property_declaration( - property_declaration: &PropertyDeclarationId, - timing_function: TimingFunction, - duration: Time, - old_style: &ComputedValues, - new_style: &ComputedValues, - ) -> Option { - // FIXME(emilio): Handle the case where old_style and new_style's writing mode differ. - let property_declaration = property_declaration.to_physical(new_style.writing_mode); - let from = AnimationValue::from_computed_values(property_declaration, old_style)?; - let to = AnimationValue::from_computed_values(property_declaration, new_style)?; - let duration = duration.seconds() as f64; - - if from == to || duration == 0.0 { - return None; - } - - Some(PropertyAnimation { - from, - to, - timing_function, - duration, - }) - } - /// The output of the timing function given the progress ration of this animation. fn timing_function_output(&self, progress: f64) -> f64 { let epsilon = 1. / (200. * self.duration); @@ -135,6 +110,11 @@ impl AnimationState { } } +enum IgnoreTransitions { + Canceled, + CanceledAndFinished, +} + /// This structure represents a keyframes animation current iteration state. /// /// If the iteration count is infinite, there's no other state, otherwise we @@ -762,6 +742,31 @@ pub struct Transition { } impl Transition { + fn new( + start_time: f64, + delay: f64, + duration: f64, + from: AnimationValue, + to: AnimationValue, + timing_function: &TimingFunction, + ) -> Self { + let property_animation = PropertyAnimation { + from: from.clone(), + to, + timing_function: timing_function.clone(), + duration, + }; + Self { + start_time, + delay, + property_animation, + state: AnimationState::Pending, + is_new: true, + reversing_adjusted_start_value: from, + reversing_shortening_factor: 1.0, + } + } + fn update_for_possibly_reversed_transition( &mut self, replaced_transition: &Transition, @@ -894,7 +899,7 @@ impl ElementAnimationSet { } } - if let Some(map) = self.get_value_map_for_active_transitions(now) { + if let Some(map) = self.get_value_map_for_transitions(now, IgnoreTransitions::Canceled) { for value in map.values() { value.set_in_style_for_servo(mutable_style); } @@ -1010,9 +1015,22 @@ impl ElementAnimationSet { self, ); - // Cancel any non-finished transitions that have properties which no longer transition. + // Cancel any non-finished transitions that have properties which no + // longer transition. + // + // Step 3 in https://drafts.csswg.org/css-transitions/#starting: + // > If the element has a running transition or completed transition for + // > the property, and there is not a matching transition-property value, + // > then implementations must cancel the running transition or remove the + // > completed transition from the set of completed transitions. + // + // TODO: This is happening here as opposed to in + // `start_transition_if_applicable` as an optimization, but maybe this + // code should be reworked to be more like the specification. for transition in self.transitions.iter_mut() { - if transition.state == AnimationState::Finished { + if transition.state == AnimationState::Finished + || transition.state == AnimationState::Canceled + { continue; } if transitioning_properties.contains(transition.property_animation.property_id()) { @@ -1032,86 +1050,192 @@ impl ElementAnimationSet { new_style: &Arc, ) { let style = new_style.get_ui(); - let allow_discrete = style.transition_behavior_mod(index) == TransitionBehavior::AllowDiscrete; + let allow_discrete = + style.transition_behavior_mod(index) == TransitionBehavior::AllowDiscrete; - if !property_declaration_id.is_animatable() - || (!allow_discrete && property_declaration_id.is_discrete_animatable()) - { + // FIXME(emilio): Handle the case where old_style and new_style's writing mode differ. + let Some(from) = AnimationValue::from_computed_values(*property_declaration_id, old_style) + else { return; - } + }; + let Some(to) = AnimationValue::from_computed_values(*property_declaration_id, new_style) + else { + return; + }; let timing_function = style.transition_timing_function_mod(index); - let duration = style.transition_duration_mod(index); + let duration = style.transition_duration_mod(index).seconds() as f64; let delay = style.transition_delay_mod(index).seconds() as f64; let now = context.current_time_for_animations; + let transitionable = property_declaration_id.is_animatable() + && (allow_discrete || !property_declaration_id.is_discrete_animatable()) + && (allow_discrete || from.interpolable_with(&to)); - // Only start a new transition if the style actually changes between - // the old style and the new style. - let property_animation = match PropertyAnimation::from_property_declaration( - property_declaration_id, - timing_function, - duration, - old_style, - new_style, - ) { - Some(property_animation) => property_animation, - None => return, - }; + let mut existing_transition = self.transitions.iter_mut().find(|transition| { + transition.property_animation.property_id() == *property_declaration_id + }); - // A property may have an animation type different than 'discrete', but still - // not be able to interpolate some values. In that case we would fall back to - // discrete interpolation, so we need to abort if `transition-behavior` doesn't - // allow discrete transitions. - if !allow_discrete && !property_animation.from.interpolable_with(&property_animation.to) { + // Step 1: + // > If all of the following are true: + // > - the element does not have a running transition for the property, + // > - the before-change style is different from the after-change style + // > for that property, and the values for the property are + // > transitionable, + // > - the element does not have a completed transition for the property + // > or the end value of the completed transition is different from the + // > after-change style for the property, + // > - there is a matching transition-property value, and + // > - the combined duration is greater than 0s, + // + // This function is only run if there is a matching transition-property + // value, so that check is skipped here. + let has_running_transition = existing_transition.as_ref().is_some_and(|transition| { + transition.state != AnimationState::Finished + && transition.state != AnimationState::Canceled + }); + let no_completed_transition_or_end_values_differ = existing_transition.as_ref().is_none_or(|transition| { + transition.state != AnimationState::Finished || transition.property_animation.to != to + }); + if !has_running_transition && + from != to && transitionable && + no_completed_transition_or_end_values_differ && + (duration + delay > 0.0) { + // > then implementations must remove the completed transition (if + // > present) from the set of completed transitions and start a + // > transition whose: + // > + // > - start time is the time of the style change event plus the matching transition delay, + // > - end time is the start time plus the matching transition duration, + // > - start value is the value of the transitioning property in the before-change style, + // > - end value is the value of the transitioning property in the after-change style, + // > - reversing-adjusted start value is the same as the start value, and + // > - reversing shortening factor is 1. + self.transitions.push(Transition::new( + now + delay, /* start_time */ + delay, + duration, + from, + to, + &timing_function, + )); + self.dirty = true; return; } - // Per [1], don't trigger a new transition if the end state for that - // transition is the same as that of a transition that's running or - // completed. We don't take into account any canceled animations. - // [1]: https://drafts.csswg.org/css-transitions/#starting - if self - .transitions - .iter() - .filter(|transition| transition.state != AnimationState::Canceled) - .any(|transition| transition.property_animation.to == property_animation.to) - { + // > Step 2: Otherwise, if the element has a completed transition for the + // > property and the end value of the completed transition is different + // > from the after-change style for the property, then implementations + // > must remove the completed transition from the set of completed + // > transitions. + // + // All completed transitions will be cleared from the `AnimationSet` in + // `process_animations_for_style in `matching.rs`. + + // > Step 3: If the element has a running transition or completed + // > transition for the property, and there is not a matching + // > transition-property value, then implementations must cancel the + // > running transition or remove the completed transition from the set + // > of completed transitions. + // + // - All completed transitions will be cleared cleared from the `AnimationSet` in + // `process_animations_for_style in `matching.rs`. + // - Transitions for properties that don't have a matching transition-property + // value will be canceled in `Self::update_transitions_for_new_style`. In addition, + // this method is only called for properties that do ahave a matching + // transition-property value. + + let Some(existing_transition) = existing_transition.as_mut() else { return; - } - - // We are going to start a new transition, but we might have to update - // it if we are replacing a reversed transition. - let reversing_adjusted_start_value = property_animation.from.clone(); - let mut new_transition = Transition { - start_time: now + delay, - delay, - property_animation, - state: AnimationState::Pending, - is_new: true, - reversing_adjusted_start_value, - reversing_shortening_factor: 1.0, }; - if let Some(old_transition) = self - .transitions - .iter_mut() - .filter(|transition| transition.state == AnimationState::Running) - .find(|transition| { - transition.property_animation.property_id() == *property_declaration_id - }) - { - // We always cancel any running transitions for the same property. - old_transition.state = AnimationState::Canceled; - new_transition.update_for_possibly_reversed_transition(old_transition, delay, now); - } + // > Step 4: If the element has a running transition for the property, + // > there is a matching transition-property value, and the end value of + // > the running transition is not equal to the value of the property in + // > the after-change style, then: + if has_running_transition && existing_transition.property_animation.to != to { + // > Step 4.1: If the current value of the property in the running transition is + // > equal to the value of the property in the after-change style, or + // > if these two values are not transitionable, then implementations + // > must cancel the running transition. + let current_value = existing_transition.calculate_value(now); + let transitionable_from_current_value = + transitionable && (allow_discrete || current_value.interpolable_with(&to)); + if current_value == to || !transitionable_from_current_value { + existing_transition.state = AnimationState::Canceled; + self.dirty = true; + return; + } - self.transitions.push(new_transition); - self.dirty = true; + // > Step 4.2: Otherwise, if the combined duration is less than or + // > equal to 0s, or if the current value of the property in the + // > running transition is not transitionable with the value of the + // > property in the after-change style, then implementations must + // > cancel the running transition. + if duration + delay <= 0.0 { + existing_transition.state = AnimationState::Canceled; + self.dirty = true; + return; + } + + // > Step 4.3: Otherwise, if the reversing-adjusted start value of the + // > running transition is the same as the value of the property in + // > the after-change style (see the section on reversing of + // > transitions for why these case exists), implementations must + // > cancel the running transition and start a new transition whose: + if existing_transition.reversing_adjusted_start_value == to { + existing_transition.state = AnimationState::Canceled; + + let mut transition = Transition::new( + now + delay, /* start_time */ + delay, + duration, + from, + to, + &timing_function, + ); + + // This function takes care of applying all of the modifications to the transition + // after "whose:" above. + transition.update_for_possibly_reversed_transition( + &existing_transition, + delay, + now, + ); + + self.transitions.push(transition); + self.dirty = true; + return; + } + + // > Step 4.4: Otherwise, implementations must cancel the running + // > transition and start a new transition whose: + // > - start time is the time of the style change event plus the matching transition delay, + // > - end time is the start time plus the matching transition duration, + // > - start value is the current value of the property in the running transition, + // > - end value is the value of the property in the after-change style, + // > - reversing-adjusted start value is the same as the start value, and + // > - reversing shortening factor is 1. + existing_transition.state = AnimationState::Canceled; + self.transitions.push(Transition::new( + now + delay, /* start_time */ + delay, + duration, + current_value, + to, + &timing_function, + )); + self.dirty = true; + } } /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s - /// active transitions at the given time value. - pub fn get_value_map_for_active_transitions(&self, now: f64) -> Option { + /// transitions, ignoring those specified by the `ignore_transitions` + /// argument. + fn get_value_map_for_transitions( + &self, + now: f64, + ignore_transitions: IgnoreTransitions, + ) -> Option { if !self.has_active_transition() { return None; } @@ -1119,9 +1243,21 @@ impl ElementAnimationSet { let mut map = AnimationValueMap::with_capacity_and_hasher(self.transitions.len(), Default::default()); for transition in &self.transitions { - if transition.state == AnimationState::Canceled { - continue; + match ignore_transitions { + IgnoreTransitions::Canceled => { + if transition.state == AnimationState::Canceled { + continue; + } + }, + IgnoreTransitions::CanceledAndFinished => { + if transition.state == AnimationState::Canceled + || transition.state == AnimationState::Finished + { + continue; + } + }, } + let value = transition.calculate_value(now); map.insert(value.id().to_owned(), value); } @@ -1235,7 +1371,9 @@ impl DocumentAnimationSet { self.sets .read() .get(key) - .and_then(|set| set.get_value_map_for_active_transitions(time)) + .and_then(|set| { + set.get_value_map_for_transitions(time, IgnoreTransitions::CanceledAndFinished) + }) .map(|map| { let block = PropertyDeclarationBlock::from_animation_value_map(&map); Arc::new(shared_lock.wrap(block)) @@ -1260,10 +1398,12 @@ impl DocumentAnimationSet { let block = PropertyDeclarationBlock::from_animation_value_map(&map); Arc::new(shared_lock.wrap(block)) }); - let transitions = set.get_value_map_for_active_transitions(time).map(|map| { - let block = PropertyDeclarationBlock::from_animation_value_map(&map); - Arc::new(shared_lock.wrap(block)) - }); + let transitions = set + .get_value_map_for_transitions(time, IgnoreTransitions::CanceledAndFinished) + .map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }); AnimationDeclarations { animations, transitions,