/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! CSS transitions and animations. // NOTE(emilio): This code isn't really executed in Gecko, but we don't want to // compile it out so that people remember it exists. use crate::bezier::Bezier; use crate::context::SharedStyleContext; use crate::dom::{OpaqueNode, TElement, TNode}; use crate::font_metrics::FontMetricsProvider; use crate::properties::animated_properties::AnimatedProperty; use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection; use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; use crate::properties::LonghandIdSet; use crate::properties::{self, CascadeMode, ComputedValues, LonghandId}; use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; use crate::stylesheets::Origin; use crate::timer::Timer; use crate::values::computed::Time; use crate::values::computed::TimingFunction; use crate::values::generics::box_::AnimationIterationCount; use crate::values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction}; use crate::Atom; use servo_arc::Arc; use std::fmt; /// This structure represents a keyframes animation current iteration state. /// /// If the iteration count is infinite, there's no other state, otherwise we /// have to keep track the current iteration and the max iteration count. #[derive(Clone, Debug)] pub enum KeyframesIterationState { /// Infinite iterations, so no need to track a state. Infinite, /// Current and max iterations. Finite(f32, f32), } /// This structure represents wether an animation is actually running. /// /// An animation can be running, or paused at a given time. #[derive(Clone, Debug)] pub enum KeyframesRunningState { /// This animation is paused. The inner field is the percentage of progress /// when it was paused, from 0 to 1. Paused(f64), /// This animation is actually running. Running, } /// This structure represents the current keyframe animation state, i.e., the /// duration, the current and maximum iteration count, and the state (either /// playing or paused). // TODO: unify the use of f32/f64 in this file. #[derive(Clone)] pub struct KeyframesAnimationState { /// The time this animation started at. pub started_at: f64, /// The duration of this animation. pub duration: f64, /// The delay of the animation. pub delay: f64, /// The current iteration state for the animation. pub iteration_state: KeyframesIterationState, /// Werther this animation is paused. pub running_state: KeyframesRunningState, /// The declared animation direction of this animation. pub direction: AnimationDirection, /// The current animation direction. This can only be `normal` or `reverse`. pub current_direction: AnimationDirection, /// The original cascade style, needed to compute the generated keyframes of /// the animation. pub cascade_style: Arc, } impl KeyframesAnimationState { /// Performs a tick in the animation state, i.e., increments the counter of /// the current iteration count, updates times and then toggles the /// direction if appropriate. /// /// Returns true if the animation should keep running. pub fn tick(&mut self) -> bool { debug!("KeyframesAnimationState::tick"); self.started_at += self.duration + self.delay; match self.running_state { // If it's paused, don't update direction or iteration count. KeyframesRunningState::Paused(_) => return true, KeyframesRunningState::Running => {}, } if let KeyframesIterationState::Finite(ref mut current, ref max) = self.iteration_state { *current += 1.0; // NB: This prevent us from updating the direction, which might be // needed for the correct handling of animation-fill-mode. if *current >= *max { return false; } } // Update the next iteration direction if applicable. match self.direction { AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { self.current_direction = match self.current_direction { AnimationDirection::Normal => AnimationDirection::Reverse, AnimationDirection::Reverse => AnimationDirection::Normal, _ => unreachable!(), }; }, _ => {}, } true } /// Updates the appropiate state from other animation. /// /// This happens when an animation is re-submitted to layout, presumably /// because of an state change. /// /// There are some bits of state we can't just replace, over all taking in /// account times, so here's that logic. pub fn update_from_other(&mut self, other: &Self, timer: &Timer) { use self::KeyframesRunningState::*; debug!( "KeyframesAnimationState::update_from_other({:?}, {:?})", self, other ); // NB: We shall not touch the started_at field, since we don't want to // restart the animation. let old_started_at = self.started_at; let old_duration = self.duration; let old_direction = self.current_direction; let old_running_state = self.running_state.clone(); let old_iteration_state = self.iteration_state.clone(); *self = other.clone(); let mut new_started_at = old_started_at; // If we're unpausing the animation, fake the start time so we seem to // restore it. // // If the animation keeps paused, keep the old value. // // If we're pausing the animation, compute the progress value. match (&mut self.running_state, old_running_state) { (&mut Running, Paused(progress)) => { new_started_at = timer.seconds() - (self.duration * progress) }, (&mut Paused(ref mut new), Paused(old)) => *new = old, (&mut Paused(ref mut progress), Running) => { *progress = (timer.seconds() - old_started_at) / old_duration }, _ => {}, } // Don't update the iteration count, just the iteration limit. // TODO: see how changing the limit affects rendering in other browsers. // We might need to keep the iteration count even when it's infinite. match (&mut self.iteration_state, old_iteration_state) { ( &mut KeyframesIterationState::Finite(ref mut iters, _), KeyframesIterationState::Finite(old_iters, _), ) => *iters = old_iters, _ => {}, } self.current_direction = old_direction; self.started_at = new_started_at; } /// Calculate the active-duration of this animation according to /// https://drafts.csswg.org/css-animations/#active-duration. active-duration /// is not really meaningful for infinite animations so we just return 0 /// here in that case. pub fn active_duration(&self) -> f64 { match self.iteration_state { KeyframesIterationState::Finite(_, max) => self.duration * (max as f64), KeyframesIterationState::Infinite => 0., } } } impl fmt::Debug for KeyframesAnimationState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("KeyframesAnimationState") .field("started_at", &self.started_at) .field("duration", &self.duration) .field("delay", &self.delay) .field("iteration_state", &self.iteration_state) .field("running_state", &self.running_state) .field("direction", &self.direction) .field("current_direction", &self.current_direction) .field("cascade_style", &()) .finish() } } /// State relating to an animation. #[derive(Clone, Debug)] pub enum Animation { /// A transition is just a single frame triggered at a time, with a reflow. /// /// the f64 field is the start time as returned by `time::precise_time_s()`. Transition(OpaqueNode, f64, PropertyAnimation), /// A keyframes animation is identified by a name, and can have a /// node-dependent state (i.e. iteration count, etc.). /// /// TODO(emilio): The animation object could be refcounted. Keyframes( OpaqueNode, KeyframesAnimation, Atom, KeyframesAnimationState, ), } impl Animation { /// The opaque node that owns the animation. #[inline] pub fn node(&self) -> &OpaqueNode { match *self { Animation::Transition(ref node, _, _) => node, Animation::Keyframes(ref node, _, _, _) => node, } } /// Whether this animation is a transition. #[inline] pub fn is_transition(&self) -> bool { match *self { Animation::Transition(..) => true, Animation::Keyframes(..) => false, } } /// Whether this animation has the same end value as another one. #[inline] fn is_transition_with_same_end_value(&self, other_animation: &PropertyAnimation) -> bool { match *self { Animation::Transition(_, _, ref animation) => { animation.has_the_same_end_value_as(other_animation) }, Animation::Keyframes(..) => false, } } /// Whether or not this animation is cancelled by changes from a new style. fn is_animation_cancelled_in_new_style(&self, new_style: &Arc) -> bool { let name = match *self { Animation::Transition(..) => return false, Animation::Keyframes(_, _, ref name, _) => name, }; let index = new_style .get_box() .animation_name_iter() .position(|animation_name| Some(name) == animation_name.as_atom()); let index = match index { Some(index) => index, None => return true, }; new_style.get_box().animation_duration_mod(index).seconds() == 0. } } /// Represents an animation for a given property. #[derive(Clone, Debug)] pub struct PropertyAnimation { /// An `AnimatedProperty` that this `PropertyAnimation` corresponds to. property: AnimatedProperty, /// The timing function of this `PropertyAnimation`. timing_function: TimingFunction, /// The duration of this `PropertyAnimation` in seconds. pub duration: f64, } impl PropertyAnimation { /// Returns the given property longhand id. pub fn property_id(&self) -> LonghandId { self.property.id() } /// Returns the given property name. pub fn property_name(&self) -> &'static str { self.property.name() } fn from_longhand( longhand: LonghandId, timing_function: TimingFunction, duration: Time, old_style: &ComputedValues, new_style: &ComputedValues, ) -> Option { let animated_property = AnimatedProperty::from_longhand(longhand, old_style, new_style)?; let property_animation = PropertyAnimation { property: animated_property, timing_function, duration: duration.seconds() as f64, }; if property_animation.does_animate() { Some(property_animation) } else { None } } /// Update the given animation at a given point of progress. pub fn update(&self, style: &mut ComputedValues, time: f64) { let epsilon = 1. / (200. * self.duration); let progress = match self.timing_function { GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { Bezier::new(x1, y1, x2, y2).solve(time, epsilon) }, GenericTimingFunction::Steps(steps, pos) => { let mut current_step = (time * (steps as f64)).floor() as i32; if pos == StepPosition::Start || pos == StepPosition::JumpStart || pos == StepPosition::JumpBoth { current_step = current_step + 1; } // FIXME: We should update current_step according to the "before flag". // In order to get the before flag, we have to know the current animation phase // and whether the iteration is reversed. For now, we skip this calculation. // (i.e. Treat before_flag is unset,) // https://drafts.csswg.org/css-easing/#step-timing-function-algo if time >= 0.0 && current_step < 0 { current_step = 0; } let jumps = match pos { StepPosition::JumpBoth => steps + 1, StepPosition::JumpNone => steps - 1, StepPosition::JumpStart | StepPosition::JumpEnd | StepPosition::Start | StepPosition::End => steps, }; if time <= 1.0 && current_step > jumps { current_step = jumps; } (current_step as f64) / (jumps as f64) }, GenericTimingFunction::Keyword(keyword) => { let (x1, x2, y1, y2) = keyword.to_bezier(); Bezier::new(x1, x2, y1, y2).solve(time, epsilon) }, }; self.property.update(style, progress); } #[inline] fn does_animate(&self) -> bool { self.property.does_animate() && self.duration != 0.0 } /// Whether this animation has the same end value as another one. #[inline] pub fn has_the_same_end_value_as(&self, other: &Self) -> bool { self.property.has_the_same_end_value_as(&other.property) } } /// Holds the animation state for a particular element. #[derive(Default)] pub struct ElementAnimationState { /// The animations running for this element. pub running_animations: Vec, /// The animations that have finished for this element, but not canceled. These are cleared /// upon triggering a DOM event for each finished animation. pub finished_animations: Vec, /// The animations that have been cancelled for this element. These are cleared /// upon triggering a DOM event for each cancelled animation. pub cancelled_animations: Vec, /// New animations created for this element. pub new_animations: Vec, } impl ElementAnimationState { /// Cancel all animations in this `ElementAnimationState`. This is typically called /// when the element has been removed from the DOM. pub fn cancel_all_animations(&mut self) { self.cancelled_animations.extend( self.finished_animations .drain(..) .chain(self.running_animations.drain(..)) .chain(self.new_animations.drain(..)), ); } pub(crate) fn cancel_transitions_with_nontransitioning_properties( &mut self, properties_that_transition: &LonghandIdSet, ) { if self.running_animations.is_empty() { return; } // TODO(mrobinson): We should make this more efficient perhaps by using // a linked-list or by using something like `partition`. let animation_count = self.running_animations.len(); let mut previously_running_animations = std::mem::replace( &mut self.running_animations, Vec::with_capacity(animation_count), ); for running_animation in previously_running_animations.drain(..) { if let Animation::Transition(_, _, ref property_animation) = running_animation { if !properties_that_transition.contains(property_animation.property_id()) { self.cancelled_animations.push(running_animation); continue; } } self.running_animations.push(running_animation); } } fn has_transition_with_same_end_value(&self, property_animation: &PropertyAnimation) -> bool { if self .running_animations .iter() .any(|animation| animation.is_transition_with_same_end_value(&property_animation)) { debug!( "Running transition found with the same end value for {:?}", property_animation, ); return true; } if self .finished_animations .iter() .any(|animation| animation.is_transition_with_same_end_value(&property_animation)) { debug!( "Expired transition found with the same end value for {:?}", property_animation, ); return true; } false } pub(crate) fn apply_completed_animations(&mut self, style: &mut Arc) { for animation in self.finished_animations.iter() { // TODO: Make this a bit more general and add support animation-fill-mode. // Without support for that property though, animations that have ended should // not affect property values. This is why we only process transitions here. if let Animation::Transition(_, _, property_animation) = animation { property_animation.update(Arc::make_mut(style), 1.0); } } } pub(crate) fn apply_running_animations( &mut self, context: &SharedStyleContext, style: &mut Arc, font_metrics: &dyn crate::font_metrics::FontMetricsProvider, ) where E: TElement, { // Return early so that we don't unnecessarily clone the style when making it mutable. if self.running_animations.is_empty() { return; } let style = Arc::make_mut(style); for animation in self.running_animations.iter_mut() { update_style_for_animation::(context, animation, style, font_metrics); } } pub(crate) fn apply_new_animations( &mut self, context: &SharedStyleContext, style: &mut Arc, font_metrics: &dyn crate::font_metrics::FontMetricsProvider, ) where E: TElement, { // Return early so that we don't unnecessarily clone the style when making it mutable. if self.new_animations.is_empty() { return; } let style = Arc::make_mut(style); for animation in self.new_animations.iter_mut() { update_style_for_animation::(context, animation, style, font_metrics); } } /// Whether this `ElementAnimationState` is empty, which means it doesn't /// hold any animations in any state. pub fn is_empty(&self) -> bool { self.running_animations.is_empty() && self.finished_animations.is_empty() && self.cancelled_animations.is_empty() && self.new_animations.is_empty() } fn add_new_animation(&mut self, animation: Animation) { self.new_animations.push(animation); } fn add_or_update_new_animation(&mut self, timer: &Timer, new_animation: Animation) { // If the animation was already present in the list for the node, // just update its state. if let Animation::Keyframes(_, _, ref new_name, ref new_state) = new_animation { for existing_animation in self.running_animations.iter_mut() { match existing_animation { Animation::Keyframes(_, _, ref name, ref mut state) if *name == *new_name => { state.update_from_other(&new_state, timer); return; }, _ => {}, } } } // Otherwise just add the new running animation. self.add_new_animation(new_animation); } /// Update our animations given a new style, canceling or starting new animations /// when appropriate. pub fn update_animations_for_new_style( &mut self, element: E, context: &SharedStyleContext, new_style: &Arc, ) where E: TElement, { // Cancel any animations that no longer exist in the style. // TODO(mrobinson): We should make this more efficient perhaps by using // a linked-list or by using something like `partition`. if !self.running_animations.is_empty() { let animation_count = self.running_animations.len(); let mut previously_running_animations = std::mem::replace( &mut self.running_animations, Vec::with_capacity(animation_count), ); for animation in previously_running_animations.drain(..) { if animation.is_animation_cancelled_in_new_style(new_style) { self.cancelled_animations.push(animation); } else { self.running_animations.push(animation); } } } maybe_start_animations(element, &context, &new_style, self); } /// Update our transitions given a new style, canceling or starting new animations /// when appropriate. pub fn update_transitions_for_new_style( &mut self, context: &SharedStyleContext, opaque_node: OpaqueNode, before_change_style: Option<&Arc>, new_style: &Arc, ) { // If this is the first style, we don't trigger any transitions and we assume // there were no previously triggered transitions. let before_change_style = match before_change_style { Some(before_change_style) => before_change_style, None => return, }; let transitioning_properties = start_transitions_if_applicable( context, opaque_node, before_change_style, new_style, self, ); self.cancel_transitions_with_nontransitioning_properties(&transitioning_properties); } } /// Kick off any new transitions for this node and return all of the properties that are /// transitioning. This is at the end of calculating style for a single node. pub fn start_transitions_if_applicable( context: &SharedStyleContext, opaque_node: OpaqueNode, old_style: &ComputedValues, new_style: &Arc, animation_state: &mut ElementAnimationState, ) -> LonghandIdSet { // If the style of this element is display:none, then we don't start any transitions // and we cancel any currently running transitions by returning an empty LonghandIdSet. if new_style.get_box().clone_display().is_none() { return LonghandIdSet::new(); } let mut properties_that_transition = LonghandIdSet::new(); for transition in new_style.transition_properties() { let physical_property = transition.longhand_id.to_physical(new_style.writing_mode); if properties_that_transition.contains(physical_property) { continue; } else { properties_that_transition.insert(physical_property); } let property_animation = match PropertyAnimation::from_longhand( transition.longhand_id, new_style .get_box() .transition_timing_function_mod(transition.index), new_style .get_box() .transition_duration_mod(transition.index), old_style, new_style, ) { Some(property_animation) => property_animation, None => continue, }; // 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. // [1]: https://drafts.csswg.org/css-transitions/#starting if animation_state.has_transition_with_same_end_value(&property_animation) { continue; } // Kick off the animation. debug!("Kicking off transition of {:?}", property_animation); let box_style = new_style.get_box(); let now = context.timer.seconds(); let start_time = now + (box_style.transition_delay_mod(transition.index).seconds() as f64); animation_state.add_new_animation(Animation::Transition( opaque_node, start_time, property_animation, )); } properties_that_transition } fn compute_style_for_animation_step( context: &SharedStyleContext, step: &KeyframesStep, previous_style: &ComputedValues, style_from_cascade: &Arc, font_metrics_provider: &dyn FontMetricsProvider, ) -> Arc where E: TElement, { match step.value { KeyframesStepValue::ComputedValues => style_from_cascade.clone(), KeyframesStepValue::Declarations { block: ref declarations, } => { let guard = declarations.read_with(context.guards.author); let iter = || { // It's possible to have !important properties in keyframes // so we have to filter them out. // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824 // Also we filter our non-animatable properties. guard .normal_declaration_iter() .filter(|declaration| declaration.is_animatable()) .map(|decl| (decl, Origin::Author)) }; // This currently ignores visited styles, which seems acceptable, // as existing browsers don't appear to animate visited styles. let computed = properties::apply_declarations::( context.stylist.device(), /* pseudo = */ None, previous_style.rules(), &context.guards, iter, Some(previous_style), Some(previous_style), Some(previous_style), font_metrics_provider, CascadeMode::Unvisited { visited_rules: None, }, context.quirks_mode(), /* rule_cache = */ None, &mut Default::default(), /* element = */ None, ); computed }, } } /// Triggers animations for a given node looking at the animation property /// values. pub fn maybe_start_animations( element: E, context: &SharedStyleContext, new_style: &Arc, animation_state: &mut ElementAnimationState, ) where E: TElement, { let box_style = new_style.get_box(); for (i, name) in box_style.animation_name_iter().enumerate() { let name = match name.as_atom() { Some(atom) => atom, None => continue, }; debug!("maybe_start_animations: name={}", name); let duration = box_style.animation_duration_mod(i).seconds(); if duration == 0. { continue; } let anim = match context.stylist.get_animation(name, element) { Some(animation) => animation, None => continue, }; debug!("maybe_start_animations: animation {} found", name); // If this animation doesn't have any keyframe, we can just continue // without submitting it to the compositor, since both the first and // the second keyframes would be synthetised from the computed // values. if anim.steps.is_empty() { continue; } let delay = box_style.animation_delay_mod(i).seconds(); let now = context.timer.seconds(); let animation_start = now + delay as f64; let iteration_state = match box_style.animation_iteration_count_mod(i) { AnimationIterationCount::Infinite => KeyframesIterationState::Infinite, AnimationIterationCount::Number(n) => KeyframesIterationState::Finite(0.0, n), }; let animation_direction = box_style.animation_direction_mod(i); let initial_direction = match animation_direction { AnimationDirection::Normal | AnimationDirection::Alternate => { AnimationDirection::Normal }, AnimationDirection::Reverse | AnimationDirection::AlternateReverse => { AnimationDirection::Reverse }, }; let running_state = match box_style.animation_play_state_mod(i) { AnimationPlayState::Paused => KeyframesRunningState::Paused(0.), AnimationPlayState::Running => KeyframesRunningState::Running, }; animation_state.add_or_update_new_animation( &context.timer, Animation::Keyframes( element.as_node().opaque(), anim.clone(), name.clone(), KeyframesAnimationState { started_at: animation_start, duration: duration as f64, delay: delay as f64, iteration_state, running_state, direction: animation_direction, current_direction: initial_direction, cascade_style: new_style.clone(), }, ), ); } } /// Updates a single animation and associated style based on the current time. pub fn update_style_for_animation( context: &SharedStyleContext, animation: &Animation, style: &mut ComputedValues, font_metrics_provider: &dyn FontMetricsProvider, ) where E: TElement, { debug!("update_style_for_animation: {:?}", animation); match *animation { Animation::Transition(_, start_time, ref property_animation) => { let now = context.timer.seconds(); let progress = (now - start_time) / (property_animation.duration); let progress = progress.min(1.0); if progress >= 0.0 { property_animation.update(style, progress); } }, Animation::Keyframes(_, ref animation, ref name, ref state) => { let duration = state.duration; let started_at = state.started_at; let now = match state.running_state { KeyframesRunningState::Running => context.timer.seconds(), KeyframesRunningState::Paused(progress) => started_at + duration * progress, }; debug_assert!(!animation.steps.is_empty()); let mut total_progress = (now - started_at) / duration; if total_progress < 0. { warn!("Negative progress found for animation {:?}", name); return; } if total_progress > 1. { total_progress = 1.; } // Get the target and the last keyframe position. let last_keyframe_position; let target_keyframe_position; match state.current_direction { AnimationDirection::Normal => { target_keyframe_position = animation .steps .iter() .position(|step| total_progress as f32 <= step.start_percentage.0); last_keyframe_position = target_keyframe_position .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) .unwrap_or(0); }, AnimationDirection::Reverse => { target_keyframe_position = animation .steps .iter() .rev() .position(|step| total_progress as f32 <= 1. - step.start_percentage.0) .map(|pos| animation.steps.len() - pos - 1); last_keyframe_position = target_keyframe_position .and_then(|pos| { if pos != animation.steps.len() - 1 { Some(pos + 1) } else { None } }) .unwrap_or(animation.steps.len() - 1); }, _ => unreachable!(), } debug!( "update_style_for_animation: keyframe from {:?} to {:?}", last_keyframe_position, target_keyframe_position ); let target_keyframe = match target_keyframe_position { Some(target) => &animation.steps[target], None => return, }; let last_keyframe = &animation.steps[last_keyframe_position]; let relative_timespan = (target_keyframe.start_percentage.0 - last_keyframe.start_percentage.0).abs(); let relative_duration = relative_timespan as f64 * duration; let last_keyframe_ended_at = match state.current_direction { AnimationDirection::Normal => { state.started_at + (duration * last_keyframe.start_percentage.0 as f64) }, AnimationDirection::Reverse => { state.started_at + (duration * (1. - last_keyframe.start_percentage.0 as f64)) }, _ => unreachable!(), }; let relative_progress = (now - last_keyframe_ended_at) / relative_duration; // TODO: How could we optimise it? Is it such a big deal? let from_style = compute_style_for_animation_step::( context, last_keyframe, style, &state.cascade_style, font_metrics_provider, ); // NB: The spec says that the timing function can be overwritten // from the keyframe style. let timing_function = if last_keyframe.declared_timing_function { // NB: animation_timing_function can never be empty, always has // at least the default value (`ease`). from_style.get_box().animation_timing_function_at(0) } else { // TODO(mrobinson): It isn't optimal to have to walk this list every // time. Perhaps this should be stored in the animation. let index = match style .get_box() .animation_name_iter() .position(|animation_name| Some(name) == animation_name.as_atom()) { Some(index) => index, None => return warn!("Tried to update a style with a cancelled animation."), }; style.get_box().animation_timing_function_mod(index) }; let target_style = compute_style_for_animation_step::( context, target_keyframe, &from_style, &state.cascade_style, font_metrics_provider, ); let mut new_style = (*style).clone(); for property in animation.properties_changed.iter() { debug!( "update_style_for_animation: scanning prop {:?} for animation \"{}\"", property, name ); let animation = PropertyAnimation::from_longhand( property, timing_function, Time::from_seconds(relative_duration as f32), &from_style, &target_style, ); match animation { Some(property_animation) => { debug!( "update_style_for_animation: got property animation for prop {:?}", property ); debug!("update_style_for_animation: {:?}", property_animation); property_animation.update(&mut new_style, relative_progress); }, None => { debug!( "update_style_for_animation: property animation {:?} not animating", property ); }, } } debug!( "update_style_for_animation: got style change in animation \"{}\"", name ); *style = new_style; }, } }