summaryrefslogtreecommitdiffstats
path: root/dom/animation/KeyframeEffectReadOnly.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/KeyframeEffectReadOnly.cpp')
-rw-r--r--dom/animation/KeyframeEffectReadOnly.cpp1430
1 files changed, 1430 insertions, 0 deletions
diff --git a/dom/animation/KeyframeEffectReadOnly.cpp b/dom/animation/KeyframeEffectReadOnly.cpp
new file mode 100644
index 000000000..639e0b2b0
--- /dev/null
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -0,0 +1,1430 @@
+/* -*- 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/KeyframeEffectReadOnly.h"
+
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+ // For UnrestrictedDoubleOrKeyframeAnimationOptions;
+#include "mozilla/dom/CSSPseudoElement.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/FloatingPoint.h" // For IsFinite
+#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "Layers.h" // For Layer
+#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement
+#include "nsContentUtils.h" // nsContentUtils::ReportToConsole
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
+#include "nsIPresShell.h"
+#include "nsIScriptError.h"
+
+namespace mozilla {
+
+bool
+PropertyValuePair::operator==(const PropertyValuePair& aOther) const
+{
+ if (mProperty != aOther.mProperty || mValue != aOther.mValue) {
+ 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(KeyframeEffectReadOnly,
+ AnimationEffectReadOnly,
+ mTarget)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly,
+ AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly)
+NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
+
+NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
+NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
+
+KeyframeEffectReadOnly::KeyframeEffectReadOnly(
+ nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ const TimingParams& aTiming,
+ const KeyframeEffectParams& aOptions)
+ : KeyframeEffectReadOnly(aDocument, aTarget,
+ new AnimationEffectTimingReadOnly(aDocument,
+ aTiming),
+ aOptions)
+{
+}
+
+KeyframeEffectReadOnly::KeyframeEffectReadOnly(
+ nsIDocument* aDocument,
+ const Maybe<OwningAnimationTarget>& aTarget,
+ AnimationEffectTimingReadOnly* aTiming,
+ const KeyframeEffectParams& aOptions)
+ : AnimationEffectReadOnly(aDocument, aTiming)
+ , mTarget(aTarget)
+ , mEffectOptions(aOptions)
+ , mInEffectOnLastAnimationTimingUpdate(false)
+ , mCumulativeChangeHint(nsChangeHint(0))
+{
+}
+
+JSObject*
+KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto)
+{
+ return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto);
+}
+
+IterationCompositeOperation
+KeyframeEffectReadOnly::IterationComposite() const
+{
+ return mEffectOptions.mIterationComposite;
+}
+
+CompositeOperation
+KeyframeEffectReadOnly::Composite() const
+{
+ return CompositeOperation::Replace;
+}
+
+void
+KeyframeEffectReadOnly::NotifyAnimationTimingUpdated()
+{
+ 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();
+ }
+
+ // Detect changes to "in effect" status since we need to recalculate the
+ // animation cascade for this element whenever that changes.
+ bool inEffect = IsInEffect();
+ if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
+ MarkCascadeNeedsUpdate();
+ mInEffectOnLastAnimationTimingUpdate = inEffect;
+ }
+
+ // Request restyle if necessary.
+ if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
+ EffectCompositor::RestyleType restyleType =
+ CanThrottle() ?
+ EffectCompositor::RestyleType::Throttled :
+ EffectCompositor::RestyleType::Standard;
+ RequestRestyle(restyleType);
+ }
+
+ // 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://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes
+void
+KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv)
+{
+ nsTArray<Keyframe> keyframes =
+ KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+ SetKeyframes(Move(keyframes), styleContext);
+}
+
+void
+KeyframeEffectReadOnly::SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+ nsStyleContext* aStyleContext)
+{
+ if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
+ return;
+ }
+
+ mKeyframes = Move(aKeyframes);
+ // Apply distribute spacing irrespective of the spacing mode. We will apply
+ // the specified spacing mode when we generate computed animation property
+ // values from the keyframes since both operations require a style context
+ // and need to be performed whenever the style context changes.
+ KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+
+ if (mAnimation && mAnimation->IsRelevant()) {
+ nsNodeUtils::AnimationChanged(mAnimation);
+ }
+
+ if (aStyleContext) {
+ UpdateProperties(aStyleContext);
+ MaybeUpdateFrameForCompositor();
+ }
+}
+
+const AnimationProperty*
+KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty(
+ nsCSSPropertyID aProperty) const
+{
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ for (size_t propIdx = 0, propEnd = mProperties.Length();
+ propIdx != propEnd; ++propIdx) {
+ if (aProperty == mProperties[propIdx].mProperty) {
+ const AnimationProperty* result = &mProperties[propIdx];
+ // Skip if there is a property of animation level that is overridden
+ // by !important rules.
+ if (effectSet &&
+ effectSet->PropertiesWithImportantRules()
+ .HasProperty(result->mProperty) &&
+ effectSet->PropertiesForAnimationsLevel()
+ .HasProperty(result->mProperty)) {
+ result = nullptr;
+ }
+ return result;
+ }
+ }
+ return nullptr;
+}
+
+bool
+KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const
+{
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty) {
+ return true;
+ }
+ }
+ return false;
+}
+
+#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
+
+void
+KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+
+ nsTArray<AnimationProperty> properties = BuildProperties(aStyleContext);
+
+ if (mProperties == properties) {
+ return;
+ }
+
+ // Preserve the state of the mIsRunningOnCompositor flag.
+ nsCSSPropertyIDSet runningOnCompositorProperties;
+
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mIsRunningOnCompositor) {
+ runningOnCompositorProperties.AddProperty(property.mProperty);
+ }
+ }
+
+ mProperties = Move(properties);
+
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor =
+ runningOnCompositorProperties.HasProperty(property.mProperty);
+ }
+
+ // FIXME (bug 1303235): Do this for Servo too
+ if (aStyleContext->PresContext()->StyleSet()->IsGecko()) {
+ CalculateCumulativeChangeHint(aStyleContext);
+ }
+
+ MarkCascadeNeedsUpdate();
+
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+void
+KeyframeEffectReadOnly::ComposeStyle(
+ RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip)
+{
+ ComputedTiming computedTiming = GetComputedTiming();
+ mProgressOnLastCompose = computedTiming.mProgress;
+ mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+
+ // 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");
+
+ if (!aStyleRule) {
+ // Allocate the style rule now that we know we have animation data.
+ aStyleRule = new AnimValuesStyleRule();
+ }
+
+ StyleAnimationValue fromValue = segment->mFromValue;
+ StyleAnimationValue toValue = segment->mToValue;
+ // Iteration composition for accumulate
+ if (mEffectOptions.mIterationComposite ==
+ IterationCompositeOperation::Accumulate &&
+ computedTiming.mCurrentIteration > 0) {
+ const AnimationPropertySegment& lastSegment =
+ prop.mSegments.LastElement();
+ // FIXME: Bug 1293492: Add a utility function to calculate both of
+ // below StyleAnimationValues.
+ DebugOnly<bool> accumulateResult =
+ StyleAnimationValue::Accumulate(prop.mProperty,
+ fromValue,
+ lastSegment.mToValue,
+ computedTiming.mCurrentIteration);
+ // We can't check the accumulation result in case of filter property.
+ // That's because some filter property can't accumulate,
+ // e.g. 'contrast(2) brightness(2)' onto 'brightness(1) contrast(1)'
+ // because of mismatch of the order.
+ MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
+ "could not accumulate value");
+ accumulateResult =
+ StyleAnimationValue::Accumulate(prop.mProperty,
+ toValue,
+ lastSegment.mToValue,
+ computedTiming.mCurrentIteration);
+ MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
+ "could not accumulate value");
+ }
+
+ // Special handling for zero-length segments
+ if (segment->mToKey == segment->mFromKey) {
+ if (computedTiming.mProgress.Value() < 0) {
+ aStyleRule->AddValue(prop.mProperty, Move(fromValue));
+ } else {
+ aStyleRule->AddValue(prop.mProperty, Move(toValue));
+ }
+ continue;
+ }
+
+ double positionInSegment =
+ (computedTiming.mProgress.Value() - segment->mFromKey) /
+ (segment->mToKey - segment->mFromKey);
+ double valuePosition =
+ ComputedTimingFunction::GetPortion(segment->mTimingFunction,
+ positionInSegment,
+ computedTiming.mBeforeFlag);
+
+ MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite");
+ StyleAnimationValue val;
+ if (StyleAnimationValue::Interpolate(prop.mProperty,
+ fromValue,
+ toValue,
+ valuePosition, val)) {
+ aStyleRule->AddValue(prop.mProperty, Move(val));
+ } else if (valuePosition < 0.5) {
+ aStyleRule->AddValue(prop.mProperty, Move(fromValue));
+ } else {
+ aStyleRule->AddValue(prop.mProperty, Move(toValue));
+ }
+ }
+}
+
+bool
+KeyframeEffectReadOnly::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
+KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID aProperty,
+ bool aIsRunning)
+{
+ MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
+ "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();
+ }
+ return;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::ResetIsRunningOnCompositor()
+{
+ for (AnimationProperty& property : mProperties) {
+ property.mIsRunningOnCompositor = false;
+ }
+}
+
+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,
+ nsAString& aInvalidPacedProperty,
+ ErrorResult& aRv)
+{
+ KeyframeEffectParams result;
+ if (!aOptions.IsUnrestrictedDouble()) {
+ const KeyframeEffectOptions& options =
+ KeyframeEffectOptionsFromUnion(aOptions);
+ KeyframeEffectParams::ParseSpacing(options.mSpacing,
+ result.mSpacingMode,
+ result.mPacedProperty,
+ aInvalidPacedProperty,
+ aRv);
+ // Ignore iterationComposite if the Web Animations API is not enabled,
+ // then the default value 'Replace' will be used.
+ if (AnimationUtils::IsCoreAPIEnabledForCaller()) {
+ result.mIterationComposite = options.mIterationComposite;
+ }
+ }
+ return result;
+}
+
+/* static */ Maybe<OwningAnimationTarget>
+KeyframeEffectReadOnly::ConvertTarget(
+ const Nullable<ElementOrCSSPseudoElement>& aTarget)
+{
+ // Return value optimization.
+ Maybe<OwningAnimationTarget> result;
+
+ if (aTarget.IsNull()) {
+ return result;
+ }
+
+ const ElementOrCSSPseudoElement& target = aTarget.Value();
+ MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
+ "Uninitialized target");
+
+ if (target.IsElement()) {
+ result.emplace(&target.GetAsElement());
+ } else {
+ RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement();
+ result.emplace(elem, target.GetAsCSSPseudoElement().GetType());
+ }
+ return result;
+}
+
+template <class KeyframeEffectType, class OptionsType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffectReadOnly::ConstructKeyframeEffect(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const OptionsType& aOptions,
+ ErrorResult& aRv)
+{
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ TimingParams timingParams =
+ TimingParams::FromOptionsUnion(aOptions, doc, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsAutoString invalidPacedProperty;
+ KeyframeEffectParams effectOptions =
+ KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!invalidPacedProperty.IsEmpty()) {
+ const char16_t* params[] = { invalidPacedProperty.get() };
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Animation"),
+ doc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "UnanimatablePacedProperty",
+ params, ArrayLength(params));
+ }
+
+ Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
+ RefPtr<KeyframeEffectType> effect =
+ new KeyframeEffectType(doc, target, timingParams, effectOptions);
+
+ effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return effect.forget();
+}
+
+template<class KeyframeEffectType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv)
+{
+ nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Create a new KeyframeEffectReadOnly object with aSource's target,
+ // iteration composite operation, composite operation, and spacing mode.
+ // The constructor creates a new AnimationEffect(ReadOnly) 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<KeyframeEffectType> effect =
+ new KeyframeEffectType(doc,
+ aSource.mTarget,
+ aSource.SpecifiedTiming(),
+ aSource.mEffectOptions);
+ // Copy cumulative change hint. mCumulativeChangeHint should be the same as
+ // the source one because both of targets are the same.
+ effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
+
+ // Copy aSource's keyframes and animation properties.
+ // Note: We don't call SetKeyframes directly, which might revise the
+ // computed offsets and rebuild the animation properties.
+ // FIXME: Bug 1314537: We have to make sure SharedKeyframeList is handled
+ // properly.
+ effect->mKeyframes = aSource.mKeyframes;
+ effect->mProperties = aSource.mProperties;
+ return effect.forget();
+}
+
+nsTArray<AnimationProperty>
+KeyframeEffectReadOnly::BuildProperties(nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+
+ nsTArray<AnimationProperty> result;
+ // If mTarget 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);
+
+ nsTArray<ComputedKeyframeValues> computedValues =
+ KeyframeUtils::GetComputedKeyframeValues(keyframesCopy,
+ mTarget->mElement,
+ aStyleContext);
+
+ if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+ KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced,
+ mEffectOptions.mPacedProperty,
+ computedValues, aStyleContext);
+ }
+
+ result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy,
+ computedValues,
+ aStyleContext);
+
+#ifdef DEBUG
+ MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
+ "Apart from the computed offset members, the keyframes array"
+ " should not be modified");
+#endif
+
+ mKeyframes.SwapElements(keyframesCopy);
+ return result;
+}
+
+void
+KeyframeEffectReadOnly::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(),
+ "Out of date Animation::IsRelevant value");
+
+ if (isRelevant) {
+ EffectSet* effectSet =
+ EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ effectSet->AddEffect(*this);
+ } else {
+ UnregisterTarget();
+ }
+}
+
+void
+KeyframeEffectReadOnly::UnregisterTarget()
+{
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ if (effectSet) {
+ effectSet->RemoveEffect(*this);
+ if (effectSet->IsEmpty()) {
+ EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::RequestRestyle(
+ EffectCompositor::RestyleType aRestyleType)
+{
+ nsPresContext* presContext = GetPresContext();
+ if (presContext && mTarget && mAnimation) {
+ presContext->EffectCompositor()->
+ RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
+ aRestyleType, mAnimation->CascadeLevel());
+ }
+}
+
+already_AddRefed<nsStyleContext>
+KeyframeEffectReadOnly::GetTargetStyleContext()
+{
+ nsIPresShell* shell = GetPresShell();
+ if (!shell) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mTarget,
+ "Should only have a presshell when we have a target element");
+
+ nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
+ ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
+ : nullptr;
+ return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement,
+ pseudo, shell);
+}
+
+#ifdef DEBUG
+void
+DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
+{
+ for (auto& p : aAnimationProperties) {
+ printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
+ for (auto& s : p.mSegments) {
+ nsString fromValue, toValue;
+ Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
+ s.mFromValue,
+ fromValue);
+ Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
+ s.mToValue,
+ toValue);
+ printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
+ NS_ConvertUTF16toUTF8(fromValue).get(),
+ NS_ConvertUTF16toUTF8(toValue).get());
+ }
+ }
+}
+#endif
+
+/* static */ already_AddRefed<KeyframeEffectReadOnly>
+KeyframeEffectReadOnly::Constructor(
+ const GlobalObject& aGlobal,
+ const Nullable<ElementOrCSSPseudoElement>& aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget,
+ aKeyframes, aOptions,
+ aRv);
+}
+
+/* static */ already_AddRefed<KeyframeEffectReadOnly>
+KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal,
+ KeyframeEffectReadOnly& aSource,
+ ErrorResult& aRv)
+{
+ return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aSource, aRv);
+}
+
+void
+KeyframeEffectReadOnly::GetTarget(
+ Nullable<OwningElementOrCSSPseudoElement>& aRv) const
+{
+ if (!mTarget) {
+ aRv.SetNull();
+ return;
+ }
+
+ switch (mTarget->mPseudoType) {
+ case CSSPseudoElementType::before:
+ case CSSPseudoElementType::after:
+ aRv.SetValue().SetAsCSSPseudoElement() =
+ CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement,
+ mTarget->mPseudoType);
+ break;
+
+ case CSSPseudoElementType::NotPseudo:
+ aRv.SetValue().SetAsElement() = mTarget->mElement;
+ break;
+
+ default:
+ NS_NOTREACHED("Animation of unsupported pseudo-type");
+ aRv.SetNull();
+ }
+}
+
+static void
+CreatePropertyValue(nsCSSPropertyID aProperty,
+ float aOffset,
+ const Maybe<ComputedTimingFunction>& aTimingFunction,
+ const StyleAnimationValue& aValue,
+ AnimationPropertyValueDetails& aResult)
+{
+ aResult.mOffset = aOffset;
+
+ nsString stringValue;
+ DebugOnly<bool> uncomputeResult =
+ StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
+ MOZ_ASSERT(uncomputeResult, "failed to uncompute value");
+ aResult.mValue = stringValue;
+
+ if (aTimingFunction) {
+ aResult.mEasing.Construct();
+ aTimingFunction->AppendToString(aResult.mEasing.Value());
+ } else {
+ aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
+ }
+
+ aResult.mComposite = CompositeOperation::Replace;
+}
+
+void
+KeyframeEffectReadOnly::GetProperties(
+ nsTArray<AnimationPropertyDetails>& aProperties,
+ ErrorResult& aRv) const
+{
+ for (const AnimationProperty& property : mProperties) {
+ AnimationPropertyDetails propertyDetails;
+ propertyDetails.mProperty =
+ NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
+ propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
+
+ nsXPIDLString 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,
+ 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();
+ }
+ // The following won't fail since we have already allocated the capacity
+ // above.
+ propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
+
+ // 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, 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();
+ propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
+ }
+ }
+
+ aProperties.AppendElement(propertyDetails);
+ }
+}
+
+void
+KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx,
+ nsTArray<JSObject*>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ 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".
+
+ JS::Rooted<JS::Value> keyframeJSValue(aCx);
+ if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
+ for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+
+ const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
+
+ // nsCSSValue::AppendToString does not accept shorthands properties but
+ // works with token stream values if we pass eCSSProperty_UNKNOWN as
+ // the property.
+ nsCSSPropertyID propertyForSerializing =
+ nsCSSProps::IsShorthand(propertyValue.mProperty)
+ ? eCSSProperty_UNKNOWN
+ : propertyValue.mProperty;
+
+ nsAutoString stringValue;
+ if (propertyValue.mServoDeclarationBlock) {
+ Servo_DeclarationBlock_SerializeOneValue(
+ propertyValue.mServoDeclarationBlock, &stringValue);
+ } else {
+ propertyValue.mValue.AppendToString(
+ propertyForSerializing, stringValue, nsCSSValue::eNormalized);
+ }
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, stringValue, &value) ||
+ !JS_DefineProperty(aCx, keyframeObject, name, value,
+ JSPROP_ENUMERATE)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+
+ aResult.AppendElement(keyframeObject);
+ }
+}
+
+/* static */ const TimeDuration
+KeyframeEffectReadOnly::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;
+}
+
+bool
+KeyframeEffectReadOnly::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* frame = GetAnimationFrame();
+ 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;
+ }
+
+ // 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 (CanIgnoreIfNotVisible()) {
+ nsIPresShell* presShell = GetPresShell();
+ if ((presShell && !presShell->IsActive()) ||
+ frame->IsScrolledOutOfView()) {
+ return true;
+ }
+ }
+
+ // First we need to check layer generation and transform overflow
+ // prior to the property.mIsRunningOnCompositor check because we should
+ // occasionally unthrottle these animations even if the animations are
+ // already running on compositor.
+ for (const LayerAnimationInfo::Record& record :
+ LayerAnimationInfo::sRecords) {
+ // Skip properties that are overridden by !important rules.
+ // (GetEffectiveAnimationOfProperty, as called by
+ // HasEffectiveAnimationOfProperty, only returns a property which is
+ // neither overridden by !important rules nor overridden by other
+ // animation.)
+ if (!HasEffectiveAnimationOfProperty(record.mProperty)) {
+ continue;
+ }
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
+ "associated with a target element");
+ layers::Layer* layer =
+ FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType);
+ // Unthrottle if the layer needs to be brought up to date
+ if (!layer ||
+ effectSet->GetAnimationGeneration() !=
+ layer->GetAnimationGeneration()) {
+ return false;
+ }
+
+ // If this is a transform animation that affects the overflow region,
+ // we should unthrottle the animation periodically.
+ if (record.mProperty == eCSSProperty_transform &&
+ !CanThrottleTransformChanges(*frame)) {
+ return false;
+ }
+ }
+
+ for (const AnimationProperty& property : mProperties) {
+ if (!property.mIsRunningOnCompositor) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool
+KeyframeEffectReadOnly::CanThrottleTransformChanges(nsIFrame& aFrame) const
+{
+ // If we know that the animation cannot cause overflow,
+ // we can just disable flushes for this animation.
+
+ // If we don't show scrollbars, we don't care about overflow.
+ if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0) {
+ return true;
+ }
+
+ nsPresContext* presContext = GetPresContext();
+ // CanThrottleTransformChanges is only called as part of a refresh driver tick
+ // in which case we expect to has a pres context.
+ MOZ_ASSERT(presContext);
+
+ TimeStamp now =
+ presContext->RefreshDriver()->MostRecentRefresh();
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
+ " on an effect in an effect set");
+ MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
+ " on an effect with a parent animation");
+ TimeStamp animationRuleRefreshTime =
+ effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel());
+ // If this animation can cause overflow, we can throttle some of the ticks.
+ if (!animationRuleRefreshTime.IsNull() &&
+ (now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) {
+ 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;
+ }
+
+ ScrollbarStyles ss = scrollable->GetScrollbarStyles();
+ if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
+ ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
+ scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
+ return true;
+ }
+
+ return false;
+}
+
+nsIFrame*
+KeyframeEffectReadOnly::GetAnimationFrame() const
+{
+ if (!mTarget) {
+ return nullptr;
+ }
+
+ nsIFrame* frame = mTarget->mElement->GetPrimaryFrame();
+ if (!frame) {
+ return nullptr;
+ }
+
+ if (mTarget->mPseudoType == CSSPseudoElementType::before) {
+ frame = nsLayoutUtils::GetBeforeFrame(frame);
+ } else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
+ frame = nsLayoutUtils::GetAfterFrame(frame);
+ } else {
+ MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
+ "unknown mTarget->mPseudoType");
+ }
+ if (!frame) {
+ return nullptr;
+ }
+
+ return nsLayoutUtils::GetStyleFrame(frame);
+}
+
+nsIDocument*
+KeyframeEffectReadOnly::GetRenderedDocument() const
+{
+ if (!mTarget) {
+ return nullptr;
+ }
+ return mTarget->mElement->GetComposedDoc();
+}
+
+nsIPresShell*
+KeyframeEffectReadOnly::GetPresShell() const
+{
+ nsIDocument* doc = GetRenderedDocument();
+ if (!doc) {
+ return nullptr;
+ }
+ return doc->GetShell();
+}
+
+nsPresContext*
+KeyframeEffectReadOnly::GetPresContext() const
+{
+ nsIPresShell* shell = GetPresShell();
+ if (!shell) {
+ return nullptr;
+ }
+ return shell->GetPresContext();
+}
+
+/* static */ bool
+KeyframeEffectReadOnly::IsGeometricProperty(
+ const nsCSSPropertyID aProperty)
+{
+ switch (aProperty) {
+ case eCSSProperty_bottom:
+ case eCSSProperty_height:
+ case eCSSProperty_left:
+ case eCSSProperty_right:
+ case eCSSProperty_top:
+ case eCSSProperty_width:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* static */ bool
+KeyframeEffectReadOnly::CanAnimateTransformOnCompositor(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning)
+{
+ // Disallow OMTA for preserve-3d transform. Note that we check the style property
+ // rather than Extend3DContext() since that can recurse back into this function
+ // via HasOpacity(). See bug 779598.
+ if (aFrame->Combines3DTransformWithAncestors() ||
+ aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
+ return false;
+ }
+ // 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 (aFrame->BackfaceIsHidden()) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
+ return false;
+ }
+ // Async 'transform' animations of aFrames with SVG transforms is not
+ // supported. See bug 779599.
+ if (aFrame->IsSVGTransformed()) {
+ aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
+ return false;
+ }
+
+ return true;
+}
+
+bool
+KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning) const
+{
+ // We currently only expect this method to be called for effects whose
+ // animations are eligible for the compositor since, Animations that are
+ // paused, zero-duration, finished etc. should not block other animations from
+ // running on the compositor.
+ MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor());
+
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+ 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 (IsGeometricProperty(property.mProperty)) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
+ return true;
+ }
+
+ // Check for unsupported transform animations
+ if (property.mProperty == eCSSProperty_transform) {
+ if (!CanAnimateTransformOnCompositor(aFrame,
+ aPerformanceWarning)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void
+KeyframeEffectReadOnly::SetPerformanceWarning(
+ nsCSSPropertyID aProperty,
+ const AnimationPerformanceWarning& aWarning)
+{
+ for (AnimationProperty& property : mProperties) {
+ if (property.mProperty == aProperty &&
+ (!property.mPerformanceWarning ||
+ *property.mPerformanceWarning != aWarning)) {
+ property.mPerformanceWarning = Some(aWarning);
+
+ nsXPIDLString localizedString;
+ if (nsLayoutUtils::IsAnimationLoggingEnabled() &&
+ property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+ nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
+ AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement);
+ }
+ return;
+ }
+ }
+}
+
+static already_AddRefed<nsStyleContext>
+CreateStyleContextForAnimationValue(nsCSSPropertyID aProperty,
+ const StyleAnimationValue& aValue,
+ nsStyleContext* aBaseStyleContext)
+{
+ MOZ_ASSERT(aBaseStyleContext,
+ "CreateStyleContextForAnimationValue needs to be called "
+ "with a valid nsStyleContext");
+
+ RefPtr<AnimValuesStyleRule> styleRule = new AnimValuesStyleRule();
+ styleRule->AddValue(aProperty, aValue);
+
+ nsCOMArray<nsIStyleRule> rules;
+ rules.AppendObject(styleRule);
+
+ MOZ_ASSERT(aBaseStyleContext->PresContext()->StyleSet()->IsGecko(),
+ "ServoStyleSet should not use StyleAnimationValue for animations");
+ nsStyleSet* styleSet =
+ aBaseStyleContext->PresContext()->StyleSet()->AsGecko();
+
+ RefPtr<nsStyleContext> styleContext =
+ styleSet->ResolveStyleByAddingRules(aBaseStyleContext, rules);
+
+ // We need to call StyleData to generate cached data for the style context.
+ // Otherwise CalcStyleDifference returns no meaningful result.
+ styleContext->StyleData(nsCSSProps::kSIDTable[aProperty]);
+
+ return styleContext.forget();
+}
+
+void
+KeyframeEffectReadOnly::CalculateCumulativeChangeHint(
+ nsStyleContext *aStyleContext)
+{
+ mCumulativeChangeHint = nsChangeHint(0);
+
+ for (const AnimationProperty& property : mProperties) {
+ for (const AnimationPropertySegment& segment : property.mSegments) {
+ RefPtr<nsStyleContext> fromContext =
+ CreateStyleContextForAnimationValue(property.mProperty,
+ segment.mFromValue, aStyleContext);
+
+ RefPtr<nsStyleContext> toContext =
+ CreateStyleContextForAnimationValue(property.mProperty,
+ segment.mToValue, aStyleContext);
+
+ uint32_t equalStructs = 0;
+ uint32_t samePointerStructs = 0;
+ nsChangeHint changeHint =
+ fromContext->CalcStyleDifference(toContext,
+ nsChangeHint(0),
+ &equalStructs,
+ &samePointerStructs);
+
+ mCumulativeChangeHint |= changeHint;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation)
+{
+ if (mAnimation == aAnimation) {
+ return;
+ }
+
+ // Restyle for the old animation.
+ RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+ mAnimation = aAnimation;
+
+ // 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();
+ if (mAnimation) {
+ MarkCascadeNeedsUpdate();
+ }
+}
+
+bool
+KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const
+{
+ if (!AnimationUtils::IsOffscreenThrottlingEnabled()) {
+ return false;
+ }
+
+ // FIXME (bug 1303235): We don't calculate mCumulativeChangeHint for
+ // the Servo backend yet
+ nsPresContext* presContext = GetPresContext();
+ if (!presContext || presContext->StyleSet()->IsServo()) {
+ 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
+KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor()
+{
+ nsIFrame* frame = GetAnimationFrame();
+ if (!frame) {
+ return;
+ }
+
+ // FIXME: Bug 1272495: If this effect does not win in the cascade, the
+ // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation
+ // will be removed from effect set or the transform keyframes are removed
+ // by setKeyframes. The latter case will be hard to solve though.
+ for (const AnimationProperty& property : mProperties) {
+ if (property.mProperty == eCSSProperty_transform) {
+ frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
+ return;
+ }
+ }
+}
+
+void
+KeyframeEffectReadOnly::MarkCascadeNeedsUpdate()
+{
+ if (!mTarget) {
+ return;
+ }
+
+ EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+ mTarget->mPseudoType);
+ if (!effectSet) {
+ return;
+ }
+ effectSet->MarkCascadeNeedsUpdate();
+}
+
+bool
+KeyframeEffectReadOnly::HasComputedTimingChanged() const
+{
+ // 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.
+ ComputedTiming computedTiming = GetComputedTiming();
+ return computedTiming.mProgress != mProgressOnLastCompose ||
+ (mEffectOptions.mIterationComposite ==
+ IterationCompositeOperation::Accumulate &&
+ computedTiming.mCurrentIteration !=
+ mCurrentIterationOnLastCompose);
+}
+
+} // namespace dom
+} // namespace mozilla