blob: 18dcabd79109985dd4a208ebb2af0925cc9f556a [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/animation/css/css_animations.h"
#include <algorithm>
#include <bitset>
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_computed_effect_timing.h"
#include "third_party/blink/renderer/core/animation/animation.h"
#include "third_party/blink/renderer/core/animation/compositor_animations.h"
#include "third_party/blink/renderer/core/animation/css/compositor_keyframe_value_factory.h"
#include "third_party/blink/renderer/core/animation/css/css_animation.h"
#include "third_party/blink/renderer/core/animation/css/css_keyframe_effect_model.h"
#include "third_party/blink/renderer/core/animation/css/css_scroll_timeline.h"
#include "third_party/blink/renderer/core/animation/css/css_transition.h"
#include "third_party/blink/renderer/core/animation/css_interpolation_types_map.h"
#include "third_party/blink/renderer/core/animation/document_animations.h"
#include "third_party/blink/renderer/core/animation/document_timeline.h"
#include "third_party/blink/renderer/core/animation/element_animations.h"
#include "third_party/blink/renderer/core/animation/inert_effect.h"
#include "third_party/blink/renderer/core/animation/interpolation.h"
#include "third_party/blink/renderer/core/animation/interpolation_environment.h"
#include "third_party/blink/renderer/core/animation/interpolation_type.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect_model.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline_offset.h"
#include "third_party/blink/renderer/core/animation/timing_calculations.h"
#include "third_party/blink/renderer/core/animation/transition_interpolation.h"
#include "third_party/blink/renderer/core/animation/worklet_animation_base.h"
#include "third_party/blink/renderer/core/css/css_keyframe_rule.h"
#include "third_party/blink/renderer/core/css/css_property_equality.h"
#include "third_party/blink/renderer/core/css/css_value_list.h"
#include "third_party/blink/renderer/core/css/parser/css_variable_parser.h"
#include "third_party/blink/renderer/core/css/properties/computed_style_utils.h"
#include "third_party/blink/renderer/core/css/properties/css_property.h"
#include "third_party/blink/renderer/core/css/property_registry.h"
#include "third_party/blink/renderer/core/css/resolver/css_to_style_map.h"
#include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event_path.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/pseudo_element.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/events/animation_event.h"
#include "third_party/blink/renderer/core/events/transition_event.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/style_property_shorthand.h"
#include "third_party/blink/renderer/platform/animation/timing_function.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/histogram.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
namespace blink {
using PropertySet = HashSet<const CSSProperty*>;
namespace {
// Processes keyframe rules, extracting the timing function and properties being
// animated for each keyframe. The extraction process is doing more work that
// strictly required for the setup to step 5 in the spec
// (https://drafts.csswg.org/css-animations-2/#keyframes) as an optimization
// to avoid needing to process each rule multiple times to extract different
// properties.
StringKeyframeVector ProcessKeyframesRule(
const StyleRuleKeyframes* keyframes_rule,
const Document& document,
const ComputedStyle* parent_style,
TimingFunction* default_timing_function,
WritingMode writing_mode,
TextDirection text_direction) {
StringKeyframeVector keyframes;
const HeapVector<Member<StyleRuleKeyframe>>& style_keyframes =
keyframes_rule->Keyframes();
for (wtf_size_t i = 0; i < style_keyframes.size(); ++i) {
const StyleRuleKeyframe* style_keyframe = style_keyframes[i].Get();
auto* keyframe = MakeGarbageCollected<StringKeyframe>();
const Vector<double>& offsets = style_keyframe->Keys();
DCHECK(!offsets.IsEmpty());
keyframe->SetOffset(offsets[0]);
keyframe->SetEasing(default_timing_function);
const CSSPropertyValueSet& properties = style_keyframe->Properties();
for (unsigned j = 0; j < properties.PropertyCount(); j++) {
// TODO(crbug.com/980160): Remove access to static Variable instance.
const CSSProperty& property =
CSSProperty::Get(properties.PropertyAt(j).Id());
if (property.PropertyID() == CSSPropertyID::kAnimationTimingFunction) {
const CSSValue& value = properties.PropertyAt(j).Value();
scoped_refptr<TimingFunction> timing_function;
if (value.IsInheritedValue() && parent_style->Animations()) {
timing_function = parent_style->Animations()->TimingFunctionList()[0];
} else if (auto* value_list = DynamicTo<CSSValueList>(value)) {
timing_function =
CSSToStyleMap::MapAnimationTimingFunction(value_list->Item(0));
} else {
DCHECK(value.IsCSSWideKeyword());
timing_function = CSSTimingData::InitialTimingFunction();
}
keyframe->SetEasing(std::move(timing_function));
} else if (!CSSAnimations::IsAnimationAffectingProperty(property)) {
// Map Logical to physical property name.
const CSSProperty& physical_property =
property.ResolveDirectionAwareProperty(text_direction,
writing_mode);
keyframe->SetCSSPropertyValue(physical_property,
properties.PropertyAt(j).Value());
}
}
keyframes.push_back(keyframe);
// The last keyframe specified at a given offset is used.
for (wtf_size_t j = 1; j < offsets.size(); ++j) {
keyframes.push_back(
To<StringKeyframe>(keyframe->CloneWithOffset(offsets[j])));
}
}
std::stable_sort(keyframes.begin(), keyframes.end(),
[](const Member<Keyframe>& a, const Member<Keyframe>& b) {
return a->CheckedOffset() < b->CheckedOffset();
});
return keyframes;
}
// Finds the index of a keyframe with matching offset and easing.
base::Optional<int> FindIndexOfMatchingKeyframe(
const StringKeyframeVector& keyframes,
wtf_size_t start_index,
double offset,
const TimingFunction& easing) {
for (wtf_size_t i = start_index; i < keyframes.size(); i++) {
StringKeyframe* keyframe = keyframes[i];
// Keyframes are sorted by offset. Search can stop once we hit and offset
// that exceeds the target value.
if (offset < keyframe->CheckedOffset())
break;
if (easing.ToString() == keyframe->Easing().ToString())
return i;
}
return base::nullopt;
}
// Tests conditions for inserting a bounding keyframe, which are outlined in
// steps 6 and 7 of the spec for keyframe construction.
// https://drafts.csswg.org/css-animations-2/#keyframes
bool NeedsBoundaryKeyframe(StringKeyframe* candidate,
double offset,
const PropertySet& animated_properties,
const PropertySet& bounding_properties,
TimingFunction* default_timing_function) {
if (!candidate)
return true;
if (candidate->CheckedOffset() != offset)
return true;
if (bounding_properties.size() == animated_properties.size())
return false;
return candidate->Easing().ToString() != default_timing_function->ToString();
}
StringKeyframeEffectModel* CreateKeyframeEffectModel(
StyleResolver* resolver,
const Element* animating_element,
Element& element,
const ComputedStyle* style,
const ComputedStyle* parent_style,
const AtomicString& name,
TimingFunction* default_timing_function,
size_t animation_index) {
// The algorithm for constructing string keyframes for a CSS animation is
// covered in the following spec:
// https://drafts.csswg.org/css-animations-2/#keyframes
// For a given target (pseudo-)element, element, animation name, and
// position of the animation in element’s animation-name list, keyframe
// objects are generated as follows:
// 1. Let default timing function be the timing function at the position
// of the resolved value of the animation-timing-function for element,
// repeating the list as necessary as described in CSS Animations 1 §4.2
// The animation-name property.
// 2. Find the last @keyframes at-rule in document order with <keyframes-name>
// matching name.
// If there is no @keyframes at-rule with <keyframes-name> matching name,
// abort this procedure. In this case no animation is generated, and any
// existing animation matching name is canceled.
const StyleRuleKeyframes* keyframes_rule =
resolver->FindKeyframesRule(&element, name);
DCHECK(keyframes_rule);
// 3. Let keyframes be an empty sequence of keyframe objects.
StringKeyframeVector keyframes;
// 4. Let animated properties be an empty set of longhand CSS property names.
PropertySet animated_properties;
// Start and end properties are also tracked to simplify the process of
// determining if the first and last keyframes are missing properties.
PropertySet start_properties;
PropertySet end_properties;
// Properties that have already been processed at the current keyframe.
PropertySet current_offset_properties;
// 5. Perform a stable sort of the keyframe blocks in the @keyframes rule by
// the offset specified in the keyframe selector, and iterate over the
// result in reverse applying the following steps:
keyframes = ProcessKeyframesRule(keyframes_rule, element.GetDocument(),
parent_style, default_timing_function,
style->GetWritingMode(), style->Direction());
double last_offset = 1;
wtf_size_t merged_frame_count = 0;
for (wtf_size_t i = keyframes.size(); i > 0; --i) {
// 5.1 Let keyframe offset be the value of the keyframe selector converted
// to a value in the range 0 ≤ keyframe offset ≤ 1.
int source_index = i - 1;
StringKeyframe* rule_keyframe = keyframes[source_index];
double keyframe_offset = rule_keyframe->CheckedOffset();
// 5.2 Let keyframe timing function be the value of the last valid
// declaration of animation-timing-function specified on the keyframe
// block, or, if there is no such valid declaration, default timing
// function.
const TimingFunction& easing = rule_keyframe->Easing();
// 5.3 After converting keyframe timing function to its canonical form (e.g.
// such that step-end becomes steps(1, end)) let keyframe refer to the
// existing keyframe in keyframes with matching keyframe offset and
// timing function, if any.
// If there is no such existing keyframe, let keyframe be a new empty
// keyframe with offset, keyframe offset, and timing function, keyframe
// timing function, and prepend it to keyframes.
// Prevent stomping a rule override by tracking properties applied at
// the current offset.
if (last_offset != keyframe_offset) {
current_offset_properties.clear();
last_offset = keyframe_offset;
}
// Avoid unnecessary creation of extra keyframes by merging into
// existing keyframes.
base::Optional<int> existing_keyframe_index = FindIndexOfMatchingKeyframe(
keyframes, source_index + merged_frame_count + 1, keyframe_offset,
easing);
int target_index;
if (existing_keyframe_index) {
// Merge keyframe propoerties.
target_index = existing_keyframe_index.value();
merged_frame_count++;
} else {
target_index = source_index + merged_frame_count;
if (target_index != source_index) {
// Move keyframe to fill the gap.
keyframes[target_index] = keyframes[source_index];
source_index = target_index;
}
}
// 5.4 Iterate over all declarations in the keyframe block and add them to
// keyframe such that:
// * All variable references are resolved to their current values.
// * Each shorthand property is expanded to its longhand subproperties.
// * All logical properties are converted to their equivalent physical
// properties.
// * For any expanded physical longhand properties that appear more than
// once, only the last declaration in source order is added.
// Note, since multiple keyframe blocks may specify the same keyframe
// offset, and since this algorithm iterates over these blocks in
// reverse, this implies that if any properties are encountered that
// have already added at this same keyframe offset, they should be
// skipped.
// * All property values are replaced with their computed values.
// 5.5 Add each physical longhand property name that was added to keyframe
// to animated properties.
StringKeyframe* keyframe = keyframes[target_index];
for (const auto& property : rule_keyframe->Properties()) {
const CSSProperty& css_property = property.GetCSSProperty();
// Since processing keyframes in reverse order, skipping properties that
// have already been inserted prevents overwriting a later merged
// keyframe.
if (current_offset_properties.Contains(&css_property))
continue;
if (source_index != target_index) {
keyframe->SetCSSPropertyValue(
css_property, rule_keyframe->CssPropertyValue(property));
}
current_offset_properties.insert(&css_property);
animated_properties.insert(&css_property);
if (keyframe_offset == 0)
start_properties.insert(&css_property);
else if (keyframe_offset == 1)
end_properties.insert(&css_property);
}
}
// Compact the vector of keyframes if any keyframes have been merged.
keyframes.EraseAt(0, merged_frame_count);
// 6. If there is no keyframe in keyframes with offset 0, or if amongst the
// keyframes in keyframes with offset 0 not all of the properties in
// animated properties are present,
//
// 6.1 Let initial keyframe be the keyframe in keyframes with offset 0 and
// timing function default timing function.
// 6.2 If there is no such keyframe, let initial keyframe be a new empty
// keyframe with offset 0, and timing function default timing function,
// and add it to keyframes after the last keyframe with offset 0.
// 6.3 For each property in animated properties that is not present in some
// other keyframe with offset 0, add the computed value of that property
// for element to the keyframe.
StringKeyframe* start_keyframe = keyframes.IsEmpty() ? nullptr : keyframes[0];
if (NeedsBoundaryKeyframe(start_keyframe, 0, animated_properties,
start_properties, default_timing_function)) {
start_keyframe = MakeGarbageCollected<StringKeyframe>();
start_keyframe->SetOffset(0);
start_keyframe->SetEasing(default_timing_function);
keyframes.push_front(start_keyframe);
}
// 7. Similarly, if there is no keyframe in keyframes with offset 1, or if
// amongst the keyframes in keyframes with offset 1 not all of the
// properties in animated properties are present,
//
// 7.1 Let final keyframe be the keyframe in keyframes with offset 1 and
// timing function default timing function.
// 7.2 If there is no such keyframe, let final keyframe be a new empty
// keyframe with offset 1, and timing function default timing function,
// and add it to keyframes after the last keyframe with offset 1.
// 7.3 For each property in animated properties that is not present in some
// other keyframe with offset 1, add the computed value of that property
// for element to the keyframe.
StringKeyframe* end_keyframe = keyframes[keyframes.size() - 1];
if (NeedsBoundaryKeyframe(end_keyframe, 1, animated_properties,
end_properties, default_timing_function)) {
end_keyframe = MakeGarbageCollected<StringKeyframe>();
end_keyframe->SetOffset(1);
end_keyframe->SetEasing(default_timing_function);
keyframes.push_back(end_keyframe);
}
DCHECK_GE(keyframes.size(), 2U);
DCHECK_EQ(keyframes.front()->CheckedOffset(), 0);
DCHECK_EQ(keyframes.back()->CheckedOffset(), 1);
auto* model = MakeGarbageCollected<CssKeyframeEffectModel>(
keyframes, EffectModel::kCompositeReplace, &start_keyframe->Easing());
if (animation_index > 0 && model->HasSyntheticKeyframes()) {
UseCounter::Count(element.GetDocument(),
WebFeature::kCSSAnimationsStackedNeutralKeyframe);
}
return model;
}
// Returns the start time of an animation given the start delay. A negative
// start delay results in the animation starting with non-zero progress.
AnimationTimeDelta StartTimeFromDelay(double start_delay) {
return AnimationTimeDelta::FromSecondsD(start_delay < 0 ? -start_delay : 0);
}
// Timing functions for computing elapsed time of an event.
AnimationTimeDelta IntervalStart(const AnimationEffect& effect) {
const double start_delay = effect.SpecifiedTiming().start_delay;
const double active_duration = effect.SpecifiedTiming().ActiveDuration();
return AnimationTimeDelta::FromSecondsD(
std::fmax(std::fmin(-start_delay, active_duration), 0.0));
}
AnimationTimeDelta IntervalEnd(const AnimationEffect& effect) {
const double start_delay = effect.SpecifiedTiming().start_delay;
const double end_delay = effect.SpecifiedTiming().end_delay;
const double active_duration = effect.SpecifiedTiming().ActiveDuration();
const double target_effect_end =
std::max(start_delay + active_duration + end_delay, 0.0);
return AnimationTimeDelta::FromSecondsD(std::max(
std::min(target_effect_end - start_delay, active_duration), 0.0));
}
AnimationTimeDelta IterationElapsedTime(const AnimationEffect& effect,
double previous_iteration) {
const double current_iteration = effect.CurrentIteration().value();
const double iteration_boundary = (previous_iteration > current_iteration)
? current_iteration + 1
: current_iteration;
const double iteration_start = effect.SpecifiedTiming().iteration_start;
const AnimationTimeDelta iteration_duration =
effect.SpecifiedTiming().IterationDuration();
return iteration_duration * (iteration_boundary - iteration_start);
}
CSSScrollTimeline* CreateCSSScrollTimeline(
Element* element,
CSSScrollTimeline::Options&& options) {
if (!options.IsValid())
return nullptr;
auto* scroll_timeline = MakeGarbageCollected<CSSScrollTimeline>(
&element->GetDocument(), std::move(options));
// It's is not allowed for a style resolve to create timelines that
// needs timing updates (i.e. AnimationTimeline::NeedsAnimationTimingUpdate()
// must return false). Servicing animations after creation preserves this
// invariant by ensuring the last-update time of the timeline is equal to
// the current time.
scroll_timeline->ServiceAnimations(kTimingUpdateOnDemand);
return scroll_timeline;
}
CSSScrollTimeline* FindMatchingCachedTimeline(
Document& document,
const AtomicString& name,
const CSSScrollTimeline::Options& options) {
auto* cached_timeline = DynamicTo<CSSScrollTimeline>(
document.GetDocumentAnimations().FindCachedCSSScrollTimeline(name));
if (cached_timeline && cached_timeline->Matches(options))
return cached_timeline;
return nullptr;
}
AnimationTimeline* ComputeTimeline(Element* element,
const StyleNameOrKeyword& timeline_name,
StyleRuleScrollTimeline* rule,
AnimationTimeline* existing_timeline) {
Document& document = element->GetDocument();
if (timeline_name.IsKeyword()) {
if (timeline_name.GetKeyword() == CSSValueID::kAuto)
return &document.Timeline();
DCHECK_EQ(timeline_name.GetKeyword(), CSSValueID::kNone);
return nullptr;
}
if (rule) {
CSSScrollTimeline::Options options(element, *rule);
const AtomicString& name = timeline_name.GetName().GetValue();
// When multiple animations refer to the same @scroll-timeline, the same
// CSSScrollTimeline instance should be shared.
if (auto* timeline = FindMatchingCachedTimeline(document, name, options))
return timeline;
// When the incoming options match the existing timeline (associated with
// an existing animation), we can continue to use the existing timeline,
// since creating a new timeline from the options would just yield an
// identical timeline.
if (auto* timeline = DynamicTo<CSSScrollTimeline>(existing_timeline)) {
if (timeline->Matches(options))
return existing_timeline;
}
if (auto* timeline = CreateCSSScrollTimeline(element, std::move(options)))
return timeline;
}
return nullptr;
}
StyleRuleScrollTimeline* FindScrollTimelineRule(
Document& document,
const StyleNameOrKeyword& timeline_name) {
if (timeline_name.IsKeyword())
return nullptr;
return document.GetStyleEngine().FindScrollTimelineRule(
timeline_name.GetName().GetValue());
}
} // namespace
CSSAnimations::CSSAnimations() = default;
namespace {
const KeyframeEffectModelBase* GetKeyframeEffectModelBase(
const AnimationEffect* effect) {
if (!effect)
return nullptr;
const EffectModel* model = nullptr;
if (auto* keyframe_effect = DynamicTo<KeyframeEffect>(effect))
model = keyframe_effect->Model();
else if (auto* inert_effect = DynamicTo<InertEffect>(effect))
model = inert_effect->Model();
if (!model || !model->IsKeyframeEffectModel())
return nullptr;
return To<KeyframeEffectModelBase>(model);
}
bool ComputedValuesEqual(const PropertyHandle& property,
const ComputedStyle& a,
const ComputedStyle& b) {
// If zoom hasn't changed, compare internal values (stored with zoom applied)
// for speed. Custom properties are never zoomed so they are checked here too.
if (a.EffectiveZoom() == b.EffectiveZoom() ||
property.IsCSSCustomProperty()) {
return CSSPropertyEquality::PropertiesEqual(property, a, b);
}
// If zoom has changed, we must construct and compare the unzoomed
// computed values.
if (property.GetCSSProperty().PropertyID() == CSSPropertyID::kTransform) {
// Transform lists require special handling in this case to deal with
// layout-dependent interpolation which does not yet have a CSSValue.
return a.Transform().Zoom(1 / a.EffectiveZoom()) ==
b.Transform().Zoom(1 / b.EffectiveZoom());
} else {
const CSSValue* a_val =
ComputedStyleUtils::ComputedPropertyValue(property.GetCSSProperty(), a);
const CSSValue* b_val =
ComputedStyleUtils::ComputedPropertyValue(property.GetCSSProperty(), b);
// Computed values can be null if not able to parse.
if (a_val && b_val)
return *a_val == *b_val;
// Fallback to the zoom-unaware comparator if either value could not be
// parsed.
return CSSPropertyEquality::PropertiesEqual(property, a, b);
}
}
} // namespace
void CSSAnimations::CalculateCompositorAnimationUpdate(
CSSAnimationUpdate& update,
const Element* animating_element,
Element& element,
const ComputedStyle& style,
const ComputedStyle* parent_style,
bool was_viewport_resized) {
ElementAnimations* element_animations =
animating_element ? animating_element->GetElementAnimations() : nullptr;
// If the change in style is only due to the Blink-side animation update, we
// do not need to update the compositor-side animations. The compositor is
// already changing the same properties and as such this update would provide
// no new information.
if (!element_animations || element_animations->IsAnimationStyleChange())
return;
const ComputedStyle* old_style = animating_element->GetComputedStyle();
if (!old_style || old_style->IsEnsuredInDisplayNone() ||
!old_style->ShouldCompositeForCurrentAnimations()) {
return;
}
bool transform_zoom_changed =
old_style->HasCurrentTransformAnimation() &&
old_style->EffectiveZoom() != style.EffectiveZoom();
const auto& snapshot = [&](AnimationEffect* effect) {
const KeyframeEffectModelBase* keyframe_effect =
GetKeyframeEffectModelBase(effect);
if (!keyframe_effect)
return false;
if ((transform_zoom_changed || was_viewport_resized) &&
(keyframe_effect->Affects(PropertyHandle(GetCSSPropertyTransform())) ||
keyframe_effect->Affects(PropertyHandle(GetCSSPropertyTranslate()))))
keyframe_effect->InvalidateCompositorKeyframesSnapshot();
if (keyframe_effect->SnapshotAllCompositorKeyframesIfNecessary(
element, style, parent_style)) {
return true;
} else if (keyframe_effect->HasSyntheticKeyframes() &&
keyframe_effect->SnapshotNeutralCompositorKeyframes(
element, *old_style, style, parent_style)) {
return true;
}
return false;
};
for (auto& entry : element_animations->Animations()) {
Animation& animation = *entry.key;
if (snapshot(animation.effect()))
update.UpdateCompositorKeyframes(&animation);
}
for (auto& entry : element_animations->GetWorkletAnimations()) {
WorkletAnimationBase& animation = *entry;
if (snapshot(animation.GetEffect()))
animation.InvalidateCompositingState();
}
}
void CSSAnimations::CalculateAnimationUpdate(CSSAnimationUpdate& update,
const Element* animating_element,
Element& element,
const ComputedStyle& style,
const ComputedStyle* parent_style,
StyleResolver* resolver) {
ElementAnimations* element_animations =
animating_element ? animating_element->GetElementAnimations() : nullptr;
bool is_animation_style_change =
element_animations && element_animations->IsAnimationStyleChange();
#if !DCHECK_IS_ON()
// If we're in an animation style change, no animations can have started, been
// cancelled or changed play state. When DCHECK is enabled, we verify this
// optimization.
if (is_animation_style_change) {
CalculateAnimationActiveInterpolations(update, animating_element);
return;
}
#endif
// Rebuild the keyframe model for a CSS animation if it may have been
// invalidated by a change to the text direction or writing mode.
const ComputedStyle* old_style =
animating_element ? animating_element->GetComputedStyle() : nullptr;
bool logical_property_mapping_change =
!old_style || old_style->Direction() != style.Direction() ||
old_style->GetWritingMode() != style.GetWritingMode();
if (logical_property_mapping_change && element_animations) {
// Update computed keyframes for any running animations that depend on
// logical properties.
for (auto& entry : element_animations->Animations()) {
Animation* animation = entry.key;
if (auto* keyframe_effect =
DynamicTo<KeyframeEffect>(animation->effect())) {
keyframe_effect->SetLogicalPropertyResolutionContext(
style.Direction(), style.GetWritingMode());
animation->UpdateIfNecessary();
}
}
}
const CSSAnimationData* animation_data = style.Animations();
const CSSAnimations* css_animations =
element_animations ? &element_animations->CssAnimations() : nullptr;
Vector<bool> cancel_running_animation_flags(
css_animations ? css_animations->running_animations_.size() : 0);
for (bool& flag : cancel_running_animation_flags)
flag = true;
if (animation_data && style.Display() != EDisplay::kNone) {
const Vector<AtomicString>& name_list = animation_data->NameList();
for (wtf_size_t i = 0; i < name_list.size(); ++i) {
AtomicString name = name_list[i];
if (name == CSSAnimationData::InitialName())
continue;
// Find n where this is the nth occurence of this animation name.
wtf_size_t name_index = 0;
for (wtf_size_t j = 0; j < i; j++) {
if (name_list[j] == name)
name_index++;
}
const bool is_paused =
CSSTimingData::GetRepeated(animation_data->PlayStateList(), i) ==
EAnimPlayState::kPaused;
Timing timing = animation_data->ConvertToTiming(i);
Timing specified_timing = timing;
scoped_refptr<TimingFunction> keyframe_timing_function =
timing.timing_function;
timing.timing_function = Timing().timing_function;
StyleRuleKeyframes* keyframes_rule =
resolver->FindKeyframesRule(&element, name);
if (!keyframes_rule)
continue; // Cancel the animation if there's no style rule for it.
const StyleNameOrKeyword& timeline_name = animation_data->GetTimeline(i);
StyleRuleScrollTimeline* scroll_timeline_rule =
FindScrollTimelineRule(element.GetDocument(), timeline_name);
const RunningAnimation* existing_animation = nullptr;
wtf_size_t existing_animation_index = 0;
if (css_animations) {
for (wtf_size_t j = 0; j < css_animations->running_animations_.size();
j++) {
const RunningAnimation& running_animation =
*css_animations->running_animations_[j];
if (running_animation.name == name &&
running_animation.name_index == name_index) {
existing_animation = &running_animation;
existing_animation_index = j;
break;
}
}
}
if (existing_animation) {
cancel_running_animation_flags[existing_animation_index] = false;
CSSAnimation* animation =
DynamicTo<CSSAnimation>(existing_animation->animation.Get());
animation->SetAnimationIndex(i);
const bool was_paused =
CSSTimingData::GetRepeated(existing_animation->play_state_list,
i) == EAnimPlayState::kPaused;
// Explicit calls to web-animation play controls override changes to
// play state via the animation-play-state style. Ensure that the new
// play state based on animation-play-state differs from the current
// play state and that the change is not blocked by a sticky state.
bool toggle_pause_state = false;
if (is_paused != was_paused && !animation->getIgnoreCSSPlayState()) {
if (animation->Paused() && !is_paused)
toggle_pause_state = true;
else if (animation->Playing() && is_paused)
toggle_pause_state = true;
}
bool will_be_playing =
toggle_pause_state ? animation->Paused() : animation->Playing();
AnimationTimeline* timeline = existing_animation->Timeline();
if (!is_animation_style_change && !animation->GetIgnoreCSSTimeline()) {
timeline = ComputeTimeline(&element, timeline_name,
scroll_timeline_rule, timeline);
}
if (keyframes_rule != existing_animation->style_rule ||
keyframes_rule->Version() !=
existing_animation->style_rule_version ||
existing_animation->specified_timing != specified_timing ||
is_paused != was_paused || logical_property_mapping_change ||
timeline != existing_animation->Timeline()) {
DCHECK(!is_animation_style_change);
base::Optional<TimelinePhase> inherited_phase;
base::Optional<AnimationTimeDelta> inherited_time;
if (timeline) {
inherited_phase = base::make_optional(timeline->Phase());
inherited_time = animation->UnlimitedCurrentTime();
if (will_be_playing &&
((timeline != existing_animation->Timeline()) ||
animation->ResetsCurrentTimeOnResume())) {
if (!timeline->IsMonotonicallyIncreasing())
inherited_time = timeline->CurrentTime();
}
}
update.UpdateAnimation(
existing_animation_index, animation,
*MakeGarbageCollected<InertEffect>(
CreateKeyframeEffectModel(resolver, animating_element,
element, &style, parent_style, name,
keyframe_timing_function.get(), i),
timing, is_paused, inherited_time, inherited_phase),
specified_timing, keyframes_rule, timeline,
animation_data->PlayStateList());
if (toggle_pause_state)
update.ToggleAnimationIndexPaused(existing_animation_index);
}
} else {
DCHECK(!is_animation_style_change);
AnimationTimeline* timeline =
ComputeTimeline(&element, timeline_name, scroll_timeline_rule,
nullptr /* existing_timeline */);
base::Optional<TimelinePhase> inherited_phase;
base::Optional<AnimationTimeDelta> inherited_time;
if (timeline) {
if (timeline->IsMonotonicallyIncreasing()) {
inherited_time = AnimationTimeDelta();
} else {
inherited_phase = base::make_optional(timeline->Phase());
inherited_time = timeline->CurrentTime();
}
}
update.StartAnimation(
name, name_index, i,
*MakeGarbageCollected<InertEffect>(
CreateKeyframeEffectModel(resolver, animating_element, element,
&style, parent_style, name,
keyframe_timing_function.get(), i),
timing, is_paused, inherited_time, inherited_phase),
specified_timing, keyframes_rule, timeline,
animation_data->PlayStateList());
}
}
}
for (wtf_size_t i = 0; i < cancel_running_animation_flags.size(); i++) {
if (cancel_running_animation_flags[i]) {
DCHECK(css_animations && !is_animation_style_change);
update.CancelAnimation(
i, *css_animations->running_animations_[i]->animation);
}
}
CalculateAnimationActiveInterpolations(update, animating_element);
}
AnimationEffect::EventDelegate* CSSAnimations::CreateEventDelegate(
Element* element,
const PropertyHandle& property_handle,
const AnimationEffect::EventDelegate* old_event_delegate) {
const CSSAnimations::TransitionEventDelegate* old_transition_delegate =
DynamicTo<CSSAnimations::TransitionEventDelegate>(old_event_delegate);
Timing::Phase previous_phase =
old_transition_delegate ? old_transition_delegate->getPreviousPhase()
: Timing::kPhaseNone;
return MakeGarbageCollected<TransitionEventDelegate>(element, property_handle,
previous_phase);
}
AnimationEffect::EventDelegate* CSSAnimations::CreateEventDelegate(
Element* element,
const AtomicString& animation_name,
const AnimationEffect::EventDelegate* old_event_delegate) {
const CSSAnimations::AnimationEventDelegate* old_animation_delegate =
DynamicTo<CSSAnimations::AnimationEventDelegate>(old_event_delegate);
Timing::Phase previous_phase =
old_animation_delegate ? old_animation_delegate->getPreviousPhase()
: Timing::kPhaseNone;
base::Optional<double> previous_iteration =
old_animation_delegate ? old_animation_delegate->getPreviousIteration()
: base::nullopt;
return MakeGarbageCollected<AnimationEventDelegate>(
element, animation_name, previous_phase, previous_iteration);
}
void CSSAnimations::SnapshotCompositorKeyframes(
Element& element,
CSSAnimationUpdate& update,
const ComputedStyle& style,
const ComputedStyle* parent_style) {
const auto& snapshot = [&element, &style,
parent_style](const AnimationEffect* effect) {
const KeyframeEffectModelBase* keyframe_effect =
GetKeyframeEffectModelBase(effect);
if (keyframe_effect) {
keyframe_effect->SnapshotAllCompositorKeyframesIfNecessary(element, style,
parent_style);
}
};
ElementAnimations* element_animations = element.GetElementAnimations();
if (element_animations) {
for (auto& entry : element_animations->Animations())
snapshot(entry.key->effect());
}
for (const auto& new_animation : update.NewAnimations())
snapshot(new_animation.effect.Get());
for (const auto& updated_animation : update.AnimationsWithUpdates())
snapshot(updated_animation.effect.Get());
for (const auto& new_transition : update.NewTransitions())
snapshot(new_transition.value->effect.Get());
}
void CSSAnimations::MaybeApplyPendingUpdate(Element* element) {
previous_active_interpolations_for_custom_animations_.clear();
previous_active_interpolations_for_standard_animations_.clear();
if (pending_update_.IsEmpty())
return;
previous_active_interpolations_for_custom_animations_.swap(
pending_update_.ActiveInterpolationsForCustomAnimations());
previous_active_interpolations_for_standard_animations_.swap(
pending_update_.ActiveInterpolationsForStandardAnimations());
for (wtf_size_t paused_index :
pending_update_.AnimationIndicesWithPauseToggled()) {
CSSAnimation* animation = DynamicTo<CSSAnimation>(
running_animations_[paused_index]->animation.Get());
if (animation->Paused()) {
animation->Unpause();
animation->resetIgnoreCSSPlayState();
} else {
animation->pause();
animation->resetIgnoreCSSPlayState();
}
if (animation->Outdated())
animation->Update(kTimingUpdateOnDemand);
}
for (const auto& animation : pending_update_.UpdatedCompositorKeyframes())
animation->SetCompositorPending(true);
for (const auto& entry : pending_update_.AnimationsWithUpdates()) {
if (entry.animation->effect()) {
auto* effect = To<KeyframeEffect>(entry.animation->effect());
if (!effect->GetIgnoreCSSKeyframes())
effect->SetModel(entry.effect->Model());
effect->UpdateSpecifiedTiming(entry.effect->SpecifiedTiming());
}
if (entry.animation->timeline() != entry.timeline) {
entry.animation->setTimeline(entry.timeline);
To<CSSAnimation>(*entry.animation).ResetIgnoreCSSTimeline();
}
running_animations_[entry.index]->Update(entry);
}
const Vector<wtf_size_t>& cancelled_indices =
pending_update_.CancelledAnimationIndices();
for (wtf_size_t i = cancelled_indices.size(); i-- > 0;) {
DCHECK(i == cancelled_indices.size() - 1 ||
cancelled_indices[i] < cancelled_indices[i + 1]);
Animation& animation =
*running_animations_[cancelled_indices[i]]->animation;
animation.ClearOwningElement();
if (animation.IsCSSAnimation() &&
!DynamicTo<CSSAnimation>(animation)->getIgnoreCSSPlayState())
animation.cancel();
animation.Update(kTimingUpdateOnDemand);
running_animations_.EraseAt(cancelled_indices[i]);
}
for (const auto& entry : pending_update_.NewAnimations()) {
const InertEffect* inert_animation = entry.effect.Get();
AnimationEventDelegate* event_delegate =
MakeGarbageCollected<AnimationEventDelegate>(element, entry.name);
auto* effect = MakeGarbageCollected<KeyframeEffect>(
element, inert_animation->Model(), inert_animation->SpecifiedTiming(),
KeyframeEffect::kDefaultPriority, event_delegate);
auto* animation = MakeGarbageCollected<CSSAnimation>(
element->GetExecutionContext(), entry.timeline, effect,
entry.position_index, entry.name);
animation->play();
if (inert_animation->Paused())
animation->pause();
animation->resetIgnoreCSSPlayState();
animation->Update(kTimingUpdateOnDemand);
running_animations_.push_back(
MakeGarbageCollected<RunningAnimation>(animation, entry));
}
// Track retargeted transitions that are running on the compositor in order
// to update their start times.
HashSet<PropertyHandle> retargeted_compositor_transitions;
for (const PropertyHandle& property :
pending_update_.CancelledTransitions()) {
DCHECK(transitions_.Contains(property));
Animation* animation = transitions_.Take(property)->animation;
auto* effect = To<KeyframeEffect>(animation->effect());
if (effect && effect->HasActiveAnimationsOnCompositor(property) &&
pending_update_.NewTransitions().find(property) !=
pending_update_.NewTransitions().end() &&
!animation->Limited()) {
retargeted_compositor_transitions.insert(property);
}
animation->ClearOwningElement();
animation->cancel();
// After cancellation, transitions must be downgraded or they'll fail
// to be considered when retriggering themselves. This can happen if
// the transition is captured through getAnimations then played.
effect = DynamicTo<KeyframeEffect>(animation->effect());
if (effect)
effect->DowngradeToNormal();
animation->Update(kTimingUpdateOnDemand);
}
for (const PropertyHandle& property : pending_update_.FinishedTransitions()) {
// This transition can also be cancelled and finished at the same time
if (transitions_.Contains(property)) {
Animation* animation = transitions_.Take(property)->animation;
// Transition must be downgraded
if (auto* effect = DynamicTo<KeyframeEffect>(animation->effect()))
effect->DowngradeToNormal();
}
}
if (!pending_update_.NewTransitions().IsEmpty()) {
element->GetDocument()
.GetDocumentAnimations()
.IncrementTrasitionGeneration();
}
for (const auto& entry : pending_update_.NewTransitions()) {
const CSSAnimationUpdate::NewTransition* new_transition = entry.value;
RunningTransition* running_transition =
MakeGarbageCollected<RunningTransition>();
running_transition->from = new_transition->from;
running_transition->to = new_transition->to;
running_transition->reversing_adjusted_start_value =
new_transition->reversing_adjusted_start_value;
running_transition->reversing_shortening_factor =
new_transition->reversing_shortening_factor;
const PropertyHandle& property = new_transition->property;
const InertEffect* inert_animation = new_transition->effect.Get();
TransitionEventDelegate* event_delegate =
MakeGarbageCollected<TransitionEventDelegate>(element, property);
KeyframeEffectModelBase* model = inert_animation->Model();
auto* transition_effect = MakeGarbageCollected<KeyframeEffect>(
element, model, inert_animation->SpecifiedTiming(),
KeyframeEffect::kTransitionPriority, event_delegate);
auto* animation = MakeGarbageCollected<CSSTransition>(
element->GetExecutionContext(), &(element->GetDocument().Timeline()),
transition_effect,
element->GetDocument().GetDocumentAnimations().TransitionGeneration(),
property);
animation->play();
// Set the current time as the start time for retargeted transitions
if (retargeted_compositor_transitions.Contains(property)) {
CSSNumberish current_time;
element->GetDocument().Timeline().currentTime(current_time);
animation->setStartTime(current_time);
}
animation->Update(kTimingUpdateOnDemand);
running_transition->animation = animation;
transitions_.Set(property, running_transition);
}
ClearPendingUpdate();
}
void CSSAnimations::CalculateTransitionUpdateForProperty(
TransitionUpdateState& state,
const PropertyHandle& property,
size_t transition_index) {
state.listed_properties.insert(property);
// FIXME: We should transition if an !important property changes even when an
// animation is running, but this is a bit hard to do with the current
// applyMatchedProperties system.
if (property.IsCSSCustomProperty()) {
if (state.update.ActiveInterpolationsForCustomAnimations().Contains(
property) ||
(state.animating_element->GetElementAnimations() &&
state.animating_element->GetElementAnimations()
->CssAnimations()
.previous_active_interpolations_for_custom_animations_.Contains(
property))) {
return;
}
} else if (state.update.ActiveInterpolationsForStandardAnimations().Contains(
property) ||
(state.animating_element->GetElementAnimations() &&
state.animating_element->GetElementAnimations()
->CssAnimations()
.previous_active_interpolations_for_standard_animations_
.Contains(property))) {
return;
}
const RunningTransition* interrupted_transition = nullptr;
if (state.active_transitions) {
TransitionMap::const_iterator active_transition_iter =
state.active_transitions->find(property);
if (active_transition_iter != state.active_transitions->end()) {
const RunningTransition* running_transition =
active_transition_iter->value;
if (ComputedValuesEqual(property, state.style, *running_transition->to)) {
if (!state.transition_data) {
if (!running_transition->animation->FinishedInternal()) {
UseCounter::Count(
state.animating_element->GetDocument(),
WebFeature::kCSSTransitionCancelledByRemovingStyle);
}
// TODO(crbug.com/934700): Add a return to this branch to correctly
// continue transitions under default settings (all 0s) in the absence
// of a change in base computed style.
} else {
return;
}
}
state.update.CancelTransition(property);
DCHECK(!state.animating_element->GetElementAnimations() ||
!state.animating_element->GetElementAnimations()
->IsAnimationStyleChange());
if (ComputedValuesEqual(
property, state.style,
*running_transition->reversing_adjusted_start_value)) {
interrupted_transition = running_transition;
}
}
}
// In the default configutation (transition: all 0s) we continue and cancel
// transitions but do not start them.
if (!state.transition_data)
return;
const PropertyRegistry* registry =
state.animating_element->GetDocument().GetPropertyRegistry();
if (property.IsCSSCustomProperty()) {
if (!registry || !registry->Registration(property.CustomPropertyName())) {
return;
}
}
// Lazy evaluation of the before change style. We only need to update where
// we are transitioning from if the final destination is changing.
if (!state.before_change_style) {
ElementAnimations* element_animations =
state.animating_element->GetElementAnimations();
if (element_animations) {
const ComputedStyle* base_style = element_animations->BaseComputedStyle();
if (base_style) {
state.before_change_style =
CalculateBeforeChangeStyle(state.animating_element, *base_style);
}
}
// Use the style from the previous frame if no base style is found.
// Elements that have not been animated will not have a base style.
// Elements that were previously animated, but where all previously running
// animations have stopped may also be missing a base style. In both cases,
// the old style is equivalent to the base computed style.
if (!state.before_change_style) {
state.before_change_style =
CalculateBeforeChangeStyle(state.animating_element, state.old_style);
}
}
if (ComputedValuesEqual(property, *state.before_change_style, state.style)) {
return;
}
CSSInterpolationTypesMap map(registry,
state.animating_element->GetDocument());
CSSInterpolationEnvironment old_environment(map, *state.before_change_style);
CSSInterpolationEnvironment new_environment(map, state.style);
const InterpolationType* transition_type = nullptr;
InterpolationValue start = nullptr;
InterpolationValue end = nullptr;
for (const auto& interpolation_type : map.Get(property)) {
start = interpolation_type->MaybeConvertUnderlyingValue(old_environment);
if (!start) {
continue;
}
end = interpolation_type->MaybeConvertUnderlyingValue(new_environment);
if (!end) {
continue;
}
// Merge will only succeed if the two values are considered interpolable.
if (interpolation_type->MaybeMergeSingles(start.Clone(), end.Clone())) {
transition_type = interpolation_type.get();
break;
}
}
// No smooth interpolation exists between these values so don't start a
// transition.
if (!transition_type) {
return;
}
// If we have multiple transitions on the same property, we will use the
// last one since we iterate over them in order.
Timing timing = state.transition_data->ConvertToTiming(transition_index);
// CSS Transitions always have a valid duration (i.e. the value 'auto' is not
// supported), so iteration_duration will always be set.
if (timing.start_delay + timing.iteration_duration->InSecondsF() <= 0) {
// We may have started a transition in a prior CSSTransitionData update,
// this CSSTransitionData update needs to override them.
// TODO(alancutter): Just iterate over the CSSTransitionDatas in reverse and
// skip any properties that have already been visited so we don't need to
// "undo" work like this.
state.update.UnstartTransition(property);
return;
}
const ComputedStyle* reversing_adjusted_start_value =
state.before_change_style.get();
double reversing_shortening_factor = 1;
if (interrupted_transition) {
AnimationEffect* effect = interrupted_transition->animation->effect();
const base::Optional<double> interrupted_progress =
effect ? effect->Progress() : base::nullopt;
if (interrupted_progress) {
reversing_adjusted_start_value = interrupted_transition->to.get();
reversing_shortening_factor =
clampTo((interrupted_progress.value() *
interrupted_transition->reversing_shortening_factor) +
(1 - interrupted_transition->reversing_shortening_factor),
0.0, 1.0);
timing.iteration_duration.value() *= reversing_shortening_factor;
if (timing.start_delay < 0) {
timing.start_delay *= reversing_shortening_factor;
}
}
}
TransitionKeyframeVector keyframes;
TransitionKeyframe* start_keyframe =
MakeGarbageCollected<TransitionKeyframe>(property);
start_keyframe->SetValue(std::make_unique<TypedInterpolationValue>(
*transition_type, start.interpolable_value->Clone(),
start.non_interpolable_value));
start_keyframe->SetOffset(0);
keyframes.push_back(start_keyframe);
TransitionKeyframe* end_keyframe =
MakeGarbageCollected<TransitionKeyframe>(property);
end_keyframe->SetValue(std::make_unique<TypedInterpolationValue>(
*transition_type, end.interpolable_value->Clone(),
end.non_interpolable_value));
end_keyframe->SetOffset(1);
keyframes.push_back(end_keyframe);
if (property.GetCSSProperty().IsCompositableProperty()) {
CompositorKeyframeValue* from = CompositorKeyframeValueFactory::Create(
property, *state.before_change_style, start_keyframe->Offset().value());
CompositorKeyframeValue* to = CompositorKeyframeValueFactory::Create(
property, state.style, end_keyframe->Offset().value());
start_keyframe->SetCompositorValue(from);
end_keyframe->SetCompositorValue(to);
}
auto* model = MakeGarbageCollected<TransitionKeyframeEffectModel>(keyframes);
if (!state.cloned_style) {
state.cloned_style = ComputedStyle::Clone(state.style);
}
state.update.StartTransition(
property, state.before_change_style, state.cloned_style,
reversing_adjusted_start_value, reversing_shortening_factor,
*MakeGarbageCollected<InertEffect>(model, timing, false,
AnimationTimeDelta(), base::nullopt));
DCHECK(!state.animating_element->GetElementAnimations() ||
!state.animating_element->GetElementAnimations()
->IsAnimationStyleChange());
}
void CSSAnimations::CalculateTransitionUpdateForCustomProperty(
TransitionUpdateState& state,
const CSSTransitionData::TransitionProperty& transition_property,
size_t transition_index) {
if (transition_property.property_type !=
CSSTransitionData::kTransitionUnknownProperty) {
return;
}
if (!CSSVariableParser::IsValidVariableName(
transition_property.property_string)) {
return;
}
CalculateTransitionUpdateForProperty(
state, PropertyHandle(transition_property.property_string),
transition_index);
}
void CSSAnimations::CalculateTransitionUpdateForStandardProperty(
TransitionUpdateState& state,
const CSSTransitionData::TransitionProperty& transition_property,
size_t transition_index,
const ComputedStyle& style) {
if (transition_property.property_type !=
CSSTransitionData::kTransitionKnownProperty) {
return;
}
CSSPropertyID resolved_id =
ResolveCSSPropertyID(transition_property.unresolved_property);
bool animate_all = resolved_id == CSSPropertyID::kAll;
const StylePropertyShorthand& property_list =
animate_all ? PropertiesForTransitionAll()
: shorthandForProperty(resolved_id);
// If not a shorthand we only execute one iteration of this loop, and
// refer to the property directly.
for (unsigned i = 0; !i || i < property_list.length(); ++i) {
CSSPropertyID longhand_id =
property_list.length() ? property_list.properties()[i]->PropertyID()
: resolved_id;
DCHECK_GE(longhand_id, kFirstCSSProperty);
const CSSProperty& property =
CSSProperty::Get(longhand_id)
.ResolveDirectionAwareProperty(style.Direction(),
style.GetWritingMode());
PropertyHandle property_handle = PropertyHandle(property);
if (!animate_all && !property.IsInterpolable()) {
continue;
}
CalculateTransitionUpdateForProperty(state, property_handle,
transition_index);
}
}
void CSSAnimations::CalculateTransitionUpdate(CSSAnimationUpdate& update,
PropertyPass property_pass,
Element* animating_element,
const ComputedStyle& style) {
if (!animating_element)
return;
if (animating_element->GetDocument().FinishingOrIsPrinting())
return;
ElementAnimations* element_animations =
animating_element->GetElementAnimations();
const TransitionMap* active_transitions =
element_animations ? &element_animations->CssAnimations().transitions_
: nullptr;
const CSSTransitionData* transition_data = style.Transitions();
const bool animation_style_recalc =
element_animations && element_animations->IsAnimationStyleChange();
HashSet<PropertyHandle> listed_properties;
bool any_transition_had_transition_all = false;
const ComputedStyle* old_style = animating_element->GetComputedStyle();
if (!animation_style_recalc && style.Display() != EDisplay::kNone &&
old_style && !old_style->IsEnsuredInDisplayNone()) {
TransitionUpdateState state = {update,
animating_element,
*old_style,
style,
/*before_change_style=*/nullptr,
/*cloned_style=*/nullptr,
active_transitions,
listed_properties,
transition_data};
if (transition_data) {
for (wtf_size_t transition_index = 0;
transition_index < transition_data->PropertyList().size();
++transition_index) {
const CSSTransitionData::TransitionProperty& transition_property =
transition_data->PropertyList()[transition_index];
if (transition_property.unresolved_property == CSSPropertyID::kAll) {
any_transition_had_transition_all = true;
}
if (property_pass == PropertyPass::kCustom) {
CalculateTransitionUpdateForCustomProperty(state, transition_property,
transition_index);
} else {
DCHECK_EQ(property_pass, PropertyPass::kStandard);
CalculateTransitionUpdateForStandardProperty(
state, transition_property, transition_index, style);
}
}
} else if (active_transitions && active_transitions->size()) {
// !transition_data implies transition: all 0s
any_transition_had_transition_all = true;
if (property_pass == PropertyPass::kStandard) {
CSSTransitionData::TransitionProperty default_property(
CSSPropertyID::kAll);
CalculateTransitionUpdateForStandardProperty(state, default_property, 0,
style);
}
}
}
if (active_transitions) {
for (const auto& entry : *active_transitions) {
const PropertyHandle& property = entry.key;
if (property.IsCSSCustomProperty() !=
(property_pass == PropertyPass::kCustom)) {
continue;
}
if (!any_transition_had_transition_all && !animation_style_recalc &&
!listed_properties.Contains(property)) {
update.CancelTransition(property);
} else if (entry.value->animation->FinishedInternal()) {
update.FinishTransition(property);
}
}
}
CalculateTransitionActiveInterpolations(update, property_pass,
animating_element);
}
scoped_refptr<const ComputedStyle> CSSAnimations::CalculateBeforeChangeStyle(
Element* animating_element,
const ComputedStyle& base_style) {
ActiveInterpolationsMap interpolations_map;
ElementAnimations* element_animations =
animating_element->GetElementAnimations();
if (element_animations) {
const TransitionMap& transition_map =
element_animations->CssAnimations().transitions_;
// Assemble list of animations in composite ordering.
// TODO(crbug.com/1082401): Per spec, the before change style should include
// all declarative animations. Currently, only including transitions.
HeapVector<Member<Animation>> animations;
for (const auto& entry : transition_map) {
RunningTransition* transition = entry.value;
Animation* animation = transition->animation;
animations.push_back(animation);
}
std::sort(animations.begin(), animations.end(),
[](Animation* a, Animation* b) {
return Animation::HasLowerCompositeOrdering(
a, b, Animation::CompareAnimationsOrdering::kPointerOrder);
});
// Sample animations and add to the interpolatzions map.
for (Animation* animation : animations) {
CSSNumberish current_time_numberish;
animation->currentTime(current_time_numberish);
if (current_time_numberish.IsNull())
continue;
// CSSNumericValue is not yet supported, verify that it is not used
DCHECK(!current_time_numberish.IsCSSNumericValue());
base::Optional<AnimationTimeDelta> current_time =
AnimationTimeDelta::FromMillisecondsD(
current_time_numberish.GetAsDouble());
auto* effect = DynamicTo<KeyframeEffect>(animation->effect());
if (!effect)
continue;
auto* inert_animation_for_sampling = MakeGarbageCollected<InertEffect>(
effect->Model(), effect->SpecifiedTiming(), false, current_time,
base::nullopt);
HeapVector<Member<Interpolation>> sample;
inert_animation_for_sampling->Sample(sample);
for (const auto& interpolation : sample) {
PropertyHandle handle = interpolation->GetProperty();
auto interpolation_map_entry = interpolations_map.insert(
handle, MakeGarbageCollected<ActiveInterpolations>());
auto& active_interpolations =
*interpolation_map_entry.stored_value->value;
if (!interpolation->DependsOnUnderlyingValue())
active_interpolations.clear();
active_interpolations.push_back(interpolation);
}
}
}
StyleResolver& resolver = animating_element->GetDocument().GetStyleResolver();
return resolver.BeforeChangeStyleForTransitionUpdate(
*animating_element, base_style, interpolations_map);
}
void CSSAnimations::Cancel() {
for (const auto& running_animation : running_animations_) {
running_animation->animation->cancel();
running_animation->animation->Update(kTimingUpdateOnDemand);
}
for (const auto& entry : transitions_) {
entry.value->animation->cancel();
entry.value->animation->Update(kTimingUpdateOnDemand);
}
running_animations_.clear();
transitions_.clear();
ClearPendingUpdate();
}
namespace {
bool IsCustomPropertyHandle(const PropertyHandle& property) {
return property.IsCSSCustomProperty();
}
bool IsFontAffectingPropertyHandle(const PropertyHandle& property) {
if (property.IsCSSCustomProperty() || !property.IsCSSProperty())
return false;
return property.GetCSSProperty().AffectsFont();
}
// TODO(alancutter): CSS properties and presentation attributes may have
// identical effects. By grouping them in the same set we introduce a bug where
// arbitrary hash iteration will determine the order the apply in and thus which
// one "wins". We should be more deliberate about the order of application in
// the case of effect collisions.
// Example: Both 'color' and 'svg-color' set the color on ComputedStyle but are
// considered distinct properties in the ActiveInterpolationsMap.
bool IsStandardPropertyHandle(const PropertyHandle& property) {
return (property.IsCSSProperty() && !property.IsCSSCustomProperty()) ||
property.IsPresentationAttribute();
}
void AdoptActiveAnimationInterpolations(
EffectStack* effect_stack,
CSSAnimationUpdate& update,
const HeapVector<Member<const InertEffect>>* new_animations,
const HeapHashSet<Member<const Animation>>* suppressed_animations) {
ActiveInterpolationsMap custom_interpolations(
EffectStack::ActiveInterpolations(
effect_stack, new_animations, suppressed_animations,
KeyframeEffect::kDefaultPriority, IsCustomPropertyHandle));
update.AdoptActiveInterpolationsForCustomAnimations(custom_interpolations);
ActiveInterpolationsMap standard_interpolations(
EffectStack::ActiveInterpolations(
effect_stack, new_animations, suppressed_animations,
KeyframeEffect::kDefaultPriority, IsStandardPropertyHandle));
update.AdoptActiveInterpolationsForStandardAnimations(
standard_interpolations);
}
} // namespace
void CSSAnimations::CalculateAnimationActiveInterpolations(
CSSAnimationUpdate& update,
const Element* animating_element) {
ElementAnimations* element_animations =
animating_element ? animating_element->GetElementAnimations() : nullptr;
EffectStack* effect_stack =
element_animations ? &element_animations->GetEffectStack() : nullptr;
if (update.NewAnimations().IsEmpty() &&
update.SuppressedAnimations().IsEmpty()) {
AdoptActiveAnimationInterpolations(effect_stack, update, nullptr, nullptr);
return;
}
HeapVector<Member<const InertEffect>> new_effects;
for (const auto& new_animation : update.NewAnimations())
new_effects.push_back(new_animation.effect);
// Animations with updates use a temporary InertEffect for the current frame.
for (const auto& updated_animation : update.AnimationsWithUpdates())
new_effects.push_back(updated_animation.effect);
AdoptActiveAnimationInterpolations(effect_stack, update, &new_effects,
&update.SuppressedAnimations());
}
namespace {
EffectStack::PropertyHandleFilter PropertyFilter(
CSSAnimations::PropertyPass property_pass) {
if (property_pass == CSSAnimations::PropertyPass::kCustom) {
return IsCustomPropertyHandle;
}
DCHECK_EQ(property_pass, CSSAnimations::PropertyPass::kStandard);
return IsStandardPropertyHandle;
}
} // namespace
void CSSAnimations::CalculateTransitionActiveInterpolations(
CSSAnimationUpdate& update,
PropertyPass property_pass,
const Element* animating_element) {
ElementAnimations* element_animations =
animating_element ? animating_element->GetElementAnimations() : nullptr;
EffectStack* effect_stack =
element_animations ? &element_animations->GetEffectStack() : nullptr;
ActiveInterpolationsMap active_interpolations_for_transitions;
if (update.NewTransitions().IsEmpty() &&
update.CancelledTransitions().IsEmpty()) {
active_interpolations_for_transitions = EffectStack::ActiveInterpolations(
effect_stack, nullptr, nullptr, KeyframeEffect::kTransitionPriority,
PropertyFilter(property_pass));
} else {
HeapVector<Member<const InertEffect>> new_transitions;
for (const auto& entry : update.NewTransitions())
new_transitions.push_back(entry.value->effect.Get());
HeapHashSet<Member<const Animation>> cancelled_animations;
if (!update.CancelledTransitions().IsEmpty()) {
DCHECK(element_animations);
const TransitionMap& transition_map =
element_animations->CssAnimations().transitions_;
for (const PropertyHandle& property : update.CancelledTransitions()) {
DCHECK(transition_map.Contains(property));
cancelled_animations.insert(
transition_map.at(property)->animation.Get());
}
}
active_interpolations_for_transitions = EffectStack::ActiveInterpolations(
effect_stack, &new_transitions, &cancelled_animations,
KeyframeEffect::kTransitionPriority, PropertyFilter(property_pass));
}
const ActiveInterpolationsMap& animations =
property_pass == PropertyPass::kCustom
? update.ActiveInterpolationsForCustomAnimations()
: update.ActiveInterpolationsForStandardAnimations();
// Properties being animated by animations don't get values from transitions
// applied.
if (!animations.IsEmpty() &&
!active_interpolations_for_transitions.IsEmpty()) {
for (const auto& entry : animations)
active_interpolations_for_transitions.erase(entry.key);
}
if (property_pass == PropertyPass::kCustom) {
update.AdoptActiveInterpolationsForCustomTransitions(
active_interpolations_for_transitions);
} else {
DCHECK_EQ(property_pass, PropertyPass::kStandard);
update.AdoptActiveInterpolationsForStandardTransitions(
active_interpolations_for_transitions);
}
}
EventTarget* CSSAnimations::AnimationEventDelegate::GetEventTarget() const {
return &EventPath::EventTargetRespectingTargetRules(*animation_target_);
}
void CSSAnimations::AnimationEventDelegate::MaybeDispatch(
Document::ListenerType listener_type,
const AtomicString& event_name,
const AnimationTimeDelta& elapsed_time) {
if (animation_target_->GetDocument().HasListenerType(listener_type)) {
String pseudo_element_name = PseudoElement::PseudoElementNameForEvents(
animation_target_->GetPseudoId());
AnimationEvent* event = AnimationEvent::Create(
event_name, name_, elapsed_time, pseudo_element_name);
event->SetTarget(GetEventTarget());
GetDocument().EnqueueAnimationFrameEvent(event);
}
}
bool CSSAnimations::AnimationEventDelegate::RequiresIterationEvents(
const AnimationEffect& animation_node) {
return GetDocument().HasListenerType(Document::kAnimationIterationListener);
}
void CSSAnimations::AnimationEventDelegate::OnEventCondition(
const AnimationEffect& animation_node,
Timing::Phase current_phase) {
const base::Optional<double> current_iteration =
animation_node.CurrentIteration();
// See http://drafts.csswg.org/css-animations-2/#event-dispatch
// When multiple events are dispatched for a single phase transition,
// the animationstart event is to be dispatched before the animationend
// event.
// The following phase transitions trigger an animationstart event:
// idle or before --> active or after
// after --> active or before
const bool phase_change = previous_phase_ != current_phase;
const bool was_idle_or_before = (previous_phase_ == Timing::kPhaseNone ||
previous_phase_ == Timing::kPhaseBefore);
const bool is_active_or_after = (current_phase == Timing::kPhaseActive ||
current_phase == Timing::kPhaseAfter);
const bool is_active_or_before = (current_phase == Timing::kPhaseActive ||
current_phase == Timing::kPhaseBefore);
const bool was_after = (previous_phase_ == Timing::kPhaseAfter);
if (phase_change && ((was_idle_or_before && is_active_or_after) ||
(was_after && is_active_or_before))) {
AnimationTimeDelta elapsed_time =
was_after ? IntervalEnd(animation_node) : IntervalStart(animation_node);
MaybeDispatch(Document::kAnimationStartListener,
event_type_names::kAnimationstart, elapsed_time);
}
// The following phase transitions trigger an animationend event:
// idle, before or active--> after
// active or after--> before
const bool was_active_or_after = (previous_phase_ == Timing::kPhaseActive ||
previous_phase_ == Timing::kPhaseAfter);
const bool is_after = (current_phase == Timing::kPhaseAfter);
const bool is_before = (current_phase == Timing::kPhaseBefore);
if (phase_change && (is_after || (was_active_or_after && is_before))) {
AnimationTimeDelta elapsed_time =
is_after ? IntervalEnd(animation_node) : IntervalStart(animation_node);
MaybeDispatch(Document::kAnimationEndListener,
event_type_names::kAnimationend, elapsed_time);
}
// The following phase transitions trigger an animationcalcel event:
// not idle and not after --> idle
if (phase_change && current_phase == Timing::kPhaseNone &&
previous_phase_ != Timing::kPhaseAfter) {
// TODO(crbug.com/1059968): Determine if animation direction or playback
// rate factor into the calculation of the elapsed time.
AnimationTimeDelta cancel_time = animation_node.GetCancelTime();
MaybeDispatch(Document::kAnimationCancelListener,
event_type_names::kAnimationcancel, cancel_time);
}
if (!phase_change && current_phase == Timing::kPhaseActive &&
previous_iteration_ != current_iteration) {
// We fire only a single event for all iterations that terminate
// between a single pair of samples. See http://crbug.com/275263. For
// compatibility with the existing implementation, this event uses
// the elapsedTime for the first iteration in question.
DCHECK(previous_iteration_ && current_iteration);
const AnimationTimeDelta elapsed_time =
IterationElapsedTime(animation_node, previous_iteration_.value());
MaybeDispatch(Document::kAnimationIterationListener,
event_type_names::kAnimationiteration, elapsed_time);
}
previous_iteration_ = current_iteration;
previous_phase_ = current_phase;
}
void CSSAnimations::AnimationEventDelegate::Trace(Visitor* visitor) const {
visitor->Trace(animation_target_);
AnimationEffect::EventDelegate::Trace(visitor);
}
EventTarget* CSSAnimations::TransitionEventDelegate::GetEventTarget() const {
return &EventPath::EventTargetRespectingTargetRules(*transition_target_);
}
void CSSAnimations::TransitionEventDelegate::OnEventCondition(
const AnimationEffect& animation_node,
Timing::Phase current_phase) {
if (current_phase == previous_phase_)
return;
if (GetDocument().HasListenerType(Document::kTransitionRunListener)) {
if (previous_phase_ == Timing::kPhaseNone) {
EnqueueEvent(
event_type_names::kTransitionrun,
StartTimeFromDelay(animation_node.SpecifiedTiming().start_delay));
}
}
if (GetDocument().HasListenerType(Document::kTransitionStartListener)) {
if ((current_phase == Timing::kPhaseActive ||
current_phase == Timing::kPhaseAfter) &&
(previous_phase_ == Timing::kPhaseNone ||
previous_phase_ == Timing::kPhaseBefore)) {
EnqueueEvent(
event_type_names::kTransitionstart,
StartTimeFromDelay(animation_node.SpecifiedTiming().start_delay));
} else if ((current_phase == Timing::kPhaseActive ||
current_phase == Timing::kPhaseBefore) &&
previous_phase_ == Timing::kPhaseAfter) {
// If the transition is progressing backwards it is considered to have
// started at the end position.
EnqueueEvent(event_type_names::kTransitionstart,
animation_node.SpecifiedTiming().IterationDuration());
}
}
if (GetDocument().HasListenerType(Document::kTransitionEndListener)) {
if (current_phase == Timing::kPhaseAfter &&
(previous_phase_ == Timing::kPhaseActive ||
previous_phase_ == Timing::kPhaseBefore ||
previous_phase_ == Timing::kPhaseNone)) {
EnqueueEvent(event_type_names::kTransitionend,
animation_node.SpecifiedTiming().IterationDuration());
} else if (current_phase == Timing::kPhaseBefore &&
(previous_phase_ == Timing::kPhaseActive ||
previous_phase_ == Timing::kPhaseAfter)) {
// If the transition is progressing backwards it is considered to have
// ended at the start position.
EnqueueEvent(
event_type_names::kTransitionend,
StartTimeFromDelay(animation_node.SpecifiedTiming().start_delay));
}
}
if (GetDocument().HasListenerType(Document::kTransitionCancelListener)) {
if (current_phase == Timing::kPhaseNone &&
previous_phase_ != Timing::kPhaseAfter) {
// Per the css-transitions-2 spec, transitioncancel is fired with the
// "active time of the animation at the moment it was cancelled,
// calculated using a fill mode of both".
base::Optional<AnimationTimeDelta> cancel_active_time =
CalculateActiveTime(animation_node.SpecifiedTiming().ActiveDuration(),
Timing::FillMode::BOTH,
animation_node.LocalTime(), previous_phase_,
animation_node.SpecifiedTiming());
// Being the FillMode::BOTH the only possibility to get a null
// cancel_active_time is that previous_phase_ is kPhaseNone. This cannot
// happen because we know that current_phase == kPhaseNone and
// current_phase != previous_phase_ (see early return at the beginning).
DCHECK(cancel_active_time);
EnqueueEvent(event_type_names::kTransitioncancel,
cancel_active_time.value());
}
}
previous_phase_ = current_phase;
}
void CSSAnimations::TransitionEventDelegate::EnqueueEvent(
const WTF::AtomicString& type,
const AnimationTimeDelta& elapsed_time) {
String property_name =
property_.IsCSSCustomProperty()
? property_.CustomPropertyName()
: property_.GetCSSProperty().GetPropertyNameString();
String pseudo_element =
PseudoElement::PseudoElementNameForEvents(GetPseudoId());
TransitionEvent* event = TransitionEvent::Create(
type, property_name, elapsed_time, pseudo_element);
event->SetTarget(GetEventTarget());
GetDocument().EnqueueAnimationFrameEvent(event);
}
void CSSAnimations::TransitionEventDelegate::Trace(Visitor* visitor) const {
visitor->Trace(transition_target_);
AnimationEffect::EventDelegate::Trace(visitor);
}
const StylePropertyShorthand& CSSAnimations::PropertiesForTransitionAll() {
DEFINE_STATIC_LOCAL(Vector<const CSSProperty*>, properties, ());
DEFINE_STATIC_LOCAL(StylePropertyShorthand, property_shorthand, ());
if (properties.IsEmpty()) {
for (CSSPropertyID id : CSSPropertyIDList()) {
// Avoid creating overlapping transitions with perspective-origin and
// transition-origin.
if (id == CSSPropertyID::kWebkitPerspectiveOriginX ||
id == CSSPropertyID::kWebkitPerspectiveOriginY ||
id == CSSPropertyID::kWebkitTransformOriginX ||
id == CSSPropertyID::kWebkitTransformOriginY ||
id == CSSPropertyID::kWebkitTransformOriginZ)
continue;
const CSSProperty& property = CSSProperty::Get(id);
if (property.IsInterpolable())
properties.push_back(&property);
}
property_shorthand = StylePropertyShorthand(
CSSPropertyID::kInvalid, properties.begin(), properties.size());
}
return property_shorthand;
}
// Properties that affect animations are not allowed to be affected by
// animations. https://drafts.csswg.org/web-animations/#not-animatable-section
bool CSSAnimations::IsAnimationAffectingProperty(const CSSProperty& property) {
switch (property.PropertyID()) {
case CSSPropertyID::kAnimation:
case CSSPropertyID::kAnimationDelay:
case CSSPropertyID::kAnimationDirection:
case CSSPropertyID::kAnimationDuration:
case CSSPropertyID::kAnimationFillMode:
case CSSPropertyID::kAnimationIterationCount:
case CSSPropertyID::kAnimationName:
case CSSPropertyID::kAnimationPlayState:
case CSSPropertyID::kAnimationTimeline:
case CSSPropertyID::kAnimationTimingFunction:
case CSSPropertyID::kContentVisibility:
case CSSPropertyID::kContain:
case CSSPropertyID::kDirection:
case CSSPropertyID::kDisplay:
case CSSPropertyID::kTextCombineUpright:
case CSSPropertyID::kTextOrientation:
case CSSPropertyID::kTransition:
case CSSPropertyID::kTransitionDelay:
case CSSPropertyID::kTransitionDuration:
case CSSPropertyID::kTransitionProperty:
case CSSPropertyID::kTransitionTimingFunction:
case CSSPropertyID::kUnicodeBidi:
case CSSPropertyID::kWebkitWritingMode:
case CSSPropertyID::kWillChange:
case CSSPropertyID::kWritingMode:
return true;
default:
return false;
}
}
bool CSSAnimations::IsAffectedByKeyframesFromScope(
const Element& element,
const TreeScope& tree_scope) {
// Animated elements are affected by @keyframes rules from the same scope
// and from their shadow sub-trees if they are shadow hosts.
if (element.GetTreeScope() == tree_scope)
return true;
if (!IsShadowHost(element))
return false;
if (tree_scope.RootNode() == tree_scope.GetDocument())
return false;
return To<ShadowRoot>(tree_scope.RootNode()).host() == element;
}
bool CSSAnimations::IsAnimatingCustomProperties(
const ElementAnimations* element_animations) {
return element_animations &&
element_animations->GetEffectStack().AffectsProperties(
IsCustomPropertyHandle);
}
bool CSSAnimations::IsAnimatingStandardProperties(
const ElementAnimations* element_animations,
const CSSBitset* bitset,
KeyframeEffect::Priority priority) {
if (!element_animations || !bitset)
return false;
return element_animations->GetEffectStack().AffectsProperties(*bitset,
priority);
}
bool CSSAnimations::IsAnimatingFontAffectingProperties(
const ElementAnimations* element_animations) {
return element_animations &&
element_animations->GetEffectStack().AffectsProperties(
IsFontAffectingPropertyHandle);
}
bool CSSAnimations::IsAnimatingRevert(
const ElementAnimations* element_animations) {
return element_animations && element_animations->GetEffectStack().HasRevert();
}
void CSSAnimations::Trace(Visitor* visitor) const {
visitor->Trace(transitions_);
visitor->Trace(pending_update_);
visitor->Trace(running_animations_);
visitor->Trace(previous_active_interpolations_for_standard_animations_);
visitor->Trace(previous_active_interpolations_for_custom_animations_);
}
} // namespace blink