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