| /* |
| * 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 <tuple> |
| |
| #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/bindings/core/v8/v8_union_cssnumericvalue_double.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/timing.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/post_style_update_scope.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/properties/css_property_ref.h" |
| #include "third_party/blink/renderer/core/css/properties/longhands.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/layout_tree_builder_traversal.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/garbage_collected.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<CSSPropertyName>; |
| |
| 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, |
| AnimationTimeline* timeline, |
| bool& has_named_range_keyframes) { |
| 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<KeyframeOffset>& offsets = style_keyframe->Keys(); |
| DCHECK(!offsets.empty()); |
| bool drop_keyframe = false; |
| // If keyframe doesn't have a named range offset, act as before, we don't |
| // care if we have a timeline at this point or not in this case. |
| if (offsets[0].name == Timing::TimelineNamedRange::kNone) { |
| keyframe->SetOffset(offsets[0].percent); |
| } else { |
| // No matter what the timeline is, we have named range keyframes. |
| has_named_range_keyframes = true; |
| |
| if (timeline && timeline->IsViewTimeline()) { |
| Timing::TimelineOffset timeline_offset( |
| offsets[0].name, Length::Percent(100 * offsets[0].percent)); |
| double fractional_offset = |
| To<ViewTimeline>(timeline)->ToFractionalOffset(timeline_offset); |
| keyframe->SetOffset(fractional_offset); |
| } else { |
| // This happens when you have a DocumentTimeline/ScrollTimeline with |
| // Named Range keyframes, and also sometimes when you have a |
| // ViewTimeline, the first time ProcessKeyframesRule is called, timeline |
| // does not exist yet. |
| drop_keyframe = true; |
| } |
| } |
| |
| keyframe->SetEasing(default_timing_function); |
| const CSSPropertyValueSet& properties = style_keyframe->Properties(); |
| for (unsigned j = 0; j < properties.PropertyCount(); j++) { |
| CSSPropertyValueSet::PropertyReference property_reference = |
| properties.PropertyAt(j); |
| CSSPropertyRef ref(property_reference.Name(), document); |
| const CSSProperty& property = ref.GetProperty(); |
| if (property.PropertyID() == CSSPropertyID::kAnimationTimingFunction) { |
| const CSSValue& value = property_reference.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); |
| const CSSPropertyName& name = physical_property.GetCSSPropertyName(); |
| keyframe->SetCSSPropertyValue(name, property_reference.Value()); |
| } |
| } |
| if (!drop_keyframe) { |
| keyframes.push_back(keyframe); |
| } |
| // The last keyframe specified at a given offset is used. |
| for (wtf_size_t j = 1; j < offsets.size(); ++j) { |
| if (offsets[j].name == Timing::TimelineNamedRange::kNone) { |
| keyframes.push_back( |
| To<StringKeyframe>(keyframe->CloneWithOffset(offsets[j].percent))); |
| } else { |
| has_named_range_keyframes = true; |
| if (timeline && timeline->IsViewTimeline()) { |
| Timing::TimelineOffset timeline_offset( |
| offsets[j].name, Length::Percent(100 * offsets[j].percent)); |
| double fractional_offset = |
| To<ViewTimeline>(timeline)->ToFractionalOffset(timeline_offset); |
| keyframes.push_back( |
| To<StringKeyframe>(keyframe->CloneWithOffset(fractional_offset))); |
| } |
| } |
| } |
| } |
| |
| 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. |
| absl::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 absl::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, |
| Element& element, |
| const Element& animating_element, |
| WritingDirectionMode writing_direction, |
| const ComputedStyle* parent_style, |
| const AtomicString& name, |
| TimingFunction* default_timing_function, |
| size_t animation_index, |
| AnimationTimeline* timeline) { |
| // 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, &animating_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: |
| bool has_named_range_keyframes = false; |
| keyframes = ProcessKeyframesRule( |
| keyframes_rule, element.GetDocument(), parent_style, |
| default_timing_function, writing_direction.GetWritingMode(), |
| writing_direction.Direction(), timeline, has_named_range_keyframes); |
| |
| 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. |
| absl::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()) { |
| CSSPropertyName property_name = property.GetCSSPropertyName(); |
| |
| // Since processing keyframes in reverse order, skipping properties that |
| // have already been inserted prevents overwriting a later merged |
| // keyframe. |
| if (current_offset_properties.Contains(property_name)) |
| continue; |
| |
| if (source_index != target_index) { |
| keyframe->SetCSSPropertyValue( |
| property.GetCSSPropertyName(), |
| rule_keyframe->CssPropertyValue(property)); |
| } |
| |
| current_offset_properties.insert(property_name); |
| animated_properties.insert(property_name); |
| if (keyframe_offset == 0) |
| start_properties.insert(property_name); |
| else if (keyframe_offset == 1) |
| end_properties.insert(property_name); |
| } |
| } |
| |
| // 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.empty() ? 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(), |
| has_named_range_keyframes); |
| 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(AnimationTimeDelta start_delay) { |
| return start_delay < AnimationTimeDelta() ? -start_delay |
| : AnimationTimeDelta(); |
| } |
| |
| // Timing functions for computing elapsed time of an event. |
| |
| AnimationTimeDelta IntervalStart(const AnimationEffect& effect) { |
| AnimationTimeDelta start_delay = effect.NormalizedTiming().start_delay; |
| const AnimationTimeDelta active_duration = |
| effect.NormalizedTiming().active_duration; |
| // This fixes a problem where start_delay could be -0 |
| if (!start_delay.is_zero()) { |
| start_delay = -start_delay; |
| } |
| return std::max(std::min(start_delay, active_duration), AnimationTimeDelta()); |
| } |
| |
| AnimationTimeDelta IntervalEnd(const AnimationEffect& effect) { |
| const AnimationTimeDelta start_delay = effect.NormalizedTiming().start_delay; |
| const AnimationTimeDelta end_delay = effect.NormalizedTiming().end_delay; |
| const AnimationTimeDelta active_duration = |
| effect.NormalizedTiming().active_duration; |
| const AnimationTimeDelta target_effect_end = |
| std::max(start_delay + active_duration + end_delay, AnimationTimeDelta()); |
| return std::max(std::min(target_effect_end - start_delay, active_duration), |
| AnimationTimeDelta()); |
| } |
| |
| 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.NormalizedTiming().iteration_duration; |
| return iteration_duration * (iteration_boundary - iteration_start); |
| } |
| |
| const CSSAnimationUpdate* GetPendingAnimationUpdate(Node& node) { |
| Element* element = DynamicTo<Element>(node); |
| if (!element) |
| return nullptr; |
| ElementAnimations* element_animations = element->GetElementAnimations(); |
| if (!element_animations) |
| return nullptr; |
| return &element_animations->CssAnimations().PendingUpdate(); |
| } |
| |
| // SpecifiedTimelines "zips" together name/axis/inset vectors such that |
| // individual name/axis/inset values can be accessed as a tuple. |
| // |
| // SpecifiedTimelines skips over entries with nullptr-names (which |
| // represents "none"), because such entries should not yield timelines. |
| class SpecifiedTimelines { |
| STACK_ALLOCATED(); |
| |
| public: |
| explicit SpecifiedTimelines(const ScopedCSSNameList* names, |
| const Vector<TimelineAxis>& axes, |
| const Vector<TimelineInset>& insets) |
| : names_(names ? &names->GetNames() : nullptr), |
| axes_(axes), |
| insets_(insets) {} |
| |
| class Iterator { |
| STACK_ALLOCATED(); |
| |
| public: |
| Iterator(wtf_size_t index, const SpecifiedTimelines& timelines) |
| : index_(index), timelines_(timelines) {} |
| |
| std::tuple<Member<const ScopedCSSName>, TimelineAxis, TimelineInset> |
| operator*() const { |
| const HeapVector<Member<const ScopedCSSName>>& names = *timelines_.names_; |
| const Vector<TimelineAxis>& axes = timelines_.axes_; |
| const Vector<TimelineInset>& insets = timelines_.insets_; |
| |
| Member<const ScopedCSSName> name = names[index_]; |
| TimelineAxis axis = axes.empty() |
| ? TimelineAxis::kBlock |
| : axes[std::min(index_, axes.size() - 1)]; |
| const TimelineInset& inset = |
| insets.empty() ? TimelineInset() |
| : insets[std::min(index_, insets.size() - 1)]; |
| |
| return std::make_tuple(name, axis, inset); |
| } |
| |
| void operator++() { index_ = timelines_.SkipPastNullptr(index_ + 1); } |
| |
| bool operator==(const Iterator& o) const { return index_ == o.index_; } |
| bool operator!=(const Iterator& o) const { return index_ != o.index_; } |
| |
| private: |
| wtf_size_t index_; |
| const SpecifiedTimelines& timelines_; |
| }; |
| |
| Iterator begin() const { return Iterator(SkipPastNullptr(0), *this); } |
| |
| Iterator end() const { return Iterator(Size(), *this); } |
| |
| private: |
| wtf_size_t Size() const { return names_ ? names_->size() : 0; } |
| |
| wtf_size_t SkipPastNullptr(wtf_size_t start) const { |
| wtf_size_t size = Size(); |
| wtf_size_t index = start; |
| DCHECK_LE(index, size); |
| while (index < size && !(*names_)[index]) { |
| ++index; |
| } |
| return index; |
| } |
| |
| const HeapVector<Member<const ScopedCSSName>>* names_; |
| const Vector<TimelineAxis>& axes_; |
| const Vector<TimelineInset>& insets_; |
| }; |
| |
| class SpecifiedViewTimelines : public SpecifiedTimelines { |
| STACK_ALLOCATED(); |
| |
| public: |
| explicit SpecifiedViewTimelines(const ComputedStyleBuilder& style_builder) |
| : SpecifiedTimelines(style_builder.ViewTimelineName(), |
| style_builder.ViewTimelineAxis(), |
| style_builder.ViewTimelineInset()) {} |
| }; |
| |
| // When calculating timeline updates, we initially assume that all timelines |
| // are going to be removed, and then erase the nullptr entries for timelines |
| // where we discover that this doesn't apply. |
| template <typename TimelineType> |
| CSSTimelineMap<TimelineType> NullifyExistingTimelines( |
| const CSSTimelineMap<TimelineType>* existing_timelines) { |
| CSSTimelineMap<TimelineType> map; |
| if (existing_timelines) { |
| for (const Member<const ScopedCSSName>& name : existing_timelines->Keys()) { |
| map.Set(name, nullptr); |
| } |
| } |
| return map; |
| } |
| |
| template <typename TimelineType> |
| TimelineType* GetTimeline(const CSSTimelineMap<TimelineType>* timelines, |
| const ScopedCSSName& name) { |
| if (!timelines) { |
| return nullptr; |
| } |
| auto i = timelines->find(&name); |
| return i != timelines->end() ? i->value.Get() : nullptr; |
| } |
| |
| } // namespace |
| |
| void CSSAnimations::CalculateScrollTimelineUpdate( |
| CSSAnimationUpdate& update, |
| Element& animating_element, |
| const ComputedStyleBuilder& style_builder) { |
| Document& document = animating_element.GetDocument(); |
| |
| const ScopedCSSName* name = style_builder.ScrollTimelineName(); |
| TimelineAxis axis = style_builder.ScrollTimelineAxis(); |
| |
| const CSSAnimations::TimelineData* timeline_data = |
| GetTimelineData(animating_element); |
| |
| CSSScrollTimeline* existing_timeline = |
| timeline_data ? timeline_data->GetScrollTimeline() : nullptr; |
| CSSScrollTimeline* new_timeline = nullptr; |
| |
| if (name) { |
| // If the computed values of scroll-timeline-* would produce a |
| // CSSScrollTimeline identical to the existing one, we reuse the existing |
| // one instead. |
| CSSScrollTimeline::Options options(document, |
| ScrollTimeline::ReferenceType::kSource, |
| &animating_element, *name, axis); |
| // No need to change the timeline if it would be the same as the previous. |
| if (existing_timeline && existing_timeline->Matches(document, options)) |
| return; |
| new_timeline = |
| MakeGarbageCollected<CSSScrollTimeline>(&document, std::move(options)); |
| // It is not allowed for a style update 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. |
| new_timeline->ServiceAnimations(kTimingUpdateOnDemand); |
| } |
| |
| if (new_timeline != existing_timeline) |
| update.SetChangedScrollTimeline(new_timeline); |
| } |
| |
| void CSSAnimations::CalculateViewTimelineUpdate( |
| CSSAnimationUpdate& update, |
| Element& animating_element, |
| const ComputedStyleBuilder& style_builder) { |
| const CSSAnimations::TimelineData* timeline_data = |
| GetTimelineData(animating_element); |
| const CSSViewTimelineMap* existing_view_timelines = |
| (timeline_data && !timeline_data->GetViewTimelines().empty()) |
| ? &timeline_data->GetViewTimelines() |
| : nullptr; |
| if (style_builder.ViewTimelineName() || existing_view_timelines) { |
| update.SetChangedViewTimelines(CalculateChangedViewTimelines( |
| animating_element, existing_view_timelines, style_builder)); |
| } |
| } |
| |
| CSSViewTimelineMap CSSAnimations::CalculateChangedViewTimelines( |
| Element& animating_element, |
| const CSSViewTimelineMap* existing_view_timelines, |
| const ComputedStyleBuilder& style_builder) { |
| CSSViewTimelineMap changed_timelines = |
| NullifyExistingTimelines(existing_view_timelines); |
| |
| for (auto [name, axis, inset] : SpecifiedViewTimelines(style_builder)) { |
| CSSViewTimeline* existing_timeline = |
| GetTimeline(existing_view_timelines, *name); |
| CSSViewTimeline::Options options(&animating_element, axis, inset); |
| if (existing_timeline && existing_timeline->Matches(options)) { |
| changed_timelines.erase(name); |
| continue; |
| } |
| CSSViewTimeline* new_timeline = MakeGarbageCollected<CSSViewTimeline>( |
| &animating_element.GetDocument(), std::move(options)); |
| new_timeline->ServiceAnimations(kTimingUpdateOnDemand); |
| changed_timelines.Set(name, new_timeline); |
| } |
| |
| return changed_timelines; |
| } |
| |
| const CSSAnimations::TimelineData* CSSAnimations::GetTimelineData( |
| const Element& element) { |
| const ElementAnimations* element_animations = element.GetElementAnimations(); |
| return element_animations |
| ? &element_animations->CssAnimations().timeline_data_ |
| : nullptr; |
| } |
| |
| namespace { |
| |
| // Assuming that `inner` is an inclusive descendant of `outer`, returns |
| // the distance (in the number of TreeScopes) between `inner` and `outer`. |
| // |
| // Returns std::numeric_limits::max() if `inner` is not an inclusive |
| // descendant of `outer`. |
| size_t TreeScopeDistance(const TreeScope* outer, const TreeScope* inner) { |
| size_t distance = 0; |
| |
| const TreeScope* current = inner; |
| |
| do { |
| if (current == outer) { |
| return distance; |
| } |
| ++distance; |
| } while (current && (current = current->ParentTreeScope())); |
| |
| return std::numeric_limits<size_t>::max(); |
| } |
| |
| // Update the matching timeline if the candidate is a more proximate match |
| // than the existing match. |
| template <typename TimelineType> |
| void UpdateMatchingTimeline(const ScopedCSSName& target_name, |
| const ScopedCSSName& candidate_name, |
| TimelineType* candidate, |
| TimelineType*& matching_timeline, |
| size_t& matching_distance) { |
| if (target_name.GetName() != candidate_name.GetName()) { |
| return; |
| } |
| if (RuntimeEnabledFeatures::CSSTreeScopedTimelinesEnabled()) { |
| size_t distance = TreeScopeDistance(candidate_name.GetTreeScope(), |
| target_name.GetTreeScope()); |
| if (distance < matching_distance) { |
| matching_timeline = candidate; |
| matching_distance = distance; |
| } |
| } else { |
| matching_timeline = candidate; |
| } |
| } |
| |
| } // namespace |
| |
| ScrollTimeline* CSSAnimations::FindTimelineForNode( |
| const ScopedCSSName& name, |
| Node* node, |
| const CSSAnimationUpdate* update) { |
| Element* element = DynamicTo<Element>(node); |
| if (!element) |
| return nullptr; |
| const TimelineData* timeline_data = GetTimelineData(*element); |
| if (CSSViewTimeline* timeline = |
| FindViewTimelineForElement(name, update, timeline_data)) { |
| return timeline; |
| } |
| return FindScrollTimelineForElement(name, update, timeline_data); |
| } |
| |
| CSSScrollTimeline* CSSAnimations::FindScrollTimelineForElement( |
| const ScopedCSSName& target_name, |
| const CSSAnimationUpdate* update, |
| const TimelineData* timeline_data) { |
| CSSScrollTimeline* existing_scroll_timeline = |
| timeline_data ? timeline_data->GetScrollTimeline() : nullptr; |
| // The pending CSSScrollTimeline (if any) takes precedence over the |
| // timeline stored on CSSAnimations::TimelineData. |
| absl::optional<CSSScrollTimeline*> pending_timeline = |
| update ? update->ChangedScrollTimeline() |
| : absl::optional<CSSScrollTimeline*>(); |
| CSSScrollTimeline* pending_aware_timeline = |
| pending_timeline.value_or(existing_scroll_timeline); |
| if (!pending_aware_timeline) { |
| return nullptr; |
| } |
| if (pending_aware_timeline->Name().GetName() != target_name.GetName()) { |
| return nullptr; |
| } |
| |
| if (RuntimeEnabledFeatures::CSSTreeScopedTimelinesEnabled()) { |
| if (TreeScopeDistance(pending_aware_timeline->Name().GetTreeScope(), |
| target_name.GetTreeScope()) == |
| std::numeric_limits<size_t>::max()) { |
| return nullptr; |
| } |
| } |
| return pending_aware_timeline; |
| } |
| |
| CSSViewTimeline* CSSAnimations::FindViewTimelineForElement( |
| const ScopedCSSName& target_name, |
| const CSSAnimationUpdate* update, |
| const TimelineData* timeline_data) { |
| const CSSViewTimelineMap* existing_timelines = |
| timeline_data ? &timeline_data->GetViewTimelines() : nullptr; |
| const CSSViewTimelineMap* changed_timelines = |
| update ? &update->ChangedViewTimelines() : nullptr; |
| return FindTimelineForElement<CSSViewTimeline>( |
| target_name, existing_timelines, changed_timelines); |
| } |
| |
| template <typename TimelineType> |
| TimelineType* CSSAnimations::FindTimelineForElement( |
| const ScopedCSSName& target_name, |
| const CSSTimelineMap<TimelineType>* existing_timelines, |
| const CSSTimelineMap<TimelineType>* changed_timelines) { |
| TimelineType* matching_timeline = nullptr; |
| size_t matching_distance = std::numeric_limits<size_t>::max(); |
| |
| // First, search through existing named timelines. |
| if (existing_timelines) { |
| for (auto [name, value] : *existing_timelines) { |
| // Skip timelines affected by the current CSSAnimationUpdate: |
| // they will be handled by the next for-loop. |
| if (changed_timelines && changed_timelines->Contains(name)) { |
| continue; |
| } |
| UpdateMatchingTimeline(target_name, *name, value.Get(), matching_timeline, |
| matching_distance); |
| } |
| } |
| |
| // Search through timelines created or modified this CSSAnimationUpdate. |
| if (changed_timelines) { |
| for (auto [name, value] : *changed_timelines) { |
| if (!value) { |
| // A value of nullptr means that a currently existing timeline |
| // was removed. |
| continue; |
| } |
| UpdateMatchingTimeline(target_name, *name, value.Get(), matching_timeline, |
| matching_distance); |
| } |
| } |
| |
| return matching_timeline; |
| } |
| |
| // Find a ScrollTimeline in the inclusive sibling-ancestors. |
| // |
| // The reason `update` is provided from the outside rather than just fetching |
| // it from ElementAnimations, is that for the current node we're resolving style |
| // for, the update hasn't actually been stored on ElementAnimations yet. |
| ScrollTimeline* CSSAnimations::FindPreviousSiblingAncestorTimeline( |
| const ScopedCSSName& name, |
| Node* node, |
| const CSSAnimationUpdate* update) { |
| DCHECK(node); |
| |
| if (ScrollTimeline* timeline = FindTimelineForNode(name, node, update)) |
| return timeline; |
| |
| // We use LayoutTreeBuilderTraversal to skip siblings which are not in the |
| // flat tree, because they don't have a ComputedStyle (and therefore can't |
| // provide any timelines). |
| for (Node* prev = LayoutTreeBuilderTraversal::PreviousSibling(*node); prev; |
| prev = LayoutTreeBuilderTraversal::PreviousSibling(*prev)) { |
| if (ScrollTimeline* timeline = |
| FindTimelineForNode(name, prev, GetPendingAnimationUpdate(*prev))) { |
| return timeline; |
| } |
| } |
| |
| Element* parent_element = |
| RuntimeEnabledFeatures::CSSTreeScopedTimelinesEnabled() |
| ? node->ParentOrShadowHostElement() |
| : LayoutTreeBuilderTraversal::ParentElement(*node); |
| if (!parent_element) { |
| return nullptr; |
| } |
| return FindPreviousSiblingAncestorTimeline( |
| name, parent_element, GetPendingAnimationUpdate(*parent_element)); |
| } |
| |
| namespace { |
| |
| std::pair<ScrollTimeline::ReferenceType, absl::optional<Element*>> |
| ComputeReference(Element* element, TimelineScroller scroller) { |
| using ReferenceType = ScrollTimeline::ReferenceType; |
| |
| switch (scroller) { |
| case TimelineScroller::kNearest: |
| return {ReferenceType::kNearestAncestor, element}; |
| case TimelineScroller::kRoot: |
| // Note that absl::nullopt will translate to |
| // Document::ScrollingElementNoLayout in the CSSScrollTimeline |
| // constructor. |
| return {ReferenceType::kSource, absl::nullopt}; |
| } |
| } |
| |
| CSSScrollTimeline* ComputeScrollFunctionTimeline( |
| Element* element, |
| const StyleTimeline::ScrollData& scroll_data, |
| AnimationTimeline* existing_timeline) { |
| Document& document = element->GetDocument(); |
| |
| auto [reference_type, reference_element] = |
| ComputeReference(element, scroll_data.GetScroller()); |
| auto* name = |
| MakeGarbageCollected<ScopedCSSName>("", /* tree_scope */ nullptr); |
| CSSScrollTimeline::Options options(document, reference_type, |
| reference_element, *name, |
| scroll_data.GetAxis()); |
| if (auto* scroll_timeline = DynamicTo<CSSScrollTimeline>(existing_timeline); |
| scroll_timeline && scroll_timeline->Matches(document, options)) { |
| return scroll_timeline; |
| } |
| // TODO(crbug.com/1356482): Cache/re-use timelines created from scroll(). |
| return MakeGarbageCollected<CSSScrollTimeline>(&document, std::move(options)); |
| } |
| |
| AnimationTimeline* ComputeViewFunctionTimeline( |
| Element* element, |
| const StyleTimeline::ViewData& view_data, |
| AnimationTimeline* existing_timeline) { |
| TimelineAxis axis = view_data.GetAxis(); |
| const TimelineInset& inset = view_data.GetInset(); |
| CSSViewTimeline::Options options(element, axis, inset); |
| |
| if (auto* view_timeline = DynamicTo<CSSViewTimeline>(existing_timeline); |
| view_timeline && view_timeline->Matches(options)) { |
| return view_timeline; |
| } |
| |
| CSSViewTimeline* new_timeline = MakeGarbageCollected<CSSViewTimeline>( |
| &element->GetDocument(), std::move(options)); |
| return new_timeline; |
| } |
| |
| } // namespace |
| |
| AnimationTimeline* CSSAnimations::ComputeTimeline( |
| Element* element, |
| const StyleTimeline& style_timeline, |
| const CSSAnimationUpdate& update, |
| AnimationTimeline* existing_timeline) { |
| Document& document = element->GetDocument(); |
| if (style_timeline.IsKeyword()) { |
| if (style_timeline.GetKeyword() == CSSValueID::kAuto) |
| return &document.Timeline(); |
| DCHECK_EQ(style_timeline.GetKeyword(), CSSValueID::kNone); |
| return nullptr; |
| } |
| if (style_timeline.IsName()) { |
| return FindPreviousSiblingAncestorTimeline(style_timeline.GetName(), |
| element, &update); |
| } |
| if (style_timeline.IsView()) { |
| return ComputeViewFunctionTimeline(element, style_timeline.GetView(), |
| existing_timeline); |
| } |
| DCHECK(style_timeline.IsScroll()); |
| return ComputeScrollFunctionTimeline(element, style_timeline.GetScroll(), |
| existing_timeline); |
| } |
| |
| 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, |
| bool force_update) { |
| ElementAnimations* element_animations = |
| animating_element.GetElementAnimations(); |
| |
| // 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->HasCurrentCompositableAnimation()) { |
| return; |
| } |
| |
| bool transform_zoom_changed = |
| (old_style->HasCurrentTranslateAnimation() || |
| 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 (force_update || |
| ((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::CalculateTimelineUpdate( |
| CSSAnimationUpdate& update, |
| Element& animating_element, |
| const ComputedStyleBuilder& style_builder) { |
| CalculateScrollTimelineUpdate(update, animating_element, style_builder); |
| CalculateViewTimelineUpdate(update, animating_element, style_builder); |
| } |
| |
| void CSSAnimations::CalculateAnimationUpdate( |
| CSSAnimationUpdate& update, |
| const Element& animating_element, |
| Element& element, |
| const ComputedStyleBuilder& style_builder, |
| const ComputedStyle* parent_style, |
| StyleResolver* resolver) { |
| ElementAnimations* element_animations = |
| animating_element.GetElementAnimations(); |
| |
| 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 |
| |
| const WritingDirectionMode writing_direction = |
| style_builder.GetWritingDirection(); |
| |
| // 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.GetComputedStyle(); |
| bool logical_property_mapping_change = |
| !old_style || old_style->GetWritingDirection() != writing_direction; |
| |
| 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( |
| writing_direction.Direction(), writing_direction.GetWritingMode()); |
| animation->UpdateIfNecessary(); |
| } |
| } |
| } |
| |
| const CSSAnimationData* animation_data = style_builder.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_builder.Display() != EDisplay::kNone || |
| (RuntimeEnabledFeatures::CSSDisplayAnimationEnabled() && old_style && |
| old_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 occurrence 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); |
| // We need to copy timing to a second object for cases where the original |
| // is modified and we still need original values. |
| 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, &animating_element, name); |
| if (!keyframes_rule) |
| continue; // Cancel the animation if there's no style rule for it. |
| |
| const StyleTimeline& style_timeline = animation_data->GetTimeline(i); |
| |
| 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; |
| bool will_be_playing = false; |
| const Animation::AnimationPlayState play_state = |
| animation->CalculateAnimationPlayState(); |
| if (is_paused != was_paused && !animation->getIgnoreCSSPlayState()) { |
| switch (play_state) { |
| case Animation::kIdle: |
| break; |
| |
| case Animation::kPaused: |
| toggle_pause_state = !is_paused; |
| will_be_playing = !is_paused; |
| break; |
| |
| case Animation::kRunning: |
| case Animation::kFinished: |
| toggle_pause_state = is_paused; |
| will_be_playing = !is_paused; |
| break; |
| |
| default: |
| // kUnset and kPending. |
| NOTREACHED(); |
| } |
| } else { |
| will_be_playing = (play_state == Animation::kRunning) || |
| (play_state == Animation::kFinished); |
| } |
| |
| AnimationTimeline* timeline = existing_animation->Timeline(); |
| if (!is_animation_style_change && !animation->GetIgnoreCSSTimeline()) { |
| timeline = ComputeTimeline(&element, style_timeline, update, |
| existing_animation->Timeline()); |
| } |
| |
| // If there are no named range keyframes, when scroll_offsets change, |
| // 'from' is still 'from', '10%' is still '10%',no need to recalc model. |
| bool has_named_range_keyframes = false; |
| if (animation->effect() && animation->effect()->IsKeyframeEffect()) { |
| if (auto* model = To<KeyframeEffect>(animation->effect())->Model()) |
| has_named_range_keyframes = model->HasNamedRangeKeyframes(); |
| } |
| bool scroll_offsets_changed = false; |
| if (timeline && timeline->IsViewTimeline()) { |
| scroll_offsets_changed = |
| existing_animation->scroll_offsets != |
| To<ViewTimeline>(timeline)->GetResolvedScrollOffsets(); |
| } |
| bool needs_keyframe_model_recalc = |
| has_named_range_keyframes && scroll_offsets_changed; |
| |
| if (needs_keyframe_model_recalc || |
| 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); |
| absl::optional<AnimationTimeDelta> inherited_time; |
| absl::optional<AnimationTimeDelta> timeline_duration; |
| |
| if (timeline) { |
| inherited_time = animation->UnlimitedCurrentTime(); |
| timeline_duration = timeline->GetDuration(); |
| |
| if (will_be_playing && |
| ((timeline != existing_animation->Timeline()) || |
| animation->ResetsCurrentTimeOnResume())) { |
| if (timeline->IsScrollTimeline()) { |
| inherited_time = timeline->CurrentTime(); |
| } else { |
| AnimationTimeline* previous_timeline = |
| existing_animation->Timeline(); |
| // Check to see if we are switching from a scroll timeline to a |
| // document timeline. |
| if (previous_timeline && |
| previous_timeline->IsScrollTimeline() && |
| previous_timeline->CurrentTime()) { |
| // For now, CSS Animations do not support duration 'auto'. |
| // Issue: https://github.com/w3c/csswg-drafts/issues/6530 |
| DCHECK(specified_timing.iteration_duration); |
| |
| // We need to maintain current progress in the animation when |
| // switching from scroll timeline to document timeline. |
| double progress = previous_timeline->CurrentTime().value() / |
| previous_timeline->GetDuration().value(); |
| |
| AnimationTimeDelta end_time = std::max( |
| specified_timing.start_delay.AsTimeValue() + |
| MultiplyZeroAlwaysGivesZero( |
| specified_timing.iteration_duration.value(), |
| specified_timing.iteration_count) + |
| specified_timing.end_delay.AsTimeValue(), |
| AnimationTimeDelta()); |
| |
| inherited_time = progress * end_time; |
| } |
| } |
| } |
| } |
| |
| update.UpdateAnimation( |
| existing_animation_index, animation, |
| *MakeGarbageCollected<InertEffect>( |
| CreateKeyframeEffectModel( |
| resolver, element, animating_element, writing_direction, |
| parent_style, name, keyframe_timing_function.get(), i, |
| timeline), |
| timing, is_paused, inherited_time, timeline_duration, |
| animation->playbackRate()), |
| 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, style_timeline, update, /* existing_timeline */ nullptr); |
| absl::optional<AnimationTimeDelta> inherited_time = |
| AnimationTimeDelta(); |
| |
| absl::optional<AnimationTimeDelta> timeline_duration; |
| if (timeline) { |
| timeline_duration = timeline->GetDuration(); |
| if (!timeline->IsMonotonicallyIncreasing()) { |
| inherited_time = timeline->CurrentTime(); |
| } |
| } |
| update.StartAnimation( |
| name, name_index, i, |
| *MakeGarbageCollected<InertEffect>( |
| CreateKeyframeEffectModel(resolver, element, animating_element, |
| writing_direction, parent_style, name, |
| keyframe_timing_function.get(), i, |
| timeline), |
| timing, is_paused, inherited_time, timeline_duration, 1.0), |
| 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; |
| absl::optional<double> previous_iteration = |
| old_animation_delegate ? old_animation_delegate->getPreviousIteration() |
| : absl::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()); |
| } |
| |
| namespace { |
| |
| bool AffectsBackgroundColor(const AnimationEffect& effect) { |
| return effect.Affects(PropertyHandle(GetCSSPropertyBackgroundColor())); |
| } |
| |
| void UpdateAnimationFlagsForEffect(const AnimationEffect& effect, |
| ComputedStyleBuilder& builder) { |
| if (effect.Affects(PropertyHandle(GetCSSPropertyOpacity()))) |
| builder.SetHasCurrentOpacityAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyTransform()))) |
| builder.SetHasCurrentTransformAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyRotate()))) |
| builder.SetHasCurrentRotateAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyScale()))) |
| builder.SetHasCurrentScaleAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyTranslate()))) |
| builder.SetHasCurrentTranslateAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyFilter()))) |
| builder.SetHasCurrentFilterAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyBackdropFilter()))) |
| builder.SetHasCurrentBackdropFilterAnimation(true); |
| if (AffectsBackgroundColor(effect)) |
| builder.SetHasCurrentBackgroundColorAnimation(true); |
| if (effect.Affects(PropertyHandle(GetCSSPropertyClipPath()))) |
| builder.SetHasCurrentClipPathAnimation(true); |
| } |
| |
| void SetCompositablePaintAnimationChangedIfAffected( |
| const AnimationEffect& effect, |
| ComputedStyleBuilder& builder) { |
| if (RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() && |
| AffectsBackgroundColor(effect)) { |
| builder.SetCompositablePaintAnimationChanged(true); |
| } |
| } |
| |
| // Called for animations that are newly created or updated. |
| void UpdateAnimationFlagsForInertEffect(const InertEffect& effect, |
| ComputedStyleBuilder& builder) { |
| if (!effect.IsCurrent()) |
| return; |
| |
| UpdateAnimationFlagsForEffect(effect, builder); |
| |
| // We defensively assume that any update to an existing animation |
| // would result in CompositorPending()==true. |
| SetCompositablePaintAnimationChangedIfAffected(effect, builder); |
| } |
| |
| // Called for existing animations that are not modified in this update. |
| void UpdateAnimationFlagsForAnimation(const Animation& animation, |
| ComputedStyleBuilder& builder) { |
| const AnimationEffect& effect = *animation.effect(); |
| |
| if (!effect.IsCurrent()) |
| return; |
| |
| UpdateAnimationFlagsForEffect(effect, builder); |
| |
| if (animation.CalculateAnimationPlayState() != Animation::kIdle && |
| animation.CompositorPending()) { |
| // If something about the animation changed since the last frame (e.g. the |
| // effect was modified), we may need to repaint. We use the |
| // CompositorPending flag to detect such changes, and conditionally set |
| // the CompositablePaintAnimationChanged on ComputedStyle which ultimately |
| // invalidates paint. |
| // |
| // See ComputedStyle::UpdatePropertySpecificDifferences for how this flag |
| // is used. |
| SetCompositablePaintAnimationChangedIfAffected(effect, builder); |
| } |
| } |
| |
| } // namespace |
| |
| void CSSAnimations::UpdateAnimationFlags(Element& animating_element, |
| CSSAnimationUpdate& update, |
| ComputedStyleBuilder& builder) { |
| for (const auto& new_animation : update.NewAnimations()) |
| UpdateAnimationFlagsForInertEffect(*new_animation.effect, builder); |
| |
| for (const auto& updated_animation : update.AnimationsWithUpdates()) |
| UpdateAnimationFlagsForInertEffect(*updated_animation.effect, builder); |
| |
| for (const auto& entry : update.NewTransitions()) |
| UpdateAnimationFlagsForInertEffect(*entry.value->effect, builder); |
| |
| if (auto* element_animations = animating_element.GetElementAnimations()) { |
| HeapHashSet<Member<const Animation>> cancelled_transitions = |
| CreateCancelledTransitionsSet(element_animations, update); |
| const HeapHashSet<Member<const Animation>>& suppressed_animations = |
| update.SuppressedAnimations(); |
| |
| auto is_suppressed = [&cancelled_transitions, &suppressed_animations]( |
| const Animation& animation) -> bool { |
| return suppressed_animations.Contains(&animation) || |
| cancelled_transitions.Contains(&animation); |
| }; |
| |
| for (auto& entry : element_animations->Animations()) { |
| if (!is_suppressed(*entry.key)) |
| UpdateAnimationFlagsForAnimation(*entry.key, builder); |
| } |
| |
| for (auto& entry : element_animations->GetWorkletAnimations()) { |
| // TODO(majidvp): we should check the effect's phase before updating the |
| // style once the timing of effect is ready to use. |
| // https://crbug.com/814851. |
| UpdateAnimationFlagsForEffect(*entry->GetEffect(), builder); |
| } |
| |
| // All Animations in this list will get SetCompositorPending(true) |
| // during MaybeApplyPendingUpdate. |
| for (const Animation* animation : update.UpdatedCompositorKeyframes()) { |
| if (!is_suppressed(*animation)) { |
| SetCompositablePaintAnimationChangedIfAffected(*animation->effect(), |
| builder); |
| } |
| } |
| |
| EffectStack& effect_stack = element_animations->GetEffectStack(); |
| |
| if (builder.HasCurrentOpacityAnimation()) { |
| builder.SetIsRunningOpacityAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyOpacity()))); |
| } |
| if (builder.HasCurrentTransformAnimation()) { |
| builder.SetIsRunningTransformAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyTransform()))); |
| } |
| if (builder.HasCurrentScaleAnimation()) { |
| builder.SetIsRunningScaleAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyScale()))); |
| } |
| if (builder.HasCurrentRotateAnimation()) { |
| builder.SetIsRunningRotateAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyRotate()))); |
| } |
| if (builder.HasCurrentTranslateAnimation()) { |
| builder.SetIsRunningTranslateAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyTranslate()))); |
| } |
| if (builder.HasCurrentFilterAnimation()) { |
| builder.SetIsRunningFilterAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyFilter()))); |
| } |
| if (builder.HasCurrentBackdropFilterAnimation()) { |
| builder.SetIsRunningBackdropFilterAnimationOnCompositor( |
| effect_stack.HasActiveAnimationsOnCompositor( |
| PropertyHandle(GetCSSPropertyBackdropFilter()))); |
| } |
| } |
| } |
| |
| void CSSAnimations::MaybeApplyPendingUpdate(Element* element) { |
| previous_active_interpolations_for_animations_.clear(); |
| if (pending_update_.IsEmpty()) { |
| return; |
| } |
| |
| previous_active_interpolations_for_animations_.swap( |
| pending_update_.ActiveInterpolationsForAnimations()); |
| |
| if (!pending_update_.HasUpdates()) { |
| ClearPendingUpdate(); |
| return; |
| } |
| |
| if (absl::optional<CSSScrollTimeline*> changed_timeline = |
| pending_update_.ChangedScrollTimeline()) { |
| timeline_data_.SetScrollTimeline(*changed_timeline); |
| } |
| for (auto [name, value] : pending_update_.ChangedViewTimelines()) { |
| timeline_data_.SetViewTimeline(*name, value.Get()); |
| } |
| |
| 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); |
| entry.animation->Update(kTimingUpdateOnDemand); |
| } |
| |
| 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(); |
| } |
| } |
| |
| HashSet<PropertyHandle> suppressed_transitions; |
| |
| if (!pending_update_.NewTransitions().empty()) { |
| element->GetDocument() |
| .GetDocumentAnimations() |
| .IncrementTrasitionGeneration(); |
| } |
| |
| for (const auto& entry : pending_update_.NewTransitions()) { |
| const CSSAnimationUpdate::NewTransition* new_transition = entry.value; |
| const PropertyHandle& property = new_transition->property; |
| |
| if (suppressed_transitions.Contains(property)) |
| continue; |
| |
| 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 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)) { |
| animation->setStartTime(element->GetDocument().Timeline().currentTime(), |
| ASSERT_NO_EXCEPTION); |
| } |
| animation->Update(kTimingUpdateOnDemand); |
| running_transition->animation = animation; |
| transitions_.Set(property, running_transition); |
| } |
| ClearPendingUpdate(); |
| } |
| |
| HeapHashSet<Member<const Animation>> |
| CSSAnimations::CreateCancelledTransitionsSet( |
| ElementAnimations* element_animations, |
| CSSAnimationUpdate& update) { |
| HeapHashSet<Member<const Animation>> cancelled_transitions; |
| if (!update.CancelledTransitions().empty()) { |
| DCHECK(element_animations); |
| const TransitionMap& transition_map = |
| element_animations->CssAnimations().transitions_; |
| for (const PropertyHandle& property : update.CancelledTransitions()) { |
| DCHECK(transition_map.Contains(property)); |
| cancelled_transitions.insert( |
| transition_map.at(property)->animation.Get()); |
| } |
| } |
| return cancelled_transitions; |
| } |
| |
| bool CSSAnimations::CanCalculateTransitionUpdateForProperty( |
| TransitionUpdateState& state, |
| const PropertyHandle& property) { |
| // TODO(crbug.com/1226772): We should transition if an !important property |
| // changes even when an animation is running. |
| if (state.update.ActiveInterpolationsForAnimations().Contains(property) || |
| (state.animating_element.GetElementAnimations() && |
| state.animating_element.GetElementAnimations() |
| ->CssAnimations() |
| .previous_active_interpolations_for_animations_.Contains( |
| property))) { |
| UseCounter::Count(state.animating_element.GetDocument(), |
| WebFeature::kCSSTransitionBlockedByAnimation); |
| return false; |
| } |
| return true; |
| } |
| |
| void CSSAnimations::CalculateTransitionUpdateForPropertyHandle( |
| TransitionUpdateState& state, |
| const PropertyHandle& property, |
| size_t transition_index) { |
| if (state.listed_properties) { |
| state.listed_properties->insert(property); |
| } |
| |
| if (!CanCalculateTransitionUpdateForProperty(state, 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.base_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.base_style, |
| *running_transition->reversing_adjusted_start_value)) { |
| interrupted_transition = running_transition; |
| } |
| } |
| } |
| |
| // In the default configuration (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) { |
| // By calling GetBaseComputedStyleOrThis, we're using 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. |
| state.before_change_style = CalculateBeforeChangeStyle( |
| state.animating_element, *state.old_style.GetBaseComputedStyleOrThis()); |
| } |
| |
| if (ComputedValuesEqual(property, *state.before_change_style, |
| state.base_style)) { |
| return; |
| } |
| |
| CSSInterpolationTypesMap map(registry, state.animating_element.GetDocument()); |
| CSSInterpolationEnvironment old_environment(map, *state.before_change_style); |
| CSSInterpolationEnvironment new_environment(map, state.base_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.AsTimeValue() + timing.iteration_duration.value() <= |
| AnimationTimeDelta()) { |
| // 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 absl::optional<double> interrupted_progress = |
| effect ? effect->Progress() : absl::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.AsTimeValue() < AnimationTimeDelta()) { |
| timing.start_delay.Scale(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() && |
| CompositorAnimations::CompositedPropertyRequiresSnapshot(property)) { |
| CompositorKeyframeValue* from = CompositorKeyframeValueFactory::Create( |
| property, *state.before_change_style, start_keyframe->Offset().value()); |
| CompositorKeyframeValue* to = CompositorKeyframeValueFactory::Create( |
| property, state.base_style, end_keyframe->Offset().value()); |
| start_keyframe->SetCompositorValue(from); |
| end_keyframe->SetCompositorValue(to); |
| } |
| |
| auto* model = MakeGarbageCollected<TransitionKeyframeEffectModel>(keyframes); |
| state.update.StartTransition( |
| property, state.before_change_style, &state.base_style, |
| reversing_adjusted_start_value, reversing_shortening_factor, |
| *MakeGarbageCollected<InertEffect>( |
| model, timing, false, AnimationTimeDelta(), absl::nullopt, 1.0)); |
| DCHECK(!state.animating_element.GetElementAnimations() || |
| !state.animating_element.GetElementAnimations() |
| ->IsAnimationStyleChange()); |
| } |
| |
| void CSSAnimations::CalculateTransitionUpdateForProperty( |
| TransitionUpdateState& state, |
| const CSSTransitionData::TransitionProperty& transition_property, |
| size_t transition_index, |
| WritingDirectionMode writing_direction) { |
| switch (transition_property.property_type) { |
| case CSSTransitionData::kTransitionUnknownProperty: |
| CalculateTransitionUpdateForCustomProperty(state, transition_property, |
| transition_index); |
| break; |
| case CSSTransitionData::kTransitionKnownProperty: |
| CalculateTransitionUpdateForStandardProperty( |
| state, transition_property, transition_index, writing_direction); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void CSSAnimations::CalculateTransitionUpdateForCustomProperty( |
| TransitionUpdateState& state, |
| const CSSTransitionData::TransitionProperty& transition_property, |
| size_t transition_index) { |
| DCHECK_EQ(transition_property.property_type, |
| CSSTransitionData::kTransitionUnknownProperty); |
| |
| if (!CSSVariableParser::IsValidVariableName( |
| transition_property.property_string)) { |
| return; |
| } |
| CalculateTransitionUpdateForPropertyHandle( |
| state, PropertyHandle(transition_property.property_string), |
| transition_index); |
| } |
| |
| void CSSAnimations::CalculateTransitionUpdateForStandardProperty( |
| TransitionUpdateState& state, |
| const CSSTransitionData::TransitionProperty& transition_property, |
| size_t transition_index, |
| WritingDirectionMode writing_direction) { |
| DCHECK_EQ(transition_property.property_type, |
| CSSTransitionData::kTransitionKnownProperty); |
| |
| 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(writing_direction.Direction(), |
| writing_direction.GetWritingMode()); |
| PropertyHandle property_handle = PropertyHandle(property); |
| |
| if (!animate_all && !property.IsInterpolable()) { |
| continue; |
| } |
| |
| CalculateTransitionUpdateForPropertyHandle(state, property_handle, |
| transition_index); |
| } |
| } |
| |
| void CSSAnimations::CalculateTransitionUpdate( |
| CSSAnimationUpdate& update, |
| Element& animating_element, |
| const ComputedStyleBuilder& style_builder) { |
| 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_builder.Transitions(); |
| const WritingDirectionMode writing_direction = |
| style_builder.GetWritingDirection(); |
| |
| 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 (auto* data = PostStyleUpdateScope::CurrentAnimationData()) |
| old_style = data->GetOldStyle(animating_element); |
| |
| if (!animation_style_recalc && style_builder.Display() != EDisplay::kNone && |
| old_style && !old_style->IsEnsuredInDisplayNone()) { |
| // Don't bother updating listed_properties unless we need it below. |
| HashSet<PropertyHandle>* listed_properties_maybe = |
| active_transitions ? &listed_properties : nullptr; |
| TransitionUpdateState state = {update, |
| animating_element, |
| *old_style, |
| *style_builder.GetBaseComputedStyle(), |
| /*before_change_style=*/nullptr, |
| active_transitions, |
| listed_properties_maybe, |
| 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; |
| } |
| CalculateTransitionUpdateForProperty( |
| state, transition_property, transition_index, writing_direction); |
| } |
| } else if (active_transitions && active_transitions->size()) { |
| // !transition_data implies transition: all 0s |
| any_transition_had_transition_all = true; |
| CSSTransitionData::TransitionProperty default_property( |
| CSSPropertyID::kAll); |
| CalculateTransitionUpdateForProperty(state, default_property, 0, |
| writing_direction); |
| } |
| } |
| |
| if (active_transitions) { |
| for (const auto& entry : *active_transitions) { |
| const PropertyHandle& property = entry.key; |
| 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, 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) { |
| V8CSSNumberish* current_time_numberish = animation->currentTime(); |
| if (!current_time_numberish) |
| continue; |
| |
| // CSSNumericValue is not yet supported, verify that it is not used |
| DCHECK(!current_time_numberish->IsCSSNumericValue()); |
| |
| absl::optional<AnimationTimeDelta> current_time = |
| ANIMATION_TIME_DELTA_FROM_MILLISECONDS( |
| 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, |
| /* timeline_duration */ absl::nullopt, animation->playbackRate()); |
| |
| 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(); |
| timeline_data_.Clear(); |
| pending_update_.Clear(); |
| } |
| |
| void CSSAnimations::TimelineData::SetViewTimeline(const ScopedCSSName& name, |
| CSSViewTimeline* timeline) { |
| if (timeline == nullptr) { |
| view_timelines_.erase(&name); |
| } else { |
| view_timelines_.Set(&name, timeline); |
| } |
| } |
| |
| void CSSAnimations::TimelineData::Trace(blink::Visitor* visitor) const { |
| visitor->Trace(scroll_timeline_); |
| visitor->Trace(view_timelines_); |
| } |
| |
| 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 IsCSSPropertyHandle(const PropertyHandle& property) { |
| return property.IsCSSProperty() || property.IsPresentationAttribute(); |
| } |
| |
| bool IsLineHeightPropertyHandle(const PropertyHandle& property) { |
| return property == PropertyHandle(GetCSSPropertyLineHeight()); |
| } |
| |
| void AdoptActiveAnimationInterpolations( |
| EffectStack* effect_stack, |
| CSSAnimationUpdate& update, |
| const HeapVector<Member<const InertEffect>>* new_animations, |
| const HeapHashSet<Member<const Animation>>* suppressed_animations) { |
| ActiveInterpolationsMap interpolations(EffectStack::ActiveInterpolations( |
| effect_stack, new_animations, suppressed_animations, |
| KeyframeEffect::kDefaultPriority, IsCSSPropertyHandle)); |
| update.AdoptActiveInterpolationsForAnimations(interpolations); |
| } |
| |
| } // namespace |
| |
| void CSSAnimations::CalculateAnimationActiveInterpolations( |
| CSSAnimationUpdate& update, |
| const Element& animating_element) { |
| ElementAnimations* element_animations = |
| animating_element.GetElementAnimations(); |
| EffectStack* effect_stack = |
| element_animations ? &element_animations->GetEffectStack() : nullptr; |
| |
| if (update.NewAnimations().empty() && update.SuppressedAnimations().empty()) { |
| 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()); |
| } |
| |
| void CSSAnimations::CalculateTransitionActiveInterpolations( |
| CSSAnimationUpdate& update, |
| const Element& animating_element) { |
| ElementAnimations* element_animations = |
| animating_element.GetElementAnimations(); |
| EffectStack* effect_stack = |
| element_animations ? &element_animations->GetEffectStack() : nullptr; |
| |
| ActiveInterpolationsMap active_interpolations_for_transitions; |
| if (update.NewTransitions().empty() && |
| update.CancelledTransitions().empty()) { |
| active_interpolations_for_transitions = EffectStack::ActiveInterpolations( |
| effect_stack, nullptr, nullptr, KeyframeEffect::kTransitionPriority, |
| IsCSSPropertyHandle); |
| } 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 = |
| CreateCancelledTransitionsSet(element_animations, update); |
| |
| active_interpolations_for_transitions = EffectStack::ActiveInterpolations( |
| effect_stack, &new_transitions, &cancelled_animations, |
| KeyframeEffect::kTransitionPriority, IsCSSPropertyHandle); |
| } |
| |
| const ActiveInterpolationsMap& animations = |
| update.ActiveInterpolationsForAnimations(); |
| // Properties being animated by animations don't get values from transitions |
| // applied. |
| if (!animations.empty() && !active_interpolations_for_transitions.empty()) { |
| for (const auto& entry : animations) |
| active_interpolations_for_transitions.erase(entry.key); |
| } |
| |
| update.AdoptActiveInterpolationsForTransitions( |
| 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_); |
| 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 absl::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.NormalizedTiming().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.NormalizedTiming().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.NormalizedTiming().iteration_duration); |
| } |
| } |
| |
| 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.NormalizedTiming().iteration_duration); |
| } 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.NormalizedTiming().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". |
| absl::optional<AnimationTimeDelta> cancel_active_time = |
| CalculateActiveTime(animation_node.NormalizedTiming(), |
| Timing::FillMode::BOTH, |
| animation_node.LocalTime(), previous_phase_); |
| // 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(transition_target_); |
| 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.empty()) { |
| 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://w3.org/TR/web-animations-1/#animating-properties |
| bool CSSAnimations::IsAnimationAffectingProperty(const CSSProperty& property) { |
| switch (property.PropertyID()) { |
| case CSSPropertyID::kAnimation: |
| case CSSPropertyID::kAlternativeAnimation: |
| case CSSPropertyID::kAnimationDelay: |
| case CSSPropertyID::kAlternativeAnimationDelay: |
| case CSSPropertyID::kAnimationDelayEnd: |
| case CSSPropertyID::kAnimationDelayStart: |
| case CSSPropertyID::kAnimationDirection: |
| case CSSPropertyID::kAnimationDuration: |
| case CSSPropertyID::kAnimationFillMode: |
| case CSSPropertyID::kAnimationIterationCount: |
| case CSSPropertyID::kAnimationName: |
| case CSSPropertyID::kAnimationPlayState: |
| case CSSPropertyID::kAnimationRangeEnd: |
| case CSSPropertyID::kAnimationRangeStart: |
| case CSSPropertyID::kAnimationTimeline: |
| case CSSPropertyID::kAnimationTimingFunction: |
| case CSSPropertyID::kContentVisibility: |
| case CSSPropertyID::kContain: |
| case CSSPropertyID::kContainerName: |
| case CSSPropertyID::kContainerType: |
| case CSSPropertyID::kDirection: |
| case CSSPropertyID::kTextCombineUpright: |
| case CSSPropertyID::kTextOrientation: |
| case CSSPropertyID::kToggleGroup: |
| case CSSPropertyID::kToggleRoot: |
| case CSSPropertyID::kToggleTrigger: |
| 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; |
| case CSSPropertyID::kDisplay: |
| return !RuntimeEnabledFeatures::CSSDisplayAnimationEnabled(); |
| 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::IsAnimatingLineHeightProperty( |
| const ElementAnimations* element_animations) { |
| return element_animations && |
| element_animations->GetEffectStack().AffectsProperties( |
| IsLineHeightPropertyHandle); |
| } |
| |
| bool CSSAnimations::IsAnimatingRevert( |
| const ElementAnimations* element_animations) { |
| return element_animations && element_animations->GetEffectStack().HasRevert(); |
| } |
| |
| void CSSAnimations::Trace(Visitor* visitor) const { |
| visitor->Trace(timeline_data_); |
| visitor->Trace(transitions_); |
| visitor->Trace(pending_update_); |
| visitor->Trace(running_animations_); |
| visitor->Trace(previous_active_interpolations_for_animations_); |
| } |
| |
| } // namespace blink |