Bug 1646811 - servo: Better computation of animation keyframes.

This begins to address #26625 by properly applying CSS variables during
keyframe computation and no longer using `apply_declarations`. Instead,
walk the declarations, combining them into IntermediateComputedKeyframe,
maintaining declarations that modify CSS custom properties. Then compute
a set of AnimationValues for each keyframe and use those to produce
interpolated animation values.

Depends on D80233

Differential Revision: https://phabricator.services.mozilla.com/D80234
This commit is contained in:
Martin Robinson 2020-06-18 18:12:07 +00:00
parent 0afc032d4b
commit 697bb739d1
5 changed files with 261 additions and 129 deletions

View File

@ -8,20 +8,21 @@
// 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::context::{CascadeInputs, SharedStyleContext};
use crate::dom::{OpaqueNode, TDocument, TElement, TNode};
use crate::properties::animated_properties::AnimationValue;
use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode;
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::{KeyframesStep, KeyframesStepValue};
use crate::stylesheets::Origin;
use crate::properties::{
ComputedValues, Importance, LonghandId, LonghandIdSet, PropertyDeclarationBlock,
PropertyDeclarationId,
};
use crate::rule_tree::CascadeLevel;
use crate::style_resolver::StyleResolverForElement;
use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
use crate::values::animated::{Animate, Procedure};
use crate::values::computed::Time;
use crate::values::computed::TimingFunction;
use crate::values::computed::{Time, TimingFunction};
use crate::values::generics::box_::AnimationIterationCount;
use crate::values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction};
use crate::Atom;
@ -172,100 +173,222 @@ 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,
/// A temporary data structure used when calculating ComputedKeyframes for an
/// animation. This data structure is used to collapse information for steps
/// which may be spread across multiple keyframe declarations into a single
/// instance per `start_percentage`.
struct IntermediateComputedKeyframe {
declarations: PropertyDeclarationBlock,
timing_function: Option<TimingFunction>,
start_percentage: f32,
}
impl ComputedKeyframeStep {
fn generate_for_keyframes<E>(
impl IntermediateComputedKeyframe {
fn new(start_percentage: f32) -> Self {
IntermediateComputedKeyframe {
declarations: PropertyDeclarationBlock::new(),
timing_function: None,
start_percentage,
}
}
/// Walk through all keyframe declarations and combine all declarations with the
/// same `start_percentage` into individual `IntermediateComputedKeyframe`s.
fn generate_for_keyframes(
animation: &KeyframesAnimation,
context: &SharedStyleContext,
base_style: &ComputedValues,
) -> Vec<Self> {
let mut intermediate_steps: Vec<Self> = Vec::with_capacity(animation.steps.len());
let mut current_step = IntermediateComputedKeyframe::new(0.);
for step in animation.steps.iter() {
let start_percentage = step.start_percentage.0;
if start_percentage != current_step.start_percentage {
let new_step = IntermediateComputedKeyframe::new(start_percentage);
intermediate_steps.push(std::mem::replace(&mut current_step, new_step));
}
current_step.update_from_step(step, context, base_style);
}
intermediate_steps.push(current_step);
// We should always have a first and a last step, even if these are just
// generated by KeyframesStepValue::ComputedValues.
debug_assert!(intermediate_steps.first().unwrap().start_percentage == 0.);
debug_assert!(intermediate_steps.last().unwrap().start_percentage == 1.);
intermediate_steps
}
fn update_from_step(
&mut self,
step: &KeyframesStep,
context: &SharedStyleContext,
base_style: &ComputedValues,
) {
// Each keyframe declaration may optionally specify a timing function, falling
// back to the one defined global for the animation.
let guard = &context.guards.author;
if let Some(timing_function) = step.get_animation_timing_function(&guard) {
self.timing_function = Some(timing_function.to_computed_value_without_context());
}
let block = match step.value {
KeyframesStepValue::ComputedValues => return,
KeyframesStepValue::Declarations { ref block } => block,
};
// Filter out !important, non-animatable properties, and the
// 'display' property (which is only animatable from SMIL).
let guard = block.read_with(&guard);
for declaration in guard.normal_declaration_iter() {
if let PropertyDeclarationId::Longhand(id) = declaration.id() {
if id == LonghandId::Display {
continue;
}
if !id.is_animatable() {
continue;
}
}
self.declarations.push(
declaration.to_physical(base_style.writing_mode),
Importance::Normal,
);
}
}
fn resolve_style<E>(
self,
element: E,
steps: &[KeyframesStep],
context: &SharedStyleContext,
base_style: &Arc<ComputedValues>,
font_metrics_provider: &dyn FontMetricsProvider,
resolver: &mut StyleResolverForElement<E>,
) -> Arc<ComputedValues>
where
E: TElement,
{
if !self.declarations.any_normal() {
return base_style.clone();
}
let document = element.as_node().owner_doc();
let locked_block = Arc::new(document.shared_lock().wrap(self.declarations));
let mut important_rules_changed = false;
let rule_node = base_style.rules().clone();
let new_node = context.stylist.rule_tree().update_rule_at_level(
CascadeLevel::Animations,
Some(locked_block.borrow_arc()),
&rule_node,
&context.guards,
&mut important_rules_changed,
);
if new_node.is_none() {
return base_style.clone();
}
let inputs = CascadeInputs {
rules: new_node,
visited_rules: base_style.visited_rules().cloned(),
};
resolver
.cascade_style_and_visited_with_default_parents(inputs)
.0
}
}
/// A single computed keyframe for a CSS Animation.
#[derive(Clone, MallocSizeOf)]
struct ComputedKeyframe {
/// The timing function to use for transitions between this step
/// and the next one.
timing_function: TimingFunction,
/// The starting percentage (a number between 0 and 1) which represents
/// at what point in an animation iteration this step is.
start_percentage: f32,
/// The animation values to transition to and from when processing this
/// keyframe animation step.
values: Vec<AnimationValue>,
}
impl ComputedKeyframe {
fn generate_for_keyframes<E>(
element: E,
animation: &KeyframesAnimation,
context: &SharedStyleContext,
base_style: &Arc<ComputedValues>,
default_timing_function: TimingFunction,
resolver: &mut StyleResolverForElement<E>,
) -> Vec<Self>
where
E: TElement,
{
let mut previous_style = base_style.clone();
steps
let mut animating_properties = LonghandIdSet::new();
for property in animation.properties_changed.iter() {
debug_assert!(property.is_animatable());
animating_properties.insert(property.to_physical(base_style.writing_mode));
}
let animation_values_from_style: Vec<AnimationValue> = animating_properties
.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,
}
},
.map(|property| {
AnimationValue::from_computed_values(property, &**base_style)
.expect("Unexpected non-animatable property.")
})
.collect()
.collect();
let intermediate_steps =
IntermediateComputedKeyframe::generate_for_keyframes(animation, context, base_style);
let mut computed_steps: Vec<Self> = Vec::with_capacity(intermediate_steps.len());
for (step_index, step) in intermediate_steps.into_iter().enumerate() {
let start_percentage = step.start_percentage;
let timing_function = step.timing_function.unwrap_or(default_timing_function);
let properties_changed_in_step = step.declarations.longhands().clone();
let step_style = step.resolve_style(element, context, base_style, resolver);
let values = {
// If a value is not set in a property declaration we use the value from
// the style for the first and last keyframe. For intermediate ones, we
// use the value from the previous keyframe.
//
// TODO(mrobinson): According to the spec, we should use an interpolated
// value for properties missing from keyframe declarations.
let default_values = if start_percentage == 0. || start_percentage == 1.0 {
&animation_values_from_style
} else {
debug_assert!(step_index != 0);
&computed_steps[step_index - 1].values
};
// For each property that is animating, pull the value from the resolved
// style for this step if it's in one of the declarations. Otherwise, we
// use the default value from the set we calculated above.
animating_properties
.iter()
.zip(default_values.iter())
.map(|(longhand, default_value)| {
if properties_changed_in_step.contains(longhand) {
AnimationValue::from_computed_values(longhand, &step_style)
.unwrap_or_else(|| default_value.clone())
} else {
default_value.clone()
}
})
.collect()
};
computed_steps.push(ComputedKeyframe {
timing_function,
start_percentage,
values,
});
}
computed_steps
}
}
@ -282,7 +405,7 @@ pub struct Animation {
properties_changed: LonghandIdSet,
/// The computed style for each keyframe of this animation.
computed_steps: Vec<ComputedKeyframeStep>,
computed_steps: Vec<ComputedKeyframe>,
/// The time this animation started at, which is the current value of the animation
/// timeline when this animation was created plus any animation delay.
@ -517,7 +640,7 @@ impl Animation {
next_keyframe_index = self
.computed_steps
.iter()
.position(|step| total_progress as f32 <= step.step.start_percentage.0);
.position(|step| total_progress as f32 <= step.start_percentage);
prev_keyframe_index = next_keyframe_index
.and_then(|pos| if pos != 0 { Some(pos - 1) } else { None })
.unwrap_or(0);
@ -527,7 +650,7 @@ impl Animation {
.computed_steps
.iter()
.rev()
.position(|step| total_progress as f32 <= 1. - step.step.start_percentage.0)
.position(|step| total_progress as f32 <= 1. - step.start_percentage)
.map(|pos| num_steps - pos - 1);
prev_keyframe_index = next_keyframe_index
.and_then(|pos| {
@ -553,58 +676,46 @@ impl Animation {
None => return,
};
let update_with_single_keyframe_style = |style, computed_style: &Arc<ComputedValues>| {
let update_with_single_keyframe_style = |style, keyframe: &ComputedKeyframe| {
let mutable_style = Arc::make_mut(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);
for value in keyframe.values.iter() {
value.set_in_style_for_servo(mutable_style);
}
};
// TODO: How could we optimise it? Is it such a big deal?
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);
update_with_single_keyframe_style(style, &prev_keyframe);
return;
}
if total_progress >= 1.0 {
update_with_single_keyframe_style(style, &next_keyframe_style);
update_with_single_keyframe_style(style, &next_keyframe);
return;
}
let relative_timespan =
(next_keyframe.step.start_percentage.0 - prev_keyframe.step.start_percentage.0).abs();
(next_keyframe.start_percentage - prev_keyframe.start_percentage).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.step.start_percentage.0 as f64)
self.started_at + (duration * prev_keyframe.start_percentage as f64)
},
AnimationDirection::Reverse => {
self.started_at + (duration * (1. - prev_keyframe.step.start_percentage.0 as f64))
self.started_at + (duration * (1. - prev_keyframe.start_percentage as f64))
},
_ => unreachable!(),
};
let relative_progress = (now - last_keyframe_ended_at) / relative_duration;
let relative_progress = (now - last_keyframe_ended_at) / relative_duration;
let mut new_style = (**style).clone();
let mut update_style_for_longhand = |longhand| {
let from = AnimationValue::from_computed_values(longhand, &prev_keyframe_style)?;
let to = AnimationValue::from_computed_values(longhand, &next_keyframe_style)?;
for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) {
PropertyAnimation {
from,
to,
from: from.clone(),
to: to.clone(),
timing_function: prev_keyframe.timing_function,
duration: relative_duration as f64,
}
.update(&mut new_style, relative_progress);
None::<()>
};
for property in self.properties_changed.iter() {
update_style_for_longhand(property);
}
*Arc::make_mut(style) = new_style;
@ -850,7 +961,7 @@ impl ElementAnimationSet {
element: E,
context: &SharedStyleContext,
new_style: &Arc<ComputedValues>,
font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
resolver: &mut StyleResolverForElement<E>,
) where
E: TElement,
{
@ -860,7 +971,7 @@ impl ElementAnimationSet {
}
}
maybe_start_animations(element, &context, &new_style, self, font_metrics);
maybe_start_animations(element, &context, &new_style, self, resolver);
}
/// Update our transitions given a new style, canceling or starting new animations
@ -1022,7 +1133,7 @@ pub fn maybe_start_animations<E>(
context: &SharedStyleContext,
new_style: &Arc<ComputedValues>,
animation_state: &mut ElementAnimationSet,
font_metrics_provider: &dyn FontMetricsProvider,
resolver: &mut StyleResolverForElement<E>,
) where
E: TElement,
{
@ -1077,13 +1188,13 @@ pub fn maybe_start_animations<E>(
AnimationPlayState::Running => AnimationState::Pending,
};
let computed_steps = ComputedKeyframeStep::generate_for_keyframes::<E>(
let computed_steps = ComputedKeyframe::generate_for_keyframes(
element,
&keyframe_animation.steps,
&keyframe_animation,
context,
new_style,
font_metrics_provider,
new_style.get_box().animation_timing_function_mod(i),
resolver,
);
let new_animation = Animation {

View File

@ -17,7 +17,7 @@ use crate::font_metrics::FontMetricsProvider;
use crate::media_queries::Device;
use crate::properties::{AnimationRules, ComputedValues, PropertyDeclarationBlock};
use crate::selector_parser::{AttrValue, Lang, PseudoElement, SelectorImpl};
use crate::shared_lock::Locked;
use crate::shared_lock::{Locked, SharedRwLock};
use crate::stylist::CascadeData;
use crate::traversal_flags::TraversalFlags;
use crate::{Atom, LocalName, Namespace, WeakAtom};
@ -128,6 +128,9 @@ pub trait TDocument: Sized + Copy + Clone {
{
Err(())
}
/// This document's shared lock.
fn shared_lock(&self) -> &SharedRwLock;
}
/// The `TNode` trait. This is the main generic trait over which the style

View File

@ -64,7 +64,7 @@ use crate::properties::{ComputedValues, LonghandId};
use crate::properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock};
use crate::rule_tree::CascadeLevel as ServoCascadeLevel;
use crate::selector_parser::{AttrValue, HorizontalDirection, Lang};
use crate::shared_lock::Locked;
use crate::shared_lock::{Locked, SharedRwLock};
use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace};
use crate::stylist::CascadeData;
use crate::values::computed::font::GenericFontFamily;
@ -139,6 +139,10 @@ impl<'ld> TDocument for GeckoDocument<'ld> {
bindings::Gecko_Document_GetElementsWithId(self.0, id.as_ptr())
}))
}
fn shared_lock(&self) -> &SharedRwLock {
&GLOBAL_STYLE_DATA.shared_lock
}
}
/// A simple wrapper over `ShadowRoot`.

View File

@ -20,6 +20,8 @@ use crate::properties::ComputedValues;
use crate::rule_tree::{CascadeLevel, StrongRuleNode};
use crate::selector_parser::{PseudoElement, RestyleDamage};
use crate::style_resolver::ResolvedElementStyles;
use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
use crate::stylist::RuleInclusion;
use crate::traversal_flags::TraversalFlags;
use selectors::matching::ElementSelectorFlags;
use servo_arc::{Arc, ArcBorrow};
@ -199,8 +201,6 @@ trait PrivateMatchMethods: TElement {
primary_style: &Arc<ComputedValues>,
) -> Option<Arc<ComputedValues>> {
use crate::context::CascadeInputs;
use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
use crate::stylist::RuleInclusion;
let rule_node = primary_style.rules();
let without_transition_rules = context
@ -458,11 +458,18 @@ trait PrivateMatchMethods: TElement {
// for all the keyframes. We only want to do this if we think that there's a
// chance that the animations really changed.
if needs_animations_update {
let mut resolver = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
);
animation_set.update_animations_for_new_style::<Self>(
*self,
&shared_context,
&new_values,
&context.thread_local.font_metrics_provider,
&mut resolver,
);
}

View File

@ -321,6 +321,13 @@ impl PropertyDeclarationBlock {
self.longhands.contains_any_reset()
}
/// Returns a `LonghandIdSet` representing the properties that are changed in
/// this block.
#[inline]
pub fn longhands(&self) -> &LonghandIdSet {
&self.longhands
}
/// Get a declaration for a given property.
///
/// NOTE: This is linear time in the case of custom properties or in the