summaryrefslogtreecommitdiffstats
path: root/dom/animation/KeyframeEffect.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/KeyframeEffect.cpp')
-rw-r--r--dom/animation/KeyframeEffect.cpp2117
1 files changed, 2117 insertions, 0 deletions
diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp
new file mode 100644
index 0000000000..2f0d886ac1
--- /dev/null
+++ b/dom/animation/KeyframeEffect.cpp
@@ -0,0 +1,2117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/KeyframeEffect.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+// For UnrestrictedDoubleOrKeyframeAnimationOptions;
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/layers/AnimationInfo.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/FloatingPoint.h" // For IsFinite
+#include "mozilla/LayerAnimationInfo.h"
+#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetComputedStyle
+#include "nsContentUtils.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
+#include "nsCSSPseudoElements.h" // For PseudoStyleType
+#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsPresContextInlines.h"
+#include "nsRefreshDriver.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "WindowRenderer.h"
+
+namespace mozilla {
+
+void AnimationProperty::SetPerformanceWarning(
+ const AnimationPerformanceWarning& aWarning, const dom::Element* aElement) {
+ if (mPerformanceWarning && *mPerformanceWarning == aWarning) {
+ return;
+ }
+
+ mPerformanceWarning = Some(aWarning);
+
+ nsAutoString localizedString;
+ if (StaticPrefs::layers_offmainthreadcomposition_log_animations() &&
+ mPerformanceWarning->ToLocalizedString(localizedString)) {
+ nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
+ AnimationUtils::LogAsyncAnimationFailure(logMessage, aElement);
+ }
+}
+
+bool PropertyValuePair::operator==(const PropertyValuePair& aOther) const {
+ if (mProperty != aOther.mProperty) {
+ return false;
+ }
+ if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) {
+ return true;
+ }
+ if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) {
+ return false;
+ }
+ return Servo_DeclarationBlock_Equals(mServoDeclarationBlock,
+ aOther.mServoDeclarationBlock);
+}
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffect, AnimationEffect,
+ mTarget.mElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffect, AnimationEffect)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(KeyframeEffect)
+NS_INTERFACE_MAP_END_INHERITING(AnimationEffect)
+
+NS_IMPL_ADDREF_INHERITED(KeyframeEffect, AnimationEffect)
+NS_IMPL_RELEASE_INHERITED(KeyframeEffect, AnimationEffect)
+
+KeyframeEffect::KeyframeEffect(Document* aDocument,
+ OwningAnimationTarget&& aTarget,
+ TimingParams&& aTiming,
+ const KeyframeEffectParams& aOptions)
+ : AnimationEffect(aDocument, std::move(aTiming)),
+ mTarget(std::move(aTarget)),
+ mEffectOptions(aOptions) {}
+
+KeyframeEffect::KeyframeEffect(Document* aDocument,
+ OwningAnimationTarget&& aTarget,
+ const KeyframeEffect& aOther)
+ : AnimationEffect(aDocument, TimingParams{aOther.SpecifiedTiming()}),
+ mTarget(std::move(aTarget)),
+ mEffectOptions{aOther.IterationComposite(), aOther.Composite(),
+ mTarget.mPseudoType},
+ mKeyframes(aOther.mKeyframes.Clone()),
+ mProperties(aOther.mProperties.Clone()),
+ mBaseValues(aOther.mBaseValues.Clone()) {}
+
+JSObject* KeyframeEffect::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return KeyframeEffect_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+IterationCompositeOperation KeyframeEffect::IterationComposite() const {
+ return mEffectOptions.mIterationComposite;
+}
+
+void KeyframeEffect::SetIterationComposite(
+ const IterationCompositeOperation& aIterationComposite) {
+ if (mEffectOptions.mIterationComposite == aIterationComposite) {
+ return;
+ }
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(mAnimation);
+ }
+
+ mEffectOptions.mIterationComposite = aIterationComposite;
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+CompositeOperation KeyframeEffect::Composite() const {
+ return mEffectOptions.mComposite;
+}
+
+void KeyframeEffect::SetComposite(const CompositeOperation& aComposite) {
+ if (mEffectOptions.mComposite == aComposite) {
+ return;
+ }
+
+ mEffectOptions.mComposite = aComposite;
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(mAnimation);
+ }
+
+ if (mTarget) {
+ RefPtr<const ComputedStyle> computedStyle =
+ GetTargetComputedStyle(Flush::None);
+ if (computedStyle) {
+ UpdateProperties(computedStyle);
+ }
+ }
+}
+
+void KeyframeEffect::NotifySpecifiedTimingUpdated() {
+ // Use the same document for a pseudo element and its parent element.
+ // Use nullptr if we don't have mTarget, so disable the mutation batch.
+ nsAutoAnimationMutationBatch mb(mTarget ? mTarget.mElement->OwnerDoc()
+ : nullptr);
+
+ if (mAnimation) {
+ mAnimation->NotifyEffectTimingUpdated();
+
+ if (mAnimation->IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(mAnimation);
+ }
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+ }
+}
+
+void KeyframeEffect::NotifyAnimationTimingUpdated(
+ PostRestyleMode aPostRestyle) {
+ UpdateTargetRegistration();
+
+ // If the effect is not relevant it will be removed from the target
+ // element's effect set. However, effects not in the effect set
+ // will not be included in the set of candidate effects for running on
+ // the compositor and hence they won't have their compositor status
+ // updated. As a result, we need to make sure we clear their compositor
+ // status here.
+ bool isRelevant = mAnimation && mAnimation->IsRelevant();
+ if (!isRelevant) {
+ ResetIsRunningOnCompositor();
+ }
+
+ // Request restyle if necessary.
+ if (aPostRestyle == PostRestyleMode::IfNeeded && mAnimation &&
+ !mProperties.IsEmpty() && HasComputedTimingChanged()) {
+ EffectCompositor::RestyleType restyleType =
+ CanThrottle() ? EffectCompositor::RestyleType::Throttled
+ : EffectCompositor::RestyleType::Standard;
+ RequestRestyle(restyleType);
+ }
+
+ // Detect changes to "in effect" status since we need to recalculate the
+ // animation cascade for this element whenever that changes.
+ // Note that updating mInEffectOnLastAnimationTimingUpdate has to be done
+ // after above CanThrottle() call since the function uses the flag inside it.
+ bool inEffect = IsInEffect();
+ if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
+ MarkCascadeNeedsUpdate();
+ mInEffectOnLastAnimationTimingUpdate = inEffect;
+ }
+
+ // If we're no longer "in effect", our ComposeStyle method will never be
+ // called and we will never have a chance to update mProgressOnLastCompose
+ // and mCurrentIterationOnLastCompose.
+ // We clear them here to ensure that if we later become "in effect" we will
+ // request a restyle (above).
+ if (!inEffect) {
+ mProgressOnLastCompose.SetNull();
+ mCurrentIterationOnLastCompose = 0;
+ }
+}
+
+static bool KeyframesEqualIgnoringComputedOffsets(
+ const nsTArray<Keyframe>& aLhs, const nsTArray<Keyframe>& aRhs) {
+ if (aLhs.Length() != aRhs.Length()) {
+ return false;
+ }
+
+ for (size_t i = 0, len = aLhs.Length(); i < len; ++i) {
+ const Keyframe& a = aLhs[i];
+ const Keyframe& b = aRhs[i];
+ if (a.mOffset != b.mOffset || a.mTimingFunction != b.mTimingFunction ||
+ a.mPropertyValues != b.mPropertyValues) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// https://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes
+void KeyframeEffect::SetKeyframes(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv) {
+ nsTArray<Keyframe> keyframes = KeyframeUtils::GetKeyframesFromObject(
+ aContext, mDocument, aKeyframes, "KeyframeEffect.setKeyframes", aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<const ComputedStyle> style = GetTargetComputedStyle(Flush::None);
+ SetKeyframes(std::move(keyframes), style, nullptr /* AnimationTimeline */);
+}
+
+void KeyframeEffect::SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+ const ComputedStyle* aStyle,
+ const AnimationTimeline* aTimeline) {
+ if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
+ return;
+ }
+
+ mKeyframes = std::move(aKeyframes);
+ KeyframeUtils::DistributeKeyframes(mKeyframes);
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(mAnimation);
+ }
+
+ // We need to call UpdateProperties() unless the target element doesn't have
+ // style (e.g. the target element is not associated with any document).
+ if (aStyle) {
+ UpdateProperties(aStyle, aTimeline);
+ }
+}
+
+void KeyframeEffect::ReplaceTransitionStartValue(AnimationValue&& aStartValue) {
+ if (!aStartValue.mServo) {
+ return;
+ }
+
+ // A typical transition should have a single property and a single segment.
+ //
+ // (And for atypical transitions, that is, those updated by script, we don't
+ // apply the replacing behavior.)
+ if (mProperties.Length() != 1 || mProperties[0].mSegments.Length() != 1) {
+ return;
+ }
+
+ // Likewise, check that the keyframes are of the expected shape.
+ if (mKeyframes.Length() != 2 || mKeyframes[0].mPropertyValues.Length() != 1) {
+ return;
+ }
+
+ // Check that the value we are about to substitute in is actually for the
+ // same property.
+ if (Servo_AnimationValue_GetPropertyId(aStartValue.mServo) !=
+ mProperties[0].mProperty) {
+ return;
+ }
+
+ mKeyframes[0].mPropertyValues[0].mServoDeclarationBlock =
+ Servo_AnimationValue_Uncompute(aStartValue.mServo).Consume();
+ mProperties[0].mSegments[0].mFromValue = std::move(aStartValue);
+}
+
+static bool IsEffectiveProperty(const EffectSet& aEffects,
+ nsCSSPropertyID aProperty) {
+ return !aEffects.PropertiesWithImportantRules().HasProperty(aProperty) ||
+ !aEffects.PropertiesForAnimationsLevel().HasProperty(aProperty);
+}
+
+const AnimationProperty* KeyframeEffect::GetEffectiveAnimationOfProperty(
+ nsCSSPropertyID aProperty, const EffectSet& aEffects) const {
+ MOZ_ASSERT(mTarget && &aEffects == EffectSet::Get(mTarget.mElement,
+ mTarget.mPseudoType));
+
+ for (const AnimationProperty& property : mProperties) {
+ if (aProperty != property.mProperty) {
+ continue;
+ }
+
+ const AnimationProperty* result = nullptr;
+ // Only include the property if it is not overridden by !important rules in
+ // the transitions level.
+ if (IsEffectiveProperty(aEffects, property.mProperty)) {
+ result = &property;
+ }
+ return result;
+ }
+ return nullptr;
+}
+
+bool KeyframeEffect::HasEffectiveAnimationOfPropertySet(
+ const nsCSSPropertyIDSet& aPropertySet, const EffectSet& aEffectSet) const {
+ for (const AnimationProperty& property : mProperties) {
+ if (aPropertySet.HasProperty(property.mProperty) &&
+ IsEffectiveProperty(aEffectSet, property.mProperty)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsCSSPropertyIDSet KeyframeEffect::GetPropertiesForCompositor(
+ EffectSet& aEffects, const nsIFrame* aFrame) const {
+ MOZ_ASSERT(&aEffects ==
+ EffectSet::Get(mTarget.mElement, mTarget.mPseudoType));
+
+ nsCSSPropertyIDSet properties;
+
+ if (!mAnimation || !mAnimation->IsRelevant()) {
+ return properties;
+ }
+
+ static constexpr nsCSSPropertyIDSet compositorAnimatables =
+ nsCSSPropertyIDSet::CompositorAnimatables();
+ static constexpr nsCSSPropertyIDSet transformLikeProperties =
+ nsCSSPropertyIDSet::TransformLikeProperties();
+
+ nsCSSPropertyIDSet transformSet;
+ AnimationPerformanceWarning::Type dummyWarning;
+
+ for (const AnimationProperty& property : mProperties) {
+ if (!compositorAnimatables.HasProperty(property.mProperty)) {
+ continue;
+ }
+
+ // Transform-like properties are combined together on the compositor so we
+ // need to evaluate them as a group. We build up a separate set here then
+ // evaluate it as a separate step below.
+ if (transformLikeProperties.HasProperty(property.mProperty)) {
+ transformSet.AddProperty(property.mProperty);
+ continue;
+ }
+
+ KeyframeEffect::MatchForCompositor matchResult = IsMatchForCompositor(
+ nsCSSPropertyIDSet{property.mProperty}, aFrame, aEffects, dummyWarning);
+ if (matchResult ==
+ KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty ||
+ matchResult == KeyframeEffect::MatchForCompositor::No) {
+ continue;
+ }
+ properties.AddProperty(property.mProperty);
+ }
+
+ if (!transformSet.IsEmpty()) {
+ KeyframeEffect::MatchForCompositor matchResult =
+ IsMatchForCompositor(transformSet, aFrame, aEffects, dummyWarning);
+ if (matchResult == KeyframeEffect::MatchForCompositor::Yes ||
+ matchResult == KeyframeEffect::MatchForCompositor::IfNeeded) {
+ properties |= transformSet;
+ }
+ }
+
+ return properties;
+}
+
+nsCSSPropertyIDSet KeyframeEffect::GetPropertySet() const {
+ nsCSSPropertyIDSet result;
+
+ for (const AnimationProperty& property : mProperties) {
+ result.AddProperty(property.mProperty);
+ }
+
+ return result;
+}
+
+#ifdef DEBUG
+bool SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
+ const nsTArray<Keyframe>& aB) {
+ if (aA.Length() != aB.Length()) {
+ return false;
+ }
+
+ for (size_t i = 0; i < aA.Length(); i++) {
+ const Keyframe& a = aA[i];
+ const Keyframe& b = aB[i];
+ if (a.mOffset != b.mOffset || a.mTimingFunction != b.mTimingFunction ||
+ a.mPropertyValues != b.mPropertyValues) {
+ return false;
+ }
+ }
+
+ return true;
+}
+#endif
+
+static bool HasCurrentColor(
+ const nsTArray<AnimationPropertySegment>& aSegments) {
+ for (const AnimationPropertySegment& segment : aSegments) {
+ if ((!segment.mFromValue.IsNull() && segment.mFromValue.IsCurrentColor()) ||
+ (!segment.mToValue.IsNull() && segment.mToValue.IsCurrentColor())) {
+ return true;
+ }
+ }
+ return false;
+}
+void KeyframeEffect::UpdateProperties(const ComputedStyle* aStyle,
+ const AnimationTimeline* aTimeline) {
+ MOZ_ASSERT(aStyle);
+
+ nsTArray<AnimationProperty> properties = BuildProperties(aStyle);
+
+ bool propertiesChanged = mProperties != properties;
+
+ // We need to update base styles even if any properties are not changed at all
+ // since base styles might have been changed due to parent style changes, etc.
+ bool baseStylesChanged = false;
+ EnsureBaseStyles(aStyle, properties, aTimeline,
+ !propertiesChanged ? &baseStylesChanged : nullptr);
+
+ if (!propertiesChanged) {
+ if (baseStylesChanged) {
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+ }
+ // Check if we need to update the cumulative change hint because we now have
+ // style data.
+ if (mNeedsStyleData && mTarget && mTarget.mElement->HasServoData()) {
+ CalculateCumulativeChangeHint(aStyle);
+ }
+ return;
+ }
+
+ // Preserve the state of the mIsRunningOnCompositor flag.
+ nsCSSPropertyIDSet runningOnCompositorProperties;
+
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mIsRunningOnCompositor) {
+ runningOnCompositorProperties.AddProperty(property.mProperty);
+ }
+ }
+
+ mProperties = std::move(properties);
+ UpdateEffectSet();
+
+ mHasCurrentColor = false;
+
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor =
+ runningOnCompositorProperties.HasProperty(property.mProperty);
+
+ if (property.mProperty == eCSSProperty_background_color &&
+ !mHasCurrentColor) {
+ if (HasCurrentColor(property.mSegments)) {
+ mHasCurrentColor = true;
+ break;
+ }
+ }
+ }
+
+ CalculateCumulativeChangeHint(aStyle);
+
+ MarkCascadeNeedsUpdate();
+
+ if (mAnimation) {
+ mAnimation->NotifyEffectPropertiesUpdated();
+ }
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+void KeyframeEffect::EnsureBaseStyles(
+ const ComputedStyle* aComputedValues,
+ const nsTArray<AnimationProperty>& aProperties,
+ const AnimationTimeline* aTimeline, bool* aBaseStylesChanged) {
+ if (aBaseStylesChanged != nullptr) {
+ *aBaseStylesChanged = false;
+ }
+
+ if (!mTarget) {
+ return;
+ }
+
+ BaseValuesHashmap previousBaseStyles;
+ if (aBaseStylesChanged != nullptr) {
+ previousBaseStyles = std::move(mBaseValues);
+ }
+
+ mBaseValues.Clear();
+
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(mTarget.mElement);
+ // If |aProperties| is empty we're not going to dereference |presContext| so
+ // we don't care if it is nullptr.
+ //
+ // We could just return early when |aProperties| is empty and save looking up
+ // the pres context, but that won't save any effort normally since we don't
+ // call this function if we have no keyframes to begin with. Furthermore, the
+ // case where |presContext| is nullptr is so rare (we've only ever seen in
+ // fuzzing, and even then we've never been able to reproduce it reliably)
+ // it's not worth the runtime cost of an extra branch.
+ MOZ_ASSERT(presContext || aProperties.IsEmpty(),
+ "Typically presContext should not be nullptr but if it is"
+ " we should have also failed to calculate the computed values"
+ " passed-in as aProperties");
+
+ if (!aTimeline) {
+ // If we pass a valid timeline, we use it (note: this happens when we create
+ // a new animation or replace the old one, for CSS Animations and CSS
+ // Transitions). Otherwise, we check the timeline from |mAnimation|.
+ aTimeline = mAnimation ? mAnimation->GetTimeline() : nullptr;
+ }
+
+ RefPtr<const ComputedStyle> baseComputedStyle;
+ for (const AnimationProperty& property : aProperties) {
+ EnsureBaseStyle(property, presContext, aComputedValues, aTimeline,
+ baseComputedStyle);
+ }
+
+ if (aBaseStylesChanged != nullptr &&
+ std::any_of(
+ mBaseValues.cbegin(), mBaseValues.cend(), [&](const auto& entry) {
+ return AnimationValue(entry.GetData()) !=
+ AnimationValue(previousBaseStyles.Get(entry.GetKey()));
+ })) {
+ *aBaseStylesChanged = true;
+ }
+}
+
+void KeyframeEffect::EnsureBaseStyle(
+ const AnimationProperty& aProperty, nsPresContext* aPresContext,
+ const ComputedStyle* aComputedStyle, const AnimationTimeline* aTimeline,
+ RefPtr<const ComputedStyle>& aBaseComputedStyle) {
+ auto needBaseStyleForScrollTimeline =
+ [this](const AnimationProperty& aProperty,
+ const AnimationTimeline* aTimeline) {
+ static constexpr TimeDuration zeroDuration;
+ const TimingParams& timing = NormalizedTiming();
+ // For scroll-timeline with a positive delay, it's possible to scroll
+ // back and forth between delay phase and active phase, so we need to
+ // keep its base style and maybe use it to override the animations in
+ // delay on the compositor.
+ return aTimeline && aTimeline->IsScrollTimeline() &&
+ nsCSSPropertyIDSet::CompositorAnimatables().HasProperty(
+ aProperty.mProperty) &&
+ (timing.Delay() > zeroDuration ||
+ timing.EndDelay() > zeroDuration);
+ };
+ auto hasAdditiveValues = [](const AnimationProperty& aProperty) {
+ for (const AnimationPropertySegment& segment : aProperty.mSegments) {
+ if (!segment.HasReplaceableValues()) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Note: Check base style for compositor (i.e. for scroll-driven animations)
+ // first because it is much cleaper.
+ const bool needBaseStyle =
+ needBaseStyleForScrollTimeline(aProperty, aTimeline) ||
+ hasAdditiveValues(aProperty);
+ if (!needBaseStyle) {
+ return;
+ }
+
+ if (!aBaseComputedStyle) {
+ MOZ_ASSERT(mTarget, "Should have a valid target");
+
+ Element* animatingElement = AnimationUtils::GetElementForRestyle(
+ mTarget.mElement, mTarget.mPseudoType);
+ if (!animatingElement) {
+ return;
+ }
+ aBaseComputedStyle = aPresContext->StyleSet()->GetBaseContextForElement(
+ animatingElement, aComputedStyle);
+ }
+ RefPtr<StyleAnimationValue> baseValue =
+ Servo_ComputedValues_ExtractAnimationValue(aBaseComputedStyle,
+ aProperty.mProperty)
+ .Consume();
+ mBaseValues.InsertOrUpdate(aProperty.mProperty, std::move(baseValue));
+}
+
+void KeyframeEffect::WillComposeStyle() {
+ ComputedTiming computedTiming = GetComputedTiming();
+ mProgressOnLastCompose = computedTiming.mProgress;
+ mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+}
+
+void KeyframeEffect::ComposeStyleRule(StyleAnimationValueMap& aAnimationValues,
+ const AnimationProperty& aProperty,
+ const AnimationPropertySegment& aSegment,
+ const ComputedTiming& aComputedTiming) {
+ auto* opaqueTable =
+ reinterpret_cast<RawServoAnimationValueTable*>(&mBaseValues);
+ Servo_AnimationCompose(&aAnimationValues, opaqueTable, aProperty.mProperty,
+ &aSegment, &aProperty.mSegments.LastElement(),
+ &aComputedTiming, mEffectOptions.mIterationComposite);
+}
+
+void KeyframeEffect::ComposeStyle(StyleAnimationValueMap& aComposeResult,
+ const nsCSSPropertyIDSet& aPropertiesToSkip) {
+ ComputedTiming computedTiming = GetComputedTiming();
+
+ // If the progress is null, we don't have fill data for the current
+ // time so we shouldn't animate.
+ if (computedTiming.mProgress.IsNull()) {
+ return;
+ }
+
+ for (size_t propIdx = 0, propEnd = mProperties.Length(); propIdx != propEnd;
+ ++propIdx) {
+ const AnimationProperty& prop = mProperties[propIdx];
+
+ MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
+ MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
+ "incorrect last to key");
+
+ if (aPropertiesToSkip.HasProperty(prop.mProperty)) {
+ continue;
+ }
+
+ MOZ_ASSERT(prop.mSegments.Length() > 0,
+ "property should not be in animations if it has no segments");
+
+ // FIXME: Maybe cache the current segment?
+ const AnimationPropertySegment *segment = prop.mSegments.Elements(),
+ *segmentEnd =
+ segment + prop.mSegments.Length();
+ while (segment->mToKey <= computedTiming.mProgress.Value()) {
+ MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+ if ((segment + 1) == segmentEnd) {
+ break;
+ }
+ ++segment;
+ MOZ_ASSERT(segment->mFromKey == (segment - 1)->mToKey, "incorrect keys");
+ }
+ MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+ MOZ_ASSERT(segment >= prop.mSegments.Elements() &&
+ size_t(segment - prop.mSegments.Elements()) <
+ prop.mSegments.Length(),
+ "out of array bounds");
+
+ ComposeStyleRule(aComposeResult, prop, *segment, computedTiming);
+ }
+
+ // If the animation produces a change hint that affects the overflow region,
+ // we need to record the current time to unthrottle the animation
+ // periodically when the animation is being throttled because it's scrolled
+ // out of view.
+ if (HasPropertiesThatMightAffectOverflow()) {
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(mTarget.mElement);
+ EffectSet* effectSet =
+ EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ if (presContext && effectSet) {
+ TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh();
+ effectSet->UpdateLastOverflowAnimationSyncTime(now);
+ }
+ }
+}
+
+bool KeyframeEffect::IsRunningOnCompositor() const {
+ // We consider animation is running on compositor if there is at least
+ // one property running on compositor.
+ // Animation.IsRunningOnCompotitor will return more fine grained
+ // information in bug 1196114.
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mIsRunningOnCompositor) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void KeyframeEffect::SetIsRunningOnCompositor(nsCSSPropertyID aProperty,
+ bool aIsRunning) {
+ MOZ_ASSERT(
+ nsCSSProps::PropHasFlags(aProperty, CSSPropFlags::CanAnimateOnCompositor),
+ "Property being animated on compositor is a recognized "
+ "compositor-animatable property");
+ for (AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty) {
+ property.mIsRunningOnCompositor = aIsRunning;
+ // We currently only set a performance warning message when animations
+ // cannot be run on the compositor, so if this animation is running
+ // on the compositor we don't need a message.
+ if (aIsRunning) {
+ property.mPerformanceWarning.reset();
+ } else if (mAnimation && mAnimation->IsPartialPrerendered()) {
+ ResetPartialPrerendered();
+ }
+ return;
+ }
+ }
+}
+
+void KeyframeEffect::SetIsRunningOnCompositor(
+ const nsCSSPropertyIDSet& aPropertySet, bool aIsRunning) {
+ for (AnimationProperty& property : mProperties) {
+ if (aPropertySet.HasProperty(property.mProperty)) {
+ MOZ_ASSERT(nsCSSProps::PropHasFlags(property.mProperty,
+ CSSPropFlags::CanAnimateOnCompositor),
+ "Property being animated on compositor is a recognized "
+ "compositor-animatable property");
+ property.mIsRunningOnCompositor = aIsRunning;
+ // We currently only set a performance warning message when animations
+ // cannot be run on the compositor, so if this animation is running
+ // on the compositor we don't need a message.
+ if (aIsRunning) {
+ property.mPerformanceWarning.reset();
+ }
+ }
+ }
+
+ if (!aIsRunning && mAnimation && mAnimation->IsPartialPrerendered()) {
+ ResetPartialPrerendered();
+ }
+}
+
+void KeyframeEffect::ResetIsRunningOnCompositor() {
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor = false;
+ }
+
+ if (mAnimation && mAnimation->IsPartialPrerendered()) {
+ ResetPartialPrerendered();
+ }
+}
+
+void KeyframeEffect::ResetPartialPrerendered() {
+ MOZ_ASSERT(mAnimation && mAnimation->IsPartialPrerendered());
+
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) {
+ return;
+ }
+
+ nsIWidget* widget = frame->GetNearestWidget();
+ if (!widget) {
+ return;
+ }
+
+ if (WindowRenderer* windowRenderer = widget->GetWindowRenderer()) {
+ windowRenderer->RemovePartialPrerenderedAnimation(
+ mAnimation->IdOnCompositor(), mAnimation);
+ }
+}
+
+static const KeyframeEffectOptions& KeyframeEffectOptionsFromUnion(
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) {
+ MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+ return aOptions.GetAsKeyframeEffectOptions();
+}
+
+static const KeyframeEffectOptions& KeyframeEffectOptionsFromUnion(
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) {
+ MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+ return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+static KeyframeEffectParams KeyframeEffectParamsFromUnion(
+ const OptionsType& aOptions, CallerType aCallerType, ErrorResult& aRv) {
+ KeyframeEffectParams result;
+ if (aOptions.IsUnrestrictedDouble()) {
+ return result;
+ }
+
+ const KeyframeEffectOptions& options =
+ KeyframeEffectOptionsFromUnion(aOptions);
+
+ // If dom.animations-api.compositing.enabled is turned off,
+ // iterationComposite and composite are the default value 'replace' in the
+ // dictionary.
+ result.mIterationComposite = options.mIterationComposite;
+ result.mComposite = options.mComposite;
+
+ result.mPseudoType = PseudoStyleType::NotPseudo;
+ if (DOMStringIsNull(options.mPseudoElement)) {
+ return result;
+ }
+
+ Maybe<PseudoStyleType> pseudoType =
+ nsCSSPseudoElements::GetPseudoType(options.mPseudoElement);
+ if (!pseudoType) {
+ // Per the spec, we throw SyntaxError for syntactically invalid pseudos.
+ aRv.ThrowSyntaxError(
+ nsPrintfCString("'%s' is a syntactically invalid pseudo-element.",
+ NS_ConvertUTF16toUTF8(options.mPseudoElement).get()));
+ return result;
+ }
+
+ result.mPseudoType = *pseudoType;
+ if (!AnimationUtils::IsSupportedPseudoForAnimations(result.mPseudoType)) {
+ // Per the spec, we throw SyntaxError for unsupported pseudos.
+ aRv.ThrowSyntaxError(
+ nsPrintfCString("'%s' is an unsupported pseudo-element.",
+ NS_ConvertUTF16toUTF8(options.mPseudoElement).get()));
+ }
+
+ return result;
+}
+
+template <class OptionsType>
+/* static */
+already_AddRefed<KeyframeEffect> KeyframeEffect::ConstructKeyframeEffect(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes, const OptionsType& aOptions,
+ ErrorResult& aRv) {
+ // We should get the document from `aGlobal` instead of the current Realm
+ // to make this works in Xray case.
+ //
+ // In all non-Xray cases, `aGlobal` matches the current Realm, so this
+ // matches the spec behavior.
+ //
+ // In Xray case, the new objects should be created using the document of
+ // the target global, but the KeyframeEffect constructors are called in the
+ // caller's compartment to access `aKeyframes` object.
+ Document* doc = AnimationUtils::GetDocumentFromGlobal(aGlobal.Get());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ KeyframeEffectParams effectOptions =
+ KeyframeEffectParamsFromUnion(aOptions, aGlobal.CallerType(), aRv);
+ // An invalid Pseudo-element aborts all further steps.
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ TimingParams timingParams = TimingParams::FromOptionsUnion(aOptions, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<KeyframeEffect> effect = new KeyframeEffect(
+ doc, OwningAnimationTarget(aTarget, effectOptions.mPseudoType),
+ std::move(timingParams), effectOptions);
+
+ effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return effect.forget();
+}
+
+nsTArray<AnimationProperty> KeyframeEffect::BuildProperties(
+ const ComputedStyle* aStyle) {
+ MOZ_ASSERT(aStyle);
+
+ nsTArray<AnimationProperty> result;
+ // If mTarget is false (i.e. mTarget.mElement is null), return an empty
+ // property array.
+ if (!mTarget) {
+ return result;
+ }
+
+ // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes
+ // calculate computed values from |mKeyframes|, they could possibly
+ // trigger a subsequent restyle in which we rebuild animations. If that
+ // happens we could find that |mKeyframes| is overwritten while it is
+ // being iterated over. Normally that shouldn't happen but just in case we
+ // make a copy of |mKeyframes| first and iterate over that instead.
+ auto keyframesCopy(mKeyframes.Clone());
+
+ result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+ keyframesCopy, mTarget.mElement, mTarget.mPseudoType, aStyle,
+ mEffectOptions.mComposite);
+
+#ifdef DEBUG
+ MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
+ "Apart from the computed offset members, the keyframes array"
+ " should not be modified");
+#endif
+
+ mKeyframes = std::move(keyframesCopy);
+ return result;
+}
+
+template <typename FrameEnumFunc>
+static void EnumerateContinuationsOrIBSplitSiblings(nsIFrame* aFrame,
+ FrameEnumFunc&& aFunc) {
+ while (aFrame) {
+ aFunc(aFrame);
+ aFrame = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(aFrame);
+ }
+}
+
+void KeyframeEffect::UpdateTarget(Element* aElement,
+ PseudoStyleType aPseudoType) {
+ OwningAnimationTarget newTarget(aElement, aPseudoType);
+
+ if (mTarget == newTarget) {
+ // Assign the same target, skip it.
+ return;
+ }
+
+ if (mTarget) {
+ // Call ResetIsRunningOnCompositor() prior to UnregisterTarget() since
+ // ResetIsRunningOnCompositor() might try to get the EffectSet associated
+ // with this keyframe effect to remove partial pre-render animation from
+ // the layer manager.
+ ResetIsRunningOnCompositor();
+ UnregisterTarget();
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ nsAutoAnimationMutationBatch mb(mTarget.mElement->OwnerDoc());
+ if (mAnimation) {
+ MutationObservers::NotifyAnimationRemoved(mAnimation);
+ }
+ }
+
+ mTarget = newTarget;
+
+ if (mTarget) {
+ UpdateTargetRegistration();
+ RefPtr<const ComputedStyle> computedStyle =
+ GetTargetComputedStyle(Flush::None);
+ if (computedStyle) {
+ UpdateProperties(computedStyle);
+ }
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ nsAutoAnimationMutationBatch mb(mTarget.mElement->OwnerDoc());
+ if (mAnimation) {
+ MutationObservers::NotifyAnimationAdded(mAnimation);
+ mAnimation->ReschedulePendingTasks();
+ }
+ }
+
+ if (mAnimation) {
+ mAnimation->NotifyEffectTargetUpdated();
+ }
+}
+
+void KeyframeEffect::UpdateTargetRegistration() {
+ if (!mTarget) {
+ return;
+ }
+
+ bool isRelevant = mAnimation && mAnimation->IsRelevant();
+
+ // Animation::IsRelevant() returns a cached value. It only updates when
+ // something calls Animation::UpdateRelevance. Whenever our timing changes,
+ // we should be notifying our Animation before calling this, so
+ // Animation::IsRelevant() should be up-to-date by the time we get here.
+ MOZ_ASSERT(isRelevant ==
+ ((IsCurrent() || IsInEffect()) && mAnimation &&
+ mAnimation->ReplaceState() != AnimationReplaceState::Removed),
+ "Out of date Animation::IsRelevant value");
+
+ if (isRelevant && !mInEffectSet) {
+ EffectSet* effectSet =
+ EffectSet::GetOrCreate(mTarget.mElement, mTarget.mPseudoType);
+ effectSet->AddEffect(*this);
+ mInEffectSet = true;
+ UpdateEffectSet(effectSet);
+ nsIFrame* frame = GetPrimaryFrame();
+ EnumerateContinuationsOrIBSplitSiblings(
+ frame, [](nsIFrame* aFrame) { aFrame->MarkNeedsDisplayItemRebuild(); });
+ } else if (!isRelevant && mInEffectSet) {
+ UnregisterTarget();
+ }
+}
+
+void KeyframeEffect::UnregisterTarget() {
+ if (!mInEffectSet) {
+ return;
+ }
+
+ EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ MOZ_ASSERT(effectSet,
+ "If mInEffectSet is true, there must be an EffectSet"
+ " on the target element");
+ mInEffectSet = false;
+ if (effectSet) {
+ effectSet->RemoveEffect(*this);
+
+ if (effectSet->IsEmpty()) {
+ EffectSet::DestroyEffectSet(mTarget.mElement, mTarget.mPseudoType);
+ }
+ }
+ nsIFrame* frame = GetPrimaryFrame();
+ EnumerateContinuationsOrIBSplitSiblings(
+ frame, [](nsIFrame* aFrame) { aFrame->MarkNeedsDisplayItemRebuild(); });
+}
+
+void KeyframeEffect::RequestRestyle(
+ EffectCompositor::RestyleType aRestyleType) {
+ if (!mTarget) {
+ return;
+ }
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(mTarget.mElement);
+ if (presContext && mAnimation) {
+ presContext->EffectCompositor()->RequestRestyle(
+ mTarget.mElement, mTarget.mPseudoType, aRestyleType,
+ mAnimation->CascadeLevel());
+ }
+}
+
+already_AddRefed<const ComputedStyle> KeyframeEffect::GetTargetComputedStyle(
+ Flush aFlushType) const {
+ if (!GetRenderedDocument()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mTarget,
+ "Should only have a document when we have a target element");
+
+ OwningAnimationTarget kungfuDeathGrip(mTarget.mElement, mTarget.mPseudoType);
+
+ return aFlushType == Flush::Style
+ ? nsComputedDOMStyle::GetComputedStyle(mTarget.mElement,
+ mTarget.mPseudoType)
+ : nsComputedDOMStyle::GetComputedStyleNoFlush(mTarget.mElement,
+ mTarget.mPseudoType);
+}
+
+#ifdef DEBUG
+void DumpAnimationProperties(
+ const StylePerDocumentStyleData* aRawData,
+ nsTArray<AnimationProperty>& aAnimationProperties) {
+ for (auto& p : aAnimationProperties) {
+ printf("%s\n", nsCString(nsCSSProps::GetStringValue(p.mProperty)).get());
+ for (auto& s : p.mSegments) {
+ nsAutoCString fromValue, toValue;
+ s.mFromValue.SerializeSpecifiedValue(p.mProperty, aRawData, fromValue);
+ s.mToValue.SerializeSpecifiedValue(p.mProperty, aRawData, toValue);
+ printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey, fromValue.get(),
+ toValue.get());
+ }
+ }
+}
+#endif
+
+/* static */
+already_AddRefed<KeyframeEffect> KeyframeEffect::Constructor(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv) {
+ return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, aOptions, aRv);
+}
+
+/* static */
+already_AddRefed<KeyframeEffect> KeyframeEffect::Constructor(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv) {
+ return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, aOptions, aRv);
+}
+
+/* static */
+already_AddRefed<KeyframeEffect> KeyframeEffect::Constructor(
+ const GlobalObject& aGlobal, KeyframeEffect& aSource, ErrorResult& aRv) {
+ Document* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Create a new KeyframeEffect object with aSource's target,
+ // iteration composite operation, composite operation, and spacing mode.
+ // The constructor creates a new AnimationEffect object by
+ // aSource's TimingParams.
+ // Note: we don't need to re-throw exceptions since the value specified on
+ // aSource's timing object can be assumed valid.
+ RefPtr<KeyframeEffect> effect =
+ new KeyframeEffect(doc, OwningAnimationTarget{aSource.mTarget}, aSource);
+ // Copy cumulative change hint. mCumulativeChangeHint should be the same as
+ // the source one because both of targets are the same.
+ effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
+
+ return effect.forget();
+}
+
+void KeyframeEffect::SetPseudoElement(const nsAString& aPseudoElement,
+ ErrorResult& aRv) {
+ if (DOMStringIsNull(aPseudoElement)) {
+ UpdateTarget(mTarget.mElement, PseudoStyleType::NotPseudo);
+ return;
+ }
+
+ // Note: GetPseudoType() returns Some(NotPseudo) for the null string,
+ // so we handle null case before this.
+ Maybe<PseudoStyleType> pseudoType =
+ nsCSSPseudoElements::GetPseudoType(aPseudoElement);
+ if (!pseudoType || *pseudoType == PseudoStyleType::NotPseudo) {
+ // Per the spec, we throw SyntaxError for syntactically invalid pseudos.
+ aRv.ThrowSyntaxError(
+ nsPrintfCString("'%s' is a syntactically invalid pseudo-element.",
+ NS_ConvertUTF16toUTF8(aPseudoElement).get()));
+ return;
+ }
+
+ if (!AnimationUtils::IsSupportedPseudoForAnimations(*pseudoType)) {
+ // Per the spec, we throw SyntaxError for unsupported pseudos.
+ aRv.ThrowSyntaxError(
+ nsPrintfCString("'%s' is an unsupported pseudo-element.",
+ NS_ConvertUTF16toUTF8(aPseudoElement).get()));
+ return;
+ }
+
+ UpdateTarget(mTarget.mElement, *pseudoType);
+}
+
+static void CreatePropertyValue(
+ nsCSSPropertyID aProperty, float aOffset,
+ const Maybe<StyleComputedTimingFunction>& aTimingFunction,
+ const AnimationValue& aValue, dom::CompositeOperation aComposite,
+ const StylePerDocumentStyleData* aRawData,
+ AnimationPropertyValueDetails& aResult) {
+ aResult.mOffset = aOffset;
+
+ if (!aValue.IsNull()) {
+ nsAutoCString stringValue;
+ aValue.SerializeSpecifiedValue(aProperty, aRawData, stringValue);
+ aResult.mValue.Construct(stringValue);
+ }
+
+ if (aTimingFunction) {
+ aResult.mEasing.Construct();
+ aTimingFunction->AppendToString(aResult.mEasing.Value());
+ } else {
+ aResult.mEasing.Construct("linear"_ns);
+ }
+
+ aResult.mComposite = aComposite;
+}
+
+void KeyframeEffect::GetProperties(
+ nsTArray<AnimationPropertyDetails>& aProperties, ErrorResult& aRv) const {
+ const StylePerDocumentStyleData* rawData =
+ mDocument->StyleSetForPresShellOrMediaQueryEvaluation()->RawData();
+
+ for (const AnimationProperty& property : mProperties) {
+ AnimationPropertyDetails propertyDetails;
+ propertyDetails.mProperty =
+ NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
+ propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
+
+ nsAutoString localizedString;
+ if (property.mPerformanceWarning &&
+ property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+ propertyDetails.mWarning.Construct(localizedString);
+ }
+
+ if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
+ mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
+ segmentIdx < segmentLen; segmentIdx++) {
+ const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
+
+ binding_detail::FastAnimationPropertyValueDetails fromValue;
+ CreatePropertyValue(property.mProperty, segment.mFromKey,
+ segment.mTimingFunction, segment.mFromValue,
+ segment.mFromComposite, rawData, fromValue);
+ // We don't apply timing functions for zero-length segments, so
+ // don't return one here.
+ if (segment.mFromKey == segment.mToKey) {
+ fromValue.mEasing.Reset();
+ }
+ // Even though we called SetCapacity before, this could fail, since we
+ // might add multiple elements to propertyDetails.mValues for an element
+ // of property.mSegments in the cases mentioned below.
+ if (!propertyDetails.mValues.AppendElement(fromValue,
+ mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ // Normally we can ignore the to-value for this segment since it is
+ // identical to the from-value from the next segment. However, we need
+ // to add it if either:
+ // a) this is the last segment, or
+ // b) the next segment's from-value differs.
+ if (segmentIdx == segmentLen - 1 ||
+ property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
+ binding_detail::FastAnimationPropertyValueDetails toValue;
+ CreatePropertyValue(property.mProperty, segment.mToKey, Nothing(),
+ segment.mToValue, segment.mToComposite, rawData,
+ toValue);
+ // It doesn't really make sense to have a timing function on the
+ // last property value or before a sudden jump so we just drop the
+ // easing property altogether.
+ toValue.mEasing.Reset();
+ if (!propertyDetails.mValues.AppendElement(toValue,
+ mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+ }
+
+ aProperties.AppendElement(propertyDetails);
+ }
+}
+
+void KeyframeEffect::GetKeyframes(JSContext* aCx, nsTArray<JSObject*>& aResult,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ bool isCSSAnimation = mAnimation && mAnimation->AsCSSAnimation();
+
+ // For Servo, when we have CSS Animation @keyframes with variables, we convert
+ // shorthands to longhands if needed, and store a reference to the unparsed
+ // value. When it comes time to serialize, however, what do you serialize for
+ // a longhand that comes from a variable reference in a shorthand? Servo says,
+ // "an empty string" which is not particularly helpful.
+ //
+ // We should just store shorthands as-is (bug 1391537) and then return the
+ // variable references, but for now, since we don't do that, and in order to
+ // be consistent with Gecko, we just expand the variables (assuming we have
+ // enough context to do so). For that we need to grab the ComputedStyle so we
+ // know what custom property values to provide.
+ RefPtr<const ComputedStyle> computedStyle;
+ if (isCSSAnimation) {
+ // The following will flush style but that's ok since if you update
+ // a variable's computed value, you expect to see that updated value in the
+ // result of getKeyframes().
+ //
+ // If we don't have a target, the following will return null. In that case
+ // we might end up returning variables as-is or empty string. That should be
+ // acceptable however, since such a case is rare and this is only
+ // short-term (and unshipped) behavior until bug 1391537 is fixed.
+ computedStyle = GetTargetComputedStyle(Flush::Style);
+ }
+
+ const StylePerDocumentStyleData* rawData =
+ mDocument->StyleSetForPresShellOrMediaQueryEvaluation()->RawData();
+
+ for (const Keyframe& keyframe : mKeyframes) {
+ // Set up a dictionary object for the explicit members
+ BaseComputedKeyframe keyframeDict;
+ if (keyframe.mOffset) {
+ keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
+ }
+ MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Invalid computed offset");
+ keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
+ if (keyframe.mTimingFunction) {
+ keyframeDict.mEasing.Truncate();
+ keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
+ } // else if null, leave easing as its default "linear".
+
+ // With the pref off (i.e. dom.animations-api.compositing.enabled:false),
+ // the dictionary-to-JS conversion will skip this member entirely.
+ keyframeDict.mComposite = keyframe.mComposite;
+
+ JS::Rooted<JS::Value> keyframeJSValue(aCx);
+ if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<StyleLockedDeclarationBlock> customProperties;
+ // A workaround for CSS Animations in servo backend, custom properties in
+ // keyframe are stored in a servo's declaration block. Find the declaration
+ // block to resolve CSS variables in the keyframe.
+ // This workaround will be solved by bug 1391537.
+ if (isCSSAnimation) {
+ for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+ if (propertyValue.mProperty ==
+ nsCSSPropertyID::eCSSPropertyExtra_variable) {
+ customProperties = propertyValue.mServoDeclarationBlock;
+ break;
+ }
+ }
+ }
+
+ JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
+ for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+ nsAutoCString stringValue;
+ // Don't serialize the custom properties for this keyframe.
+ if (propertyValue.mProperty ==
+ nsCSSPropertyID::eCSSPropertyExtra_variable) {
+ continue;
+ }
+ if (propertyValue.mServoDeclarationBlock) {
+ Servo_DeclarationBlock_SerializeOneValue(
+ propertyValue.mServoDeclarationBlock, propertyValue.mProperty,
+ &stringValue, computedStyle, customProperties, rawData);
+ } else {
+ if (auto* value = mBaseValues.GetWeak(propertyValue.mProperty)) {
+ Servo_AnimationValue_Serialize(value, propertyValue.mProperty,
+ rawData, &stringValue);
+ }
+ }
+
+ // Basically, we need to do the mapping:
+ // * eCSSProperty_offset => "cssOffset"
+ // * eCSSProperty_float => "cssFloat"
+ // This means if property refers to the CSS "offset"/"float" property,
+ // return the string "cssOffset"/"cssFloat". (So avoid overlapping
+ // "offset" property in BaseKeyframe.)
+ // https://drafts.csswg.org/web-animations/#property-name-conversion
+ const char* name = nullptr;
+ switch (propertyValue.mProperty) {
+ case nsCSSPropertyID::eCSSProperty_offset:
+ name = "cssOffset";
+ break;
+ case nsCSSPropertyID::eCSSProperty_float:
+ // FIXME: Bug 1582314: Should handle cssFloat manually if we remove it
+ // from nsCSSProps::PropertyIDLName().
+ default:
+ name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
+ }
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!NonVoidUTF8StringToJsval(aCx, stringValue, &value) ||
+ !JS_DefineProperty(aCx, keyframeObject, name, value,
+ JSPROP_ENUMERATE)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+
+ aResult.AppendElement(keyframeObject);
+ }
+}
+
+/* static */ const TimeDuration
+KeyframeEffect::OverflowRegionRefreshInterval() {
+ // The amount of time we can wait between updating throttled animations
+ // on the main thread that influence the overflow region.
+ static const TimeDuration kOverflowRegionRefreshInterval =
+ TimeDuration::FromMilliseconds(200);
+
+ return kOverflowRegionRefreshInterval;
+}
+
+static bool IsDefinitivelyInvisibleDueToOpacity(const nsIFrame& aFrame) {
+ if (!aFrame.Style()->IsInOpacityZeroSubtree()) {
+ return false;
+ }
+
+ // Find the root of the opacity: 0 subtree.
+ const nsIFrame* root = &aFrame;
+ while (true) {
+ auto* parent = root->GetInFlowParent();
+ if (!parent || !parent->Style()->IsInOpacityZeroSubtree()) {
+ break;
+ }
+ root = parent;
+ }
+
+ MOZ_ASSERT(root && root->Style()->IsInOpacityZeroSubtree());
+
+ // If aFrame is the root of the opacity: zero subtree, we can't prove we can
+ // optimize it away, because it may have an opacity animation itself.
+ if (root == &aFrame) {
+ return false;
+ }
+
+ // Even if we're in an opacity: zero subtree, if the root of the subtree may
+ // have an opacity animation, we can't optimize us away, as we may become
+ // visible ourselves.
+ return !root->HasAnimationOfOpacity();
+}
+
+static bool CanOptimizeAwayDueToOpacity(const KeyframeEffect& aEffect,
+ const nsIFrame& aFrame) {
+ if (!aFrame.Style()->IsInOpacityZeroSubtree()) {
+ return false;
+ }
+ if (IsDefinitivelyInvisibleDueToOpacity(aFrame)) {
+ return true;
+ }
+ return !aEffect.HasOpacityChange() && !aFrame.HasAnimationOfOpacity();
+}
+
+bool KeyframeEffect::CanThrottleIfNotVisible(nsIFrame& aFrame) const {
+ // Unless we are newly in-effect, we can throttle the animation if the
+ // animation is paint only and the target frame is out of view or the document
+ // is in background tabs.
+ if (!mInEffectOnLastAnimationTimingUpdate || !CanIgnoreIfNotVisible()) {
+ return false;
+ }
+
+ PresShell* presShell = GetPresShell();
+ if (presShell && !presShell->IsActive()) {
+ return true;
+ }
+
+ const bool isVisibilityHidden =
+ !aFrame.IsVisibleOrMayHaveVisibleDescendants();
+ const bool canOptimizeAwayVisibility =
+ isVisibilityHidden && !HasVisibilityChange();
+
+ const bool invisible = canOptimizeAwayVisibility ||
+ CanOptimizeAwayDueToOpacity(*this, aFrame) ||
+ aFrame.IsScrolledOutOfView();
+ if (!invisible) {
+ return false;
+ }
+
+ // If there are no overflow change hints, we don't need to worry about
+ // unthrottling the animation periodically to update scrollbar positions for
+ // the overflow region.
+ if (!HasPropertiesThatMightAffectOverflow()) {
+ return true;
+ }
+
+ // Don't throttle finite animations since the animation might suddenly
+ // come into view and if it was throttled it will be out-of-sync.
+ if (HasFiniteActiveDuration()) {
+ return false;
+ }
+
+ return isVisibilityHidden ? CanThrottleOverflowChangesInScrollable(aFrame)
+ : CanThrottleOverflowChanges(aFrame);
+}
+
+bool KeyframeEffect::CanThrottle() const {
+ // Unthrottle if we are not in effect or current. This will be the case when
+ // our owning animation has finished, is idle, or when we are in the delay
+ // phase (but without a backwards fill). In each case the computed progress
+ // value produced on each tick will be the same so we will skip requesting
+ // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get
+ // here will be because of a change in state (e.g. we are newly finished or
+ // newly no longer in effect) in which case we shouldn't throttle the sample.
+ if (!IsInEffect() || !IsCurrent()) {
+ return false;
+ }
+
+ nsIFrame* const frame = GetStyleFrame();
+ if (!frame) {
+ // There are two possible cases here.
+ // a) No target element
+ // b) The target element has no frame, e.g. because it is in a display:none
+ // subtree.
+ // In either case we can throttle the animation because there is no
+ // need to update on the main thread.
+ return true;
+ }
+
+ // Do not throttle any animations during print preview.
+ if (frame->PresContext()->IsPrintingOrPrintPreview()) {
+ return false;
+ }
+
+ if (CanThrottleIfNotVisible(*frame)) {
+ return true;
+ }
+
+ EffectSet* effectSet = nullptr;
+ for (const AnimationProperty& property : mProperties) {
+ if (!property.mIsRunningOnCompositor) {
+ return false;
+ }
+
+ MOZ_ASSERT(nsCSSPropertyIDSet::CompositorAnimatables().HasProperty(
+ property.mProperty),
+ "The property should be able to run on the compositor");
+ if (!effectSet) {
+ effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ MOZ_ASSERT(effectSet,
+ "CanThrottle should be called on an effect "
+ "associated with a target element");
+ }
+ MOZ_ASSERT(HasEffectiveAnimationOfProperty(property.mProperty, *effectSet),
+ "There should be an effective animation of the property while "
+ "it is marked as being run on the compositor");
+
+ DisplayItemType displayItemType =
+ LayerAnimationInfo::GetDisplayItemTypeForProperty(property.mProperty);
+
+ // Note that AnimationInfo::GetGenarationFromFrame() is supposed to work
+ // with the primary frame instead of the style frame.
+ Maybe<uint64_t> generation = layers::AnimationInfo::GetGenerationFromFrame(
+ GetPrimaryFrame(), displayItemType);
+ // Unthrottle if the animation needs to be brought up to date
+ if (!generation || effectSet->GetAnimationGeneration() != *generation) {
+ return false;
+ }
+
+ // If this is a transform animation that affects the overflow region,
+ // we should unthrottle the animation periodically.
+ if (HasPropertiesThatMightAffectOverflow() &&
+ !CanThrottleOverflowChangesInScrollable(*frame)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool KeyframeEffect::CanThrottleOverflowChanges(const nsIFrame& aFrame) const {
+ TimeStamp now = aFrame.PresContext()->RefreshDriver()->MostRecentRefresh();
+
+ EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ MOZ_ASSERT(effectSet,
+ "CanOverflowTransformChanges is expected to be called"
+ " on an effect in an effect set");
+ MOZ_ASSERT(mAnimation,
+ "CanOverflowTransformChanges is expected to be called"
+ " on an effect with a parent animation");
+ TimeStamp lastSyncTime = effectSet->LastOverflowAnimationSyncTime();
+ // If this animation can cause overflow, we can throttle some of the ticks.
+ return (!lastSyncTime.IsNull() &&
+ (now - lastSyncTime) < OverflowRegionRefreshInterval());
+}
+
+bool KeyframeEffect::CanThrottleOverflowChangesInScrollable(
+ nsIFrame& aFrame) const {
+ // If the target element is not associated with any documents, we don't care
+ // it.
+ Document* doc = GetRenderedDocument();
+ if (!doc) {
+ return true;
+ }
+
+ // If we know that the animation cannot cause overflow,
+ // we can just disable flushes for this animation.
+
+ // If we have no intersection observers, we don't care about overflow.
+ if (!doc->HasIntersectionObservers()) {
+ return true;
+ }
+
+ if (CanThrottleOverflowChanges(aFrame)) {
+ return true;
+ }
+
+ // If the nearest scrollable ancestor has overflow:hidden,
+ // we don't care about overflow.
+ nsIScrollableFrame* scrollable =
+ nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
+ if (!scrollable) {
+ return true;
+ }
+
+ ScrollStyles ss = scrollable->GetScrollStyles();
+ if (ss.mVertical == StyleOverflow::Hidden &&
+ ss.mHorizontal == StyleOverflow::Hidden &&
+ scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
+ return true;
+ }
+
+ return false;
+}
+
+nsIFrame* KeyframeEffect::GetStyleFrame() const {
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) {
+ return nullptr;
+ }
+
+ return nsLayoutUtils::GetStyleFrame(frame);
+}
+
+nsIFrame* KeyframeEffect::GetPrimaryFrame() const {
+ nsIFrame* frame = nullptr;
+ if (!mTarget) {
+ return frame;
+ }
+
+ if (mTarget.mPseudoType == PseudoStyleType::before) {
+ frame = nsLayoutUtils::GetBeforeFrame(mTarget.mElement);
+ } else if (mTarget.mPseudoType == PseudoStyleType::after) {
+ frame = nsLayoutUtils::GetAfterFrame(mTarget.mElement);
+ } else if (mTarget.mPseudoType == PseudoStyleType::marker) {
+ frame = nsLayoutUtils::GetMarkerFrame(mTarget.mElement);
+ } else {
+ frame = mTarget.mElement->GetPrimaryFrame();
+ MOZ_ASSERT(mTarget.mPseudoType == PseudoStyleType::NotPseudo,
+ "unknown mTarget.mPseudoType");
+ }
+
+ return frame;
+}
+
+Document* KeyframeEffect::GetRenderedDocument() const {
+ if (!mTarget) {
+ return nullptr;
+ }
+ return mTarget.mElement->GetComposedDoc();
+}
+
+PresShell* KeyframeEffect::GetPresShell() const {
+ Document* doc = GetRenderedDocument();
+ if (!doc) {
+ return nullptr;
+ }
+ return doc->GetPresShell();
+}
+
+/* static */
+bool KeyframeEffect::IsGeometricProperty(const nsCSSPropertyID aProperty) {
+ MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty),
+ "Property should be a longhand property");
+
+ switch (aProperty) {
+ case eCSSProperty_bottom:
+ case eCSSProperty_height:
+ case eCSSProperty_left:
+ case eCSSProperty_margin_bottom:
+ case eCSSProperty_margin_left:
+ case eCSSProperty_margin_right:
+ case eCSSProperty_margin_top:
+ case eCSSProperty_padding_bottom:
+ case eCSSProperty_padding_left:
+ case eCSSProperty_padding_right:
+ case eCSSProperty_padding_top:
+ case eCSSProperty_right:
+ case eCSSProperty_top:
+ case eCSSProperty_width:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* static */
+bool KeyframeEffect::CanAnimateTransformOnCompositor(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) {
+ // In some cases, such as when we are simply collecting all the compositor
+ // animations regardless of the frame on which they run in order to calculate
+ // change hints, |aFrame| will be the style frame. However, even in that case
+ // we should look at the primary frame since that is where the transform will
+ // be applied.
+ const nsIFrame* primaryFrame =
+ nsLayoutUtils::GetPrimaryFrameFromStyleFrame(aFrame);
+
+ // Note that testing BackfaceIsHidden() is not a sufficient test for
+ // what we need for animating backface-visibility correctly if we
+ // remove the above test for Extend3DContext(); that would require
+ // looking at backface-visibility on descendants as well. See bug 1186204.
+ if (primaryFrame->BackfaceIsHidden()) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
+ return false;
+ }
+ // Async 'transform' animations of aFrames with SVG transforms is not
+ // supported. See bug 779599.
+ if (primaryFrame->IsSVGTransformed()) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
+ return false;
+ }
+
+ return true;
+}
+
+bool KeyframeEffect::ShouldBlockAsyncTransformAnimations(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const {
+ EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ // The various transform properties ('transform', 'scale' etc.) get combined
+ // on the compositor.
+ //
+ // As a result, if we have an animation of 'scale' and 'translate', but the
+ // 'translate' property is covered by an !important rule, we will not be
+ // able to combine the result on the compositor since we won't have the
+ // !important rule to incorporate. In that case we should run all the
+ // transform-related animations on the main thread (where we have the
+ // !important rule).
+ nsCSSPropertyIDSet blockedProperties =
+ effectSet->PropertiesWithImportantRules().Intersect(
+ effectSet->PropertiesForAnimationsLevel());
+ if (blockedProperties.Intersects(aPropertySet)) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformIsBlockedByImportantRules;
+ return true;
+ }
+
+ MOZ_ASSERT(mAnimation);
+ // Note: If the geometric animations are using scroll-timeline, we don't need
+ // to synchronize transform animations with them.
+ const bool enableMainthreadSynchronizationWithGeometricAnimations =
+ StaticPrefs::
+ dom_animations_mainthread_synchronization_with_geometric_animations() &&
+ !mAnimation->UsingScrollTimeline();
+
+ for (const AnimationProperty& property : mProperties) {
+ // If there is a property for animations level that is overridden by
+ // !important rules, it should not block other animations from running
+ // on the compositor.
+ // NOTE: We don't currently check for !important rules for properties that
+ // don't run on the compositor. As result such properties (e.g. margin-left)
+ // can still block async animations even if they are overridden by
+ // !important rules.
+ if (effectSet &&
+ effectSet->PropertiesWithImportantRules().HasProperty(
+ property.mProperty) &&
+ effectSet->PropertiesForAnimationsLevel().HasProperty(
+ property.mProperty)) {
+ continue;
+ }
+ // Check for geometric properties
+ if (enableMainthreadSynchronizationWithGeometricAnimations &&
+ IsGeometricProperty(property.mProperty)) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
+ return true;
+ }
+
+ // Check for unsupported transform animations
+ if (LayerAnimationInfo::GetCSSPropertiesFor(DisplayItemType::TYPE_TRANSFORM)
+ .HasProperty(property.mProperty)) {
+ if (!CanAnimateTransformOnCompositor(aFrame, aPerformanceWarning)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool KeyframeEffect::HasGeometricProperties() const {
+ for (const AnimationProperty& property : mProperties) {
+ if (IsGeometricProperty(property.mProperty)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void KeyframeEffect::SetPerformanceWarning(
+ const nsCSSPropertyIDSet& aPropertySet,
+ const AnimationPerformanceWarning& aWarning) {
+ nsCSSPropertyIDSet curr = aPropertySet;
+ for (AnimationProperty& property : mProperties) {
+ if (!curr.HasProperty(property.mProperty)) {
+ continue;
+ }
+ property.SetPerformanceWarning(aWarning, mTarget.mElement);
+ curr.RemoveProperty(property.mProperty);
+ if (curr.IsEmpty()) {
+ return;
+ }
+ }
+}
+
+already_AddRefed<const ComputedStyle>
+KeyframeEffect::CreateComputedStyleForAnimationValue(
+ nsCSSPropertyID aProperty, const AnimationValue& aValue,
+ nsPresContext* aPresContext, const ComputedStyle* aBaseComputedStyle) {
+ MOZ_ASSERT(aBaseComputedStyle,
+ "CreateComputedStyleForAnimationValue needs to be called "
+ "with a valid ComputedStyle");
+
+ Element* elementForResolve = AnimationUtils::GetElementForRestyle(
+ mTarget.mElement, mTarget.mPseudoType);
+ // The element may be null if, for example, we target a pseudo-element that no
+ // longer exists.
+ if (!elementForResolve) {
+ return nullptr;
+ }
+
+ ServoStyleSet* styleSet = aPresContext->StyleSet();
+ return styleSet->ResolveServoStyleByAddingAnimation(
+ elementForResolve, aBaseComputedStyle, aValue.mServo);
+}
+
+void KeyframeEffect::CalculateCumulativeChangeHint(
+ const ComputedStyle* aComputedStyle) {
+ mCumulativeChangeHint = nsChangeHint(0);
+ mNeedsStyleData = false;
+
+ nsPresContext* presContext =
+ mTarget ? nsContentUtils::GetContextForContent(mTarget.mElement)
+ : nullptr;
+ if (!presContext) {
+ // Change hints make no sense if we're not rendered.
+ //
+ // Actually, we cannot even post them anywhere.
+ mNeedsStyleData = true;
+ return;
+ }
+
+ for (const AnimationProperty& property : mProperties) {
+ // For opacity property we don't produce any change hints that are not
+ // included in nsChangeHint_Hints_CanIgnoreIfNotVisible so we can throttle
+ // opacity animations regardless of the change they produce. This
+ // optimization is particularly important since it allows us to throttle
+ // opacity animations with missing 0%/100% keyframes.
+ if (property.mProperty == eCSSProperty_opacity) {
+ continue;
+ }
+
+ for (const AnimationPropertySegment& segment : property.mSegments) {
+ // In case composite operation is not 'replace' or value is null,
+ // we can't throttle animations which will not cause any layout changes
+ // on invisible elements because we can't calculate the change hint for
+ // such properties until we compose it.
+ if (!segment.HasReplaceableValues()) {
+ if (!nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
+ property.mProperty)) {
+ mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+ return;
+ }
+ // We try a little harder to optimize transform animations simply
+ // because they are so common (the second-most commonly animated
+ // property at the time of writing). So if we encounter a transform
+ // segment that needs composing with the underlying value, we just add
+ // all the change hints a transform animation is known to be able to
+ // generate.
+ mCumulativeChangeHint |=
+ nsChangeHint_ComprehensiveAddOrRemoveTransform |
+ nsChangeHint_UpdatePostTransformOverflow |
+ nsChangeHint_UpdateTransformLayer;
+ continue;
+ }
+
+ RefPtr<const ComputedStyle> fromContext =
+ CreateComputedStyleForAnimationValue(property.mProperty,
+ segment.mFromValue, presContext,
+ aComputedStyle);
+ if (!fromContext) {
+ mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+ mNeedsStyleData = true;
+ return;
+ }
+
+ RefPtr<const ComputedStyle> toContext =
+ CreateComputedStyleForAnimationValue(property.mProperty,
+ segment.mToValue, presContext,
+ aComputedStyle);
+ if (!toContext) {
+ mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+ mNeedsStyleData = true;
+ return;
+ }
+
+ uint32_t equalStructs = 0;
+ nsChangeHint changeHint =
+ fromContext->CalcStyleDifference(*toContext, &equalStructs);
+
+ mCumulativeChangeHint |= changeHint;
+ }
+ }
+}
+
+void KeyframeEffect::SetAnimation(Animation* aAnimation) {
+ if (mAnimation == aAnimation) {
+ return;
+ }
+
+ // Restyle for the old animation.
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ mAnimation = aAnimation;
+
+ UpdateNormalizedTiming();
+
+ // The order of these function calls is important:
+ // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check
+ // if it should create the effectSet or not, and MarkCascadeNeedsUpdate()
+ // needs a valid effectSet, so we should call them in this order.
+ if (mAnimation) {
+ mAnimation->UpdateRelevance();
+ }
+ NotifyAnimationTimingUpdated(PostRestyleMode::IfNeeded);
+ if (mAnimation) {
+ MarkCascadeNeedsUpdate();
+ }
+}
+
+bool KeyframeEffect::CanIgnoreIfNotVisible() const {
+ if (!StaticPrefs::dom_animations_offscreen_throttling()) {
+ return false;
+ }
+
+ // FIXME: For further sophisticated optimization we need to check
+ // change hint on the segment corresponding to computedTiming.progress.
+ return NS_IsHintSubset(mCumulativeChangeHint,
+ nsChangeHint_Hints_CanIgnoreIfNotVisible);
+}
+
+void KeyframeEffect::MarkCascadeNeedsUpdate() {
+ if (!mTarget) {
+ return;
+ }
+
+ EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ if (!effectSet) {
+ return;
+ }
+ effectSet->MarkCascadeNeedsUpdate();
+}
+
+/* static */
+bool KeyframeEffect::HasComputedTimingChanged(
+ const ComputedTiming& aComputedTiming,
+ IterationCompositeOperation aIterationComposite,
+ const Nullable<double>& aProgressOnLastCompose,
+ uint64_t aCurrentIterationOnLastCompose) {
+ // Typically we don't need to request a restyle if the progress hasn't
+ // changed since the last call to ComposeStyle. The one exception is if the
+ // iteration composite mode is 'accumulate' and the current iteration has
+ // changed, since that will often produce a different result.
+ return aComputedTiming.mProgress != aProgressOnLastCompose ||
+ (aIterationComposite == IterationCompositeOperation::Accumulate &&
+ aComputedTiming.mCurrentIteration != aCurrentIterationOnLastCompose);
+}
+
+bool KeyframeEffect::HasComputedTimingChanged() const {
+ ComputedTiming computedTiming = GetComputedTiming();
+ return HasComputedTimingChanged(
+ computedTiming, mEffectOptions.mIterationComposite,
+ mProgressOnLastCompose, mCurrentIterationOnLastCompose);
+}
+
+bool KeyframeEffect::ContainsAnimatedScale(const nsIFrame* aFrame) const {
+ // For display:table content, transform animations run on the table wrapper
+ // frame. If we are being passed a frame that doesn't support transforms
+ // (i.e. the inner table frame) we could just return false, but it possibly
+ // means we looked up the wrong EffectSet so for now we just assert instead.
+ MOZ_ASSERT(aFrame && aFrame->IsFrameOfType(nsIFrame::eSupportsCSSTransforms),
+ "We should be passed a frame that supports transforms");
+
+ if (!IsCurrent()) {
+ return false;
+ }
+
+ if (!mAnimation ||
+ mAnimation->ReplaceState() == AnimationReplaceState::Removed) {
+ return false;
+ }
+
+ for (const AnimationProperty& prop : mProperties) {
+ if (prop.mProperty != eCSSProperty_transform &&
+ prop.mProperty != eCSSProperty_scale &&
+ prop.mProperty != eCSSProperty_rotate) {
+ continue;
+ }
+
+ AnimationValue baseStyle = BaseStyle(prop.mProperty);
+ if (!baseStyle.IsNull()) {
+ gfx::MatrixScales size = baseStyle.GetScaleValue(aFrame);
+ if (size != gfx::MatrixScales()) {
+ return true;
+ }
+ }
+
+ // This is actually overestimate because there are some cases that combining
+ // the base value and from/to value produces 1:1 scale. But it doesn't
+ // really matter.
+ for (const AnimationPropertySegment& segment : prop.mSegments) {
+ if (!segment.mFromValue.IsNull()) {
+ gfx::MatrixScales from = segment.mFromValue.GetScaleValue(aFrame);
+ if (from != gfx::MatrixScales()) {
+ return true;
+ }
+ }
+ if (!segment.mToValue.IsNull()) {
+ gfx::MatrixScales to = segment.mToValue.GetScaleValue(aFrame);
+ if (to != gfx::MatrixScales()) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void KeyframeEffect::UpdateEffectSet(EffectSet* aEffectSet) const {
+ if (!mInEffectSet) {
+ return;
+ }
+
+ EffectSet* effectSet =
+ aEffectSet ? aEffectSet
+ : EffectSet::Get(mTarget.mElement, mTarget.mPseudoType);
+ if (!effectSet) {
+ return;
+ }
+
+ nsIFrame* styleFrame = GetStyleFrame();
+ if (HasAnimationOfPropertySet(nsCSSPropertyIDSet::OpacityProperties())) {
+ effectSet->SetMayHaveOpacityAnimation();
+ EnumerateContinuationsOrIBSplitSiblings(styleFrame, [](nsIFrame* aFrame) {
+ aFrame->SetMayHaveOpacityAnimation();
+ });
+ }
+
+ nsIFrame* primaryFrame = GetPrimaryFrame();
+ if (HasAnimationOfPropertySet(
+ nsCSSPropertyIDSet::TransformLikeProperties())) {
+ effectSet->SetMayHaveTransformAnimation();
+ // For table frames, it's not clear if we should iterate over the
+ // continuations of the table wrapper or the inner table frame.
+ //
+ // Fortunately, this is not currently an issue because we only split tables
+ // when printing (page breaks) where we don't run animations.
+ EnumerateContinuationsOrIBSplitSiblings(primaryFrame, [](nsIFrame* aFrame) {
+ aFrame->SetMayHaveTransformAnimation();
+ });
+ }
+}
+
+KeyframeEffect::MatchForCompositor KeyframeEffect::IsMatchForCompositor(
+ const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
+ const EffectSet& aEffects,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const {
+ MOZ_ASSERT(mAnimation);
+
+ if (!mAnimation->IsRelevant()) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+
+ if (mAnimation->ShouldBeSynchronizedWithMainThread(aPropertySet, aFrame,
+ aPerformanceWarning)) {
+ // For a given |aFrame|, we don't want some animations of |aProperty| to
+ // run on the compositor and others to run on the main thread, so if any
+ // need to be synchronized with the main thread, run them all there.
+ return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty;
+ }
+
+ if (mAnimation->UsingScrollTimeline()) {
+ const ScrollTimeline* scrollTimeline =
+ mAnimation->GetTimeline()->AsScrollTimeline();
+ // We don't send this animation to the compositor if
+ // 1. the APZ is disabled entirely or for the source, or
+ // 2. the associated scroll-timeline is inactive, or
+ // 3. the scrolling direction is not available (i.e. no scroll range).
+ // 4. the scroll style of the scroller is overflow:hidden.
+ if (!scrollTimeline->APZIsActiveForSource() ||
+ !scrollTimeline->IsActive() ||
+ !scrollTimeline->ScrollingDirectionIsAvailable() ||
+ scrollTimeline->SourceScrollStyle() == StyleOverflow::Hidden) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+
+ // FIXME: Bug 1818346. Support OMTA for view-timeline. We disable it for now
+ // because we need to make view-timeline-inset animations run on the OMTA as
+ // well before enable this.
+ if (scrollTimeline->IsViewTimeline()) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+ }
+
+ if (!HasEffectiveAnimationOfPropertySet(aPropertySet, aEffects)) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+
+ // If we know that the animation is not visible, we don't need to send the
+ // animation to the compositor.
+ if (!aFrame->IsVisibleOrMayHaveVisibleDescendants() ||
+ CanOptimizeAwayDueToOpacity(*this, *aFrame) ||
+ aFrame->IsScrolledOutOfView()) {
+ return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty;
+ }
+
+ if (aPropertySet.HasProperty(eCSSProperty_background_color)) {
+ if (!StaticPrefs::gfx_omta_background_color()) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+
+ // We don't yet support off-main-thread background-color animations on
+ // canvas frame or on <html> or <body> which genarate
+ // nsDisplayCanvasBackgroundColor or nsDisplaySolidColor display item.
+ if (aFrame->IsCanvasFrame() ||
+ (aFrame->GetContent() &&
+ (aFrame->GetContent()->IsHTMLElement(nsGkAtoms::body) ||
+ aFrame->GetContent()->IsHTMLElement(nsGkAtoms::html)))) {
+ return KeyframeEffect::MatchForCompositor::No;
+ }
+ }
+
+ // We can't run this background color animation on the compositor if there
+ // is any `current-color` keyframe.
+ if (mHasCurrentColor) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::HasCurrentColor;
+ return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty;
+ }
+
+ return mAnimation->IsPlaying() ? KeyframeEffect::MatchForCompositor::Yes
+ : KeyframeEffect::MatchForCompositor::IfNeeded;
+}
+
+} // namespace dom
+} // namespace mozilla