Cache animation computed values when animations change

Instead of recalculating the animation style every tick of an animation,
cache the computed values when animations change. In addition to being
more efficient, this will allow us to return animation rules as property
declarations because we don't need to consult the final style to produce
them.
This commit is contained in:
Martin Robinson 2020-05-18 16:32:41 +02:00
parent 7df4655b60
commit 83fa1b9eaa
9 changed files with 246 additions and 169 deletions

View file

@ -17,7 +17,7 @@ use crate::properties::longhands::animation_fill_mode::computed_value::single_va
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::keyframes_rule::{KeyframesStep, KeyframesStepValue};
use crate::stylesheets::Origin;
use crate::values::animated::{Animate, Procedure};
use crate::values::computed::Time;
@ -172,6 +172,103 @@ pub enum KeyframesIterationState {
Finite(f64, f64),
}
/// A single computed keyframe for a CSS Animation.
#[derive(Clone, MallocSizeOf)]
struct ComputedKeyframeStep {
step: KeyframesStep,
#[ignore_malloc_size_of = "ComputedValues"]
style: Arc<ComputedValues>,
timing_function: TimingFunction,
}
impl ComputedKeyframeStep {
fn generate_for_keyframes<E>(
element: E,
steps: &[KeyframesStep],
context: &SharedStyleContext,
base_style: &Arc<ComputedValues>,
font_metrics_provider: &dyn FontMetricsProvider,
default_timing_function: TimingFunction,
) -> Vec<Self>
where
E: TElement,
{
let mut previous_style = base_style.clone();
steps
.iter()
.cloned()
.map(|step| match step.value {
KeyframesStepValue::ComputedValues => ComputedKeyframeStep {
step,
style: base_style.clone(),
timing_function: default_timing_function,
},
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.
//
// TODO(mrobinson): We shouldn't be calling `apply_declarations`
// here because it doesn't really produce the correct values (for
// instance for keyframes that are missing animating properties).
// Instead we should do something like what Gecko does in
// Servo_StyleSet_GetKeyframesForName.
let computed_style = properties::apply_declarations::<E, _, _>(
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(),
Some(element),
);
// NB: The spec says that the timing function can be overwritten
// from the keyframe style. `animation_timing_function` can never
// be empty, always has at least the default value (`ease`).
let timing_function = if step.declared_timing_function {
computed_style.get_box().animation_timing_function_at(0)
} else {
default_timing_function
};
previous_style = computed_style.clone();
ComputedKeyframeStep {
step,
style: computed_style,
timing_function,
}
},
})
.collect()
}
}
/// A CSS Animation
#[derive(Clone, MallocSizeOf)]
pub struct Animation {
@ -181,8 +278,11 @@ pub struct Animation {
/// The name of this animation as defined by the style.
pub name: Atom,
/// The internal animation from the style system.
pub keyframes_animation: KeyframesAnimation,
/// The properties that change in this animation.
properties_changed: LonghandIdSet,
/// The computed style for each keyframe of this animation.
computed_steps: Vec<ComputedKeyframeStep>,
/// The time this animation started at, which is the current value of the animation
/// timeline when this animation was created plus any animation delay.
@ -377,14 +477,7 @@ impl Animation {
/// Update the given style to reflect the values specified by this `Animation`
/// at the time provided by the given `SharedStyleContext`.
fn update_style<E>(
&self,
context: &SharedStyleContext,
style: &mut Arc<ComputedValues>,
font_metrics_provider: &dyn FontMetricsProvider,
) where
E: TElement,
{
fn update_style(&self, context: &SharedStyleContext, style: &mut Arc<ComputedValues>) {
let duration = self.duration;
let started_at = self.started_at;
@ -396,7 +489,7 @@ impl Animation {
AnimationState::Canceled => return,
};
debug_assert!(!self.keyframes_animation.steps.is_empty());
debug_assert!(!self.computed_steps.is_empty());
let mut total_progress = (now - started_at) / duration;
if total_progress < 0. &&
@ -417,34 +510,34 @@ impl Animation {
// Get the indices of the previous (from) keyframe and the next (to) keyframe.
let next_keyframe_index;
let prev_keyframe_index;
let num_steps = self.computed_steps.len();
debug_assert!(num_steps > 0);
match self.current_direction {
AnimationDirection::Normal => {
next_keyframe_index = self
.keyframes_animation
.steps
.computed_steps
.iter()
.position(|step| total_progress as f32 <= step.start_percentage.0);
.position(|step| total_progress as f32 <= step.step.start_percentage.0);
prev_keyframe_index = next_keyframe_index
.and_then(|pos| if pos != 0 { Some(pos - 1) } else { None })
.unwrap_or(0);
},
AnimationDirection::Reverse => {
next_keyframe_index = self
.keyframes_animation
.steps
.computed_steps
.iter()
.rev()
.position(|step| total_progress as f32 <= 1. - step.start_percentage.0)
.map(|pos| self.keyframes_animation.steps.len() - pos - 1);
.position(|step| total_progress as f32 <= 1. - step.step.start_percentage.0)
.map(|pos| num_steps - pos - 1);
prev_keyframe_index = next_keyframe_index
.and_then(|pos| {
if pos != self.keyframes_animation.steps.len() - 1 {
if pos != num_steps - 1 {
Some(pos + 1)
} else {
None
}
})
.unwrap_or(self.keyframes_animation.steps.len() - 1)
.unwrap_or(num_steps - 1)
},
_ => unreachable!(),
}
@ -454,87 +547,48 @@ impl Animation {
prev_keyframe_index, next_keyframe_index
);
let prev_keyframe = &self.keyframes_animation.steps[prev_keyframe_index];
let prev_keyframe = &self.computed_steps[prev_keyframe_index];
let next_keyframe = match next_keyframe_index {
Some(target) => &self.keyframes_animation.steps[target],
Some(index) => &self.computed_steps[index],
None => return,
};
let update_with_single_keyframe_style = |style, computed_style: &Arc<ComputedValues>| {
let mutable_style = Arc::make_mut(style);
for property in self
.keyframes_animation
.properties_changed
.iter()
.filter_map(|longhand| {
AnimationValue::from_computed_values(longhand, &**computed_style)
})
{
for property in self.properties_changed.iter().filter_map(|longhand| {
AnimationValue::from_computed_values(longhand, &**computed_style)
}) {
property.set_in_style_for_servo(mutable_style);
}
};
// TODO: How could we optimise it? Is it such a big deal?
let prev_keyframe_style = compute_style_for_animation_step::<E>(
context,
prev_keyframe,
style,
&self.cascade_style,
font_metrics_provider,
);
let prev_keyframe_style = &prev_keyframe.style;
let next_keyframe_style = &next_keyframe.style;
if total_progress <= 0.0 {
update_with_single_keyframe_style(style, &prev_keyframe_style);
return;
}
let next_keyframe_style = compute_style_for_animation_step::<E>(
context,
next_keyframe,
&prev_keyframe_style,
&self.cascade_style,
font_metrics_provider,
);
if total_progress >= 1.0 {
update_with_single_keyframe_style(style, &next_keyframe_style);
return;
}
let relative_timespan =
(next_keyframe.start_percentage.0 - prev_keyframe.start_percentage.0).abs();
(next_keyframe.step.start_percentage.0 - prev_keyframe.step.start_percentage.0).abs();
let relative_duration = relative_timespan as f64 * duration;
let last_keyframe_ended_at = match self.current_direction {
AnimationDirection::Normal => {
self.started_at + (duration * prev_keyframe.start_percentage.0 as f64)
self.started_at + (duration * prev_keyframe.step.start_percentage.0 as f64)
},
AnimationDirection::Reverse => {
self.started_at + (duration * (1. - prev_keyframe.start_percentage.0 as f64))
self.started_at + (duration * (1. - prev_keyframe.step.start_percentage.0 as f64))
},
_ => unreachable!(),
};
let relative_progress = (now - last_keyframe_ended_at) / relative_duration;
// NB: The spec says that the timing function can be overwritten
// from the keyframe style.
let timing_function = if prev_keyframe.declared_timing_function {
// NB: animation_timing_function can never be empty, always has
// at least the default value (`ease`).
prev_keyframe_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(&self.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 mut new_style = (**style).clone();
let mut update_style_for_longhand = |longhand| {
let from = AnimationValue::from_computed_values(longhand, &prev_keyframe_style)?;
@ -542,14 +596,14 @@ impl Animation {
PropertyAnimation {
from,
to,
timing_function,
timing_function: prev_keyframe.timing_function,
duration: relative_duration as f64,
}
.update(&mut new_style, relative_progress);
None::<()>
};
for property in self.keyframes_animation.properties_changed.iter() {
for property in self.properties_changed.iter() {
update_style_for_longhand(property);
}
@ -724,16 +778,13 @@ impl ElementAnimationSet {
}
}
pub(crate) fn apply_active_animations<E>(
pub(crate) fn apply_active_animations(
&mut self,
context: &SharedStyleContext,
style: &mut Arc<ComputedValues>,
font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
) where
E: TElement,
{
) {
for animation in &self.animations {
animation.update_style::<E>(context, style, font_metrics);
animation.update_style(context, style);
}
for transition in &self.transitions {
@ -778,13 +829,18 @@ impl ElementAnimationSet {
.count()
}
fn has_active_transition_or_animation(&self) -> bool {
/// If this `ElementAnimationSet` has any any active animations.
pub fn has_active_animation(&self) -> bool {
self.animations
.iter()
.any(|animation| animation.state != AnimationState::Canceled) ||
self.transitions
.iter()
.any(|transition| transition.state != AnimationState::Canceled)
.any(|animation| animation.state != AnimationState::Canceled)
}
/// If this `ElementAnimationSet` has any any active transitions.
pub fn has_active_transition(&self) -> bool {
self.transitions
.iter()
.any(|transition| transition.state != AnimationState::Canceled)
}
/// Update our animations given a new style, canceling or starting new animations
@ -794,6 +850,7 @@ impl ElementAnimationSet {
element: E,
context: &SharedStyleContext,
new_style: &Arc<ComputedValues>,
font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
) where
E: TElement,
{
@ -803,21 +860,18 @@ impl ElementAnimationSet {
}
}
maybe_start_animations(element, &context, &new_style, self);
maybe_start_animations(element, &context, &new_style, self, font_metrics);
}
/// Update our transitions given a new style, canceling or starting new animations
/// when appropriate.
pub fn update_transitions_for_new_style<E>(
pub fn update_transitions_for_new_style(
&mut self,
context: &SharedStyleContext,
opaque_node: OpaqueNode,
old_style: Option<&Arc<ComputedValues>>,
after_change_style: &Arc<ComputedValues>,
font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
) where
E: TElement,
{
) {
// If this is the first style, we don't trigger any transitions and we assume
// there were no previously triggered transitions.
let mut before_change_style = match old_style {
@ -829,9 +883,9 @@ impl ElementAnimationSet {
// See https://drafts.csswg.org/css-transitions/#starting. We need to clone the
// style because this might still be a reference to the original `old_style` and
// we want to preserve that so that we can later properly calculate restyle damage.
if self.has_active_transition_or_animation() {
if self.has_active_transition() || self.has_active_animation() {
before_change_style = before_change_style.clone();
self.apply_active_animations::<E>(context, &mut before_change_style, font_metrics);
self.apply_active_animations(context, &mut before_change_style);
}
let transitioning_properties = start_transitions_if_applicable(
@ -961,55 +1015,6 @@ pub fn start_transitions_if_applicable(
properties_that_transition
}
fn compute_style_for_animation_step<E>(
context: &SharedStyleContext,
step: &KeyframesStep,
previous_style: &ComputedValues,
style_from_cascade: &Arc<ComputedValues>,
font_metrics_provider: &dyn FontMetricsProvider,
) -> Arc<ComputedValues>
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);
// This currently ignores visited styles, which seems acceptable,
// as existing browsers don't appear to animate visited styles.
let computed = properties::apply_declarations::<E, _>(
context.stylist.device(),
/* pseudo = */ None,
previous_style.rules(),
&context.guards,
// 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)),
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<E>(
@ -1017,6 +1022,7 @@ pub fn maybe_start_animations<E>(
context: &SharedStyleContext,
new_style: &Arc<ComputedValues>,
animation_state: &mut ElementAnimationSet,
font_metrics_provider: &dyn FontMetricsProvider,
) where
E: TElement,
{
@ -1033,7 +1039,7 @@ pub fn maybe_start_animations<E>(
continue;
}
let anim = match context.stylist.get_animation(name, element) {
let keyframe_animation = match context.stylist.get_animation(name, element) {
Some(animation) => animation,
None => continue,
};
@ -1044,7 +1050,7 @@ pub fn maybe_start_animations<E>(
// 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() {
if keyframe_animation.steps.is_empty() {
continue;
}
@ -1071,10 +1077,20 @@ pub fn maybe_start_animations<E>(
AnimationPlayState::Running => AnimationState::Pending,
};
let computed_steps = ComputedKeyframeStep::generate_for_keyframes::<E>(
element,
&keyframe_animation.steps,
context,
new_style,
font_metrics_provider,
new_style.get_box().animation_timing_function_mod(i),
);
let new_animation = Animation {
node: element.as_node().opaque(),
name: name.clone(),
keyframes_animation: anim.clone(),
properties_changed: keyframe_animation.properties_changed,
computed_steps,
started_at: animation_start,
duration: duration as f64,
fill_mode: box_style.animation_fill_mode_mod(i),