summaryrefslogtreecommitdiffstats
path: root/dom/animation/Animation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/Animation.cpp')
-rw-r--r--dom/animation/Animation.cpp1426
1 files changed, 1426 insertions, 0 deletions
diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp
new file mode 100644
index 000000000..6dd583ed1
--- /dev/null
+++ b/dom/animation/Animation.cpp
@@ -0,0 +1,1426 @@
+/* -*- 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 "Animation.h"
+#include "AnimationUtils.h"
+#include "mozilla/dom/AnimationBinding.h"
+#include "mozilla/dom/AnimationPlaybackEvent.h"
+#include "mozilla/dom/DocumentTimeline.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher
+#include "mozilla/Maybe.h" // For Maybe
+#include "nsAnimationManager.h" // For CSSAnimation
+#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
+#include "nsIDocument.h" // For nsIDocument
+#include "nsIPresShell.h" // For nsIPresShell
+#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
+#include "nsTransitionManager.h" // For CSSTransition
+#include "PendingAnimationTracker.h" // For PendingAnimationTracker
+
+namespace mozilla {
+namespace dom {
+
+// Static members
+uint64_t Animation::sNextAnimationIndex = 0;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper,
+ mTimeline,
+ mEffect,
+ mReady,
+ mFinished)
+
+NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Animation)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+JSObject*
+Animation::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return dom::AnimationBinding::Wrap(aCx, this, aGivenProto);
+}
+
+// ---------------------------------------------------------------------------
+//
+// Utility methods
+//
+// ---------------------------------------------------------------------------
+
+namespace {
+ // A wrapper around nsAutoAnimationMutationBatch that looks up the
+ // appropriate document from the supplied animation.
+ class MOZ_RAII AutoMutationBatchForAnimation {
+ public:
+ explicit AutoMutationBatchForAnimation(const Animation& aAnimation
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM) {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ Maybe<NonOwningAnimationTarget> target =
+ nsNodeUtils::GetTargetForAnimation(&aAnimation);
+ if (!target) {
+ return;
+ }
+
+ // For mutation observers, we use the OwnerDoc.
+ nsIDocument* doc = target->mElement->OwnerDoc();
+ if (!doc) {
+ return;
+ }
+
+ mAutoBatch.emplace(doc);
+ }
+
+ private:
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+ Maybe<nsAutoAnimationMutationBatch> mAutoBatch;
+ };
+}
+
+// ---------------------------------------------------------------------------
+//
+// Animation interface:
+//
+// ---------------------------------------------------------------------------
+/* static */ already_AddRefed<Animation>
+Animation::Constructor(const GlobalObject& aGlobal,
+ AnimationEffectReadOnly* aEffect,
+ const Optional<AnimationTimeline*>& aTimeline,
+ ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<Animation> animation = new Animation(global);
+
+ AnimationTimeline* timeline;
+ if (aTimeline.WasPassed()) {
+ timeline = aTimeline.Value();
+ } else {
+ nsIDocument* document =
+ AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!document) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ timeline = document->Timeline();
+ }
+
+ animation->SetTimelineNoUpdate(timeline);
+ animation->SetEffectNoUpdate(aEffect);
+
+ return animation.forget();
+}
+
+void
+Animation::SetId(const nsAString& aId)
+{
+ if (mId == aId) {
+ return;
+ }
+ mId = aId;
+ nsNodeUtils::AnimationChanged(this);
+}
+
+void
+Animation::SetEffect(AnimationEffectReadOnly* aEffect)
+{
+ SetEffectNoUpdate(aEffect);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#setting-the-target-effect
+void
+Animation::SetEffectNoUpdate(AnimationEffectReadOnly* aEffect)
+{
+ RefPtr<Animation> kungFuDeathGrip(this);
+
+ if (mEffect == aEffect) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+ bool wasRelevant = mIsRelevant;
+
+ if (mEffect) {
+ if (!aEffect) {
+ // If the new effect is null, call ResetPendingTasks before clearing
+ // mEffect since ResetPendingTasks needs it to get the appropriate
+ // PendingAnimationTracker.
+ ResetPendingTasks();
+ }
+
+ // We need to notify observers now because once we set mEffect to null
+ // we won't be able to find the target element to notify.
+ if (mIsRelevant) {
+ nsNodeUtils::AnimationRemoved(this);
+ }
+
+ // Break links with the old effect and then drop it.
+ RefPtr<AnimationEffectReadOnly> oldEffect = mEffect;
+ mEffect = nullptr;
+ oldEffect->SetAnimation(nullptr);
+
+ // The following will not do any notification because mEffect is null.
+ UpdateRelevance();
+ }
+
+ if (aEffect) {
+ // Break links from the new effect to its previous animation, if any.
+ RefPtr<AnimationEffectReadOnly> newEffect = aEffect;
+ Animation* prevAnim = aEffect->GetAnimation();
+ if (prevAnim) {
+ prevAnim->SetEffect(nullptr);
+ }
+
+ // Create links with the new effect. SetAnimation(this) will also update
+ // mIsRelevant of this animation, and then notify mutation observer if
+ // needed by calling Animation::UpdateRelevance(), so we don't need to
+ // call it again.
+ mEffect = newEffect;
+ mEffect->SetAnimation(this);
+
+ // Notify possible add or change.
+ // If the target is different, the change notification will be ignored by
+ // AutoMutationBatchForAnimation.
+ if (wasRelevant && mIsRelevant) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+
+ // Reschedule pending pause or pending play tasks.
+ // If we have a pending animation, it will either be registered
+ // in the pending animation tracker and have a null pending ready time,
+ // or, after it has been painted, it will be removed from the tracker
+ // and assigned a pending ready time.
+ // After updating the effect we'll typically need to repaint so if we've
+ // already been assigned a pending ready time, we should clear it and put
+ // the animation back in the tracker.
+ if (!mPendingReadyTime.IsNull()) {
+ mPendingReadyTime.SetNull();
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ if (mPendingState == PendingState::PlayPending) {
+ tracker->AddPlayPending(*this);
+ } else {
+ tracker->AddPausePending(*this);
+ }
+ }
+ }
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+}
+
+void
+Animation::SetTimeline(AnimationTimeline* aTimeline)
+{
+ SetTimelineNoUpdate(aTimeline);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#setting-the-timeline
+void
+Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline)
+{
+ if (mTimeline == aTimeline) {
+ return;
+ }
+
+ RefPtr<AnimationTimeline> oldTimeline = mTimeline;
+ if (oldTimeline) {
+ oldTimeline->RemoveAnimation(this);
+ }
+
+ mTimeline = aTimeline;
+ if (!mStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+}
+
+// https://w3c.github.io/web-animations/#set-the-animation-start-time
+void
+Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime)
+{
+ if (aNewStartTime == mStartTime) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> timelineTime;
+ if (mTimeline) {
+ // The spec says to check if the timeline is active (has a resolved time)
+ // before using it here, but we don't need to since it's harmless to set
+ // the already null time to null.
+ timelineTime = mTimeline->GetCurrentTime();
+ }
+ if (timelineTime.IsNull() && !aNewStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ Nullable<TimeDuration> previousCurrentTime = GetCurrentTime();
+ mStartTime = aNewStartTime;
+ if (!aNewStartTime.IsNull()) {
+ if (mPlaybackRate != 0.0) {
+ mHoldTime.SetNull();
+ }
+ } else {
+ mHoldTime = previousCurrentTime;
+ }
+
+ CancelPendingTasks();
+ if (mReady) {
+ // We may have already resolved mReady, but in that case calling
+ // MaybeResolve is a no-op, so that's okay.
+ mReady->MaybeResolve(this);
+ }
+
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#current-time
+Nullable<TimeDuration>
+Animation::GetCurrentTime() const
+{
+ Nullable<TimeDuration> result;
+ if (!mHoldTime.IsNull()) {
+ result = mHoldTime;
+ return result;
+ }
+
+ if (mTimeline && !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timelineTime = mTimeline->GetCurrentTime();
+ if (!timelineTime.IsNull()) {
+ result.SetValue((timelineTime.Value() - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ }
+ return result;
+}
+
+// https://w3c.github.io/web-animations/#set-the-current-time
+void
+Animation::SetCurrentTime(const TimeDuration& aSeekTime)
+{
+ // Return early if the current time has not changed. However, if we
+ // are pause-pending, then setting the current time to any value
+ // including the current value has the effect of aborting the
+ // pause so we should not return early in that case.
+ if (mPendingState != PendingState::PausePending &&
+ Nullable<TimeDuration>(aSeekTime) == GetCurrentTime()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ SilentlySetCurrentTime(aSeekTime);
+
+ if (mPendingState == PendingState::PausePending) {
+ // Finish the pause operation
+ mHoldTime.SetValue(aSeekTime);
+ mStartTime.SetNull();
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+ CancelPendingTasks();
+ }
+
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#set-the-animation-playback-rate
+void
+Animation::SetPlaybackRate(double aPlaybackRate)
+{
+ if (aPlaybackRate == mPlaybackRate) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> previousTime = GetCurrentTime();
+ mPlaybackRate = aPlaybackRate;
+ if (!previousTime.IsNull()) {
+ SetCurrentTime(previousTime.Value());
+ }
+
+ // In the case where GetCurrentTime() returns the same result before and
+ // after updating mPlaybackRate, SetCurrentTime will return early since,
+ // as far as it can tell, nothing has changed.
+ // As a result, we need to perform the following updates here:
+ // - update timing (since, if the sign of the playback rate has changed, our
+ // finished state may have changed),
+ // - dispatch a change notification for the changed playback rate, and
+ // - update the playback rate on animations on layers.
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#play-state
+AnimationPlayState
+Animation::PlayState() const
+{
+ if (mPendingState != PendingState::NotPending) {
+ return AnimationPlayState::Pending;
+ }
+
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ if (currentTime.IsNull()) {
+ return AnimationPlayState::Idle;
+ }
+
+ if (mStartTime.IsNull()) {
+ return AnimationPlayState::Paused;
+ }
+
+ if ((mPlaybackRate > 0.0 && currentTime.Value() >= EffectEnd()) ||
+ (mPlaybackRate < 0.0 && currentTime.Value() <= TimeDuration())) {
+ return AnimationPlayState::Finished;
+ }
+
+ return AnimationPlayState::Running;
+}
+
+Promise*
+Animation::GetReady(ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+ if (!mReady && global) {
+ mReady = Promise::Create(global, aRv); // Lazily create on demand
+ }
+ if (!mReady) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ } else if (PlayState() != AnimationPlayState::Pending) {
+ mReady->MaybeResolve(this);
+ }
+ return mReady;
+}
+
+Promise*
+Animation::GetFinished(ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+ if (!mFinished && global) {
+ mFinished = Promise::Create(global, aRv); // Lazily create on demand
+ }
+ if (!mFinished) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ } else if (mFinishedIsResolved) {
+ MaybeResolveFinishedPromise();
+ }
+ return mFinished;
+}
+
+void
+Animation::Cancel()
+{
+ CancelNoUpdate();
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#finish-an-animation
+void
+Animation::Finish(ErrorResult& aRv)
+{
+ if (mPlaybackRate == 0 ||
+ (mPlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ // Seek to the end
+ TimeDuration limit =
+ mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0);
+ bool didChange = GetCurrentTime() != Nullable<TimeDuration>(limit);
+ SilentlySetCurrentTime(limit);
+
+ // If we are paused or play-pending we need to fill in the start time in
+ // order to transition to the finished state.
+ //
+ // We only do this, however, if we have an active timeline. If we have an
+ // inactive timeline we can't transition into the finished state just like
+ // we can't transition to the running state (this finished state is really
+ // a substate of the running state).
+ if (mStartTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ limit.MultDouble(1.0 / mPlaybackRate));
+ didChange = true;
+ }
+
+ // If we just resolved the start time for a pause or play-pending
+ // animation, we need to clear the task. We don't do this as a branch of
+ // the above however since we can have a play-pending animation with a
+ // resolved start time if we aborted a pause operation.
+ if (!mStartTime.IsNull() &&
+ (mPendingState == PendingState::PlayPending ||
+ mPendingState == PendingState::PausePending)) {
+ if (mPendingState == PendingState::PausePending) {
+ mHoldTime.SetNull();
+ }
+ CancelPendingTasks();
+ didChange = true;
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+ }
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Sync);
+ if (didChange && IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+void
+Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior)
+{
+ PlayNoUpdate(aRv, aLimitBehavior);
+ PostUpdate();
+}
+
+void
+Animation::Pause(ErrorResult& aRv)
+{
+ PauseNoUpdate(aRv);
+ PostUpdate();
+}
+
+// https://w3c.github.io/web-animations/#reverse-an-animation
+void
+Animation::Reverse(ErrorResult& aRv)
+{
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (mPlaybackRate == 0.0) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ SilentlySetPlaybackRate(-mPlaybackRate);
+ Play(aRv, LimitBehavior::AutoRewind);
+
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+// ---------------------------------------------------------------------------
+//
+// JS wrappers for Animation interface:
+//
+// ---------------------------------------------------------------------------
+
+Nullable<double>
+Animation::GetStartTimeAsDouble() const
+{
+ return AnimationUtils::TimeDurationToDouble(mStartTime);
+}
+
+void
+Animation::SetStartTimeAsDouble(const Nullable<double>& aStartTime)
+{
+ return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime));
+}
+
+Nullable<double>
+Animation::GetCurrentTimeAsDouble() const
+{
+ return AnimationUtils::TimeDurationToDouble(GetCurrentTime());
+}
+
+void
+Animation::SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
+ ErrorResult& aRv)
+{
+ if (aCurrentTime.IsNull()) {
+ if (!GetCurrentTime().IsNull()) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ }
+ return;
+ }
+
+ return SetCurrentTime(TimeDuration::FromMilliseconds(aCurrentTime.Value()));
+}
+
+// ---------------------------------------------------------------------------
+
+void
+Animation::Tick()
+{
+ // Finish pending if we have a pending ready time, but only if we also
+ // have an active timeline.
+ if (mPendingState != PendingState::NotPending &&
+ !mPendingReadyTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ // Even though mPendingReadyTime is initialized using TimeStamp::Now()
+ // during the *previous* tick of the refresh driver, it can still be
+ // ahead of the *current* timeline time when we are using the
+ // vsync timer so we need to clamp it to the timeline time.
+ mPendingReadyTime.SetValue(std::min(mTimeline->GetCurrentTime().Value(),
+ mPendingReadyTime.Value()));
+ FinishPendingAt(mPendingReadyTime.Value());
+ mPendingReadyTime.SetNull();
+ }
+
+ if (IsPossiblyOrphanedPendingAnimation()) {
+ MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(),
+ "Orphaned pending animations should have an active timeline");
+ FinishPendingAt(mTimeline->GetCurrentTime().Value());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (!mEffect) {
+ return;
+ }
+
+ // Update layers if we are newly finished.
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect &&
+ !keyframeEffect->Properties().IsEmpty() &&
+ !mFinishedAtLastComposeStyle &&
+ PlayState() == AnimationPlayState::Finished) {
+ PostUpdate();
+ }
+}
+
+void
+Animation::TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime)
+{
+ // Normally we expect the play state to be pending but it's possible that,
+ // due to the handling of possibly orphaned animations in Tick(), this
+ // animation got started whilst still being in another document's pending
+ // animation map.
+ if (PlayState() != AnimationPlayState::Pending) {
+ return;
+ }
+
+ // If aReadyTime.IsNull() we'll detect this in Tick() where we check for
+ // orphaned animations and trigger this animation anyway
+ mPendingReadyTime = aReadyTime;
+}
+
+void
+Animation::TriggerNow()
+{
+ // Normally we expect the play state to be pending but when an animation
+ // is cancelled and its rendered document can't be reached, we can end up
+ // with the animation still in a pending player tracker even after it is
+ // no longer pending.
+ if (PlayState() != AnimationPlayState::Pending) {
+ return;
+ }
+
+ // If we don't have an active timeline we can't trigger the animation.
+ // However, this is a test-only method that we don't expect to be used in
+ // conjunction with animations without an active timeline so generate
+ // a warning if we do find ourselves in that situation.
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ NS_WARNING("Failed to trigger an animation with an active timeline");
+ return;
+ }
+
+ FinishPendingAt(mTimeline->GetCurrentTime().Value());
+}
+
+Nullable<TimeDuration>
+Animation::GetCurrentOrPendingStartTime() const
+{
+ Nullable<TimeDuration> result;
+
+ if (!mStartTime.IsNull()) {
+ result = mStartTime;
+ return result;
+ }
+
+ if (mPendingReadyTime.IsNull() || mHoldTime.IsNull()) {
+ return result;
+ }
+
+ // Calculate the equivalent start time from the pending ready time.
+ result = StartTimeFromReadyTime(mPendingReadyTime.Value());
+
+ return result;
+}
+
+TimeDuration
+Animation::StartTimeFromReadyTime(const TimeDuration& aReadyTime) const
+{
+ MOZ_ASSERT(!mHoldTime.IsNull(), "Hold time should be set in order to"
+ " convert a ready time to a start time");
+ if (mPlaybackRate == 0) {
+ return aReadyTime;
+ }
+ return aReadyTime - mHoldTime.Value().MultDouble(1 / mPlaybackRate);
+}
+
+TimeStamp
+Animation::AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const
+{
+ // Initializes to null. Return the same object every time to benefit from
+ // return-value-optimization.
+ TimeStamp result;
+
+ // We *don't* check for mTimeline->TracksWallclockTime() here because that
+ // method only tells us if the timeline times can be converted to
+ // TimeStamps that can be compared to TimeStamp::Now() or not, *not*
+ // whether the timelines can be converted to TimeStamp values at all.
+ //
+ // Furthermore, we want to be able to use this method when the refresh driver
+ // is under test control (in which case TracksWallclockTime() will return
+ // false).
+ //
+ // Once we introduce timelines that are not time-based we will need to
+ // differentiate between them here and determine how to sort their events.
+ if (!mTimeline) {
+ return result;
+ }
+
+ // Check the time is convertible to a timestamp
+ if (aTime == TimeDuration::Forever() ||
+ mPlaybackRate == 0.0 ||
+ mStartTime.IsNull()) {
+ return result;
+ }
+
+ // Invert the standard relation:
+ // animation time = (timeline time - start time) * playback rate
+ TimeDuration timelineTime =
+ TimeDuration(aTime).MultDouble(1.0 / mPlaybackRate) + mStartTime.Value();
+
+ result = mTimeline->ToTimeStamp(timelineTime);
+ return result;
+}
+
+TimeStamp
+Animation::ElapsedTimeToTimeStamp(
+ const StickyTimeDuration& aElapsedTime) const
+{
+ return AnimationTimeToTimeStamp(aElapsedTime +
+ mEffect->SpecifiedTiming().mDelay);
+}
+
+
+// https://w3c.github.io/web-animations/#silently-set-the-current-time
+void
+Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime)
+{
+ if (!mHoldTime.IsNull() ||
+ mStartTime.IsNull() ||
+ !mTimeline ||
+ mTimeline->GetCurrentTime().IsNull() ||
+ mPlaybackRate == 0.0) {
+ mHoldTime.SetValue(aSeekTime);
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ mStartTime.SetNull();
+ }
+ } else {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ (aSeekTime.MultDouble(1 / mPlaybackRate)));
+ }
+
+ mPreviousCurrentTime.SetNull();
+}
+
+void
+Animation::SilentlySetPlaybackRate(double aPlaybackRate)
+{
+ Nullable<TimeDuration> previousTime = GetCurrentTime();
+ mPlaybackRate = aPlaybackRate;
+ if (!previousTime.IsNull()) {
+ SilentlySetCurrentTime(previousTime.Value());
+ }
+}
+
+// https://w3c.github.io/web-animations/#cancel-an-animation
+void
+Animation::CancelNoUpdate()
+{
+ ResetPendingTasks();
+
+ if (mFinished) {
+ mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+ ResetFinishedPromise();
+
+ DispatchPlaybackEvent(NS_LITERAL_STRING("cancel"));
+
+ mHoldTime.SetNull();
+ mStartTime.SetNull();
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mTimeline) {
+ mTimeline->RemoveAnimation(this);
+ }
+}
+
+void
+Animation::UpdateRelevance()
+{
+ bool wasRelevant = mIsRelevant;
+ mIsRelevant = HasCurrentEffect() || IsInEffect();
+
+ // Notify animation observers.
+ if (wasRelevant && !mIsRelevant) {
+ nsNodeUtils::AnimationRemoved(this);
+ } else if (!wasRelevant && mIsRelevant) {
+ nsNodeUtils::AnimationAdded(this);
+ }
+}
+
+bool
+Animation::HasLowerCompositeOrderThan(const Animation& aOther) const
+{
+ // 0. Object-equality case
+ if (&aOther == this) {
+ return false;
+ }
+
+ // 1. CSS Transitions sort lowest
+ {
+ auto asCSSTransitionForSorting =
+ [] (const Animation& anim) -> const CSSTransition*
+ {
+ const CSSTransition* transition = anim.AsCSSTransition();
+ return transition && transition->IsTiedToMarkup() ?
+ transition :
+ nullptr;
+ };
+ auto thisTransition = asCSSTransitionForSorting(*this);
+ auto otherTransition = asCSSTransitionForSorting(aOther);
+ if (thisTransition && otherTransition) {
+ return thisTransition->HasLowerCompositeOrderThan(*otherTransition);
+ }
+ if (thisTransition || otherTransition) {
+ return thisTransition;
+ }
+ }
+
+ // 2. CSS Animations sort next
+ {
+ auto asCSSAnimationForSorting =
+ [] (const Animation& anim) -> const CSSAnimation*
+ {
+ const CSSAnimation* animation = anim.AsCSSAnimation();
+ return animation && animation->IsTiedToMarkup() ? animation : nullptr;
+ };
+ auto thisAnimation = asCSSAnimationForSorting(*this);
+ auto otherAnimation = asCSSAnimationForSorting(aOther);
+ if (thisAnimation && otherAnimation) {
+ return thisAnimation->HasLowerCompositeOrderThan(*otherAnimation);
+ }
+ if (thisAnimation || otherAnimation) {
+ return thisAnimation;
+ }
+ }
+
+ // Subclasses of Animation repurpose mAnimationIndex to implement their
+ // own brand of composite ordering. However, by this point we should have
+ // handled any such custom composite ordering so we should now have unique
+ // animation indices.
+ MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex,
+ "Animation indices should be unique");
+
+ // 3. Finally, generic animations sort by their position in the global
+ // animation array.
+ return mAnimationIndex < aOther.mAnimationIndex;
+}
+
+void
+Animation::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
+ const nsCSSPropertyIDSet& aPropertiesToSkip)
+{
+ if (!mEffect) {
+ return;
+ }
+
+ if (!IsInEffect()) {
+ return;
+ }
+
+ // In order to prevent flicker, there are a few cases where we want to use
+ // a different time for rendering that would otherwise be returned by
+ // GetCurrentTime. These are:
+ //
+ // (a) For animations that are pausing but which are still running on the
+ // compositor. In this case we send a layer transaction that removes the
+ // animation but which also contains the animation values calculated on
+ // the main thread. To prevent flicker when this occurs we want to ensure
+ // the timeline time used to calculate the main thread animation values
+ // does not lag far behind the time used on the compositor. Ideally we
+ // would like to use the "animation ready time" calculated at the end of
+ // the layer transaction as the timeline time but it will be too late to
+ // update the style rule at that point so instead we just use the current
+ // wallclock time.
+ //
+ // (b) For animations that are pausing that we have already taken off the
+ // compositor. In this case we record a pending ready time but we don't
+ // apply it until the next tick. However, while waiting for the next tick,
+ // we should still use the pending ready time as the timeline time. If we
+ // use the regular timeline time the animation may appear jump backwards
+ // if the main thread's timeline time lags behind the compositor.
+ //
+ // (c) For animations that are play-pending due to an aborted pause operation
+ // (i.e. a pause operation that was interrupted before we entered the
+ // paused state). When we cancel a pending pause we might momentarily take
+ // the animation off the compositor, only to re-add it moments later. In
+ // that case the compositor might have been ahead of the main thread so we
+ // should use the current wallclock time to ensure the animation doesn't
+ // temporarily jump backwards.
+ //
+ // To address each of these cases we temporarily tweak the hold time
+ // immediately before updating the style rule and then restore it immediately
+ // afterwards. This is purely to prevent visual flicker. Other behavior
+ // such as dispatching events continues to rely on the regular timeline time.
+ AnimationPlayState playState = PlayState();
+ {
+ AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime);
+
+ if (playState == AnimationPlayState::Pending &&
+ mHoldTime.IsNull() &&
+ !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timeToUse = mPendingReadyTime;
+ if (timeToUse.IsNull() &&
+ mTimeline &&
+ mTimeline->TracksWallclockTime()) {
+ timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now());
+ }
+ if (!timeToUse.IsNull()) {
+ mHoldTime.SetValue((timeToUse.Value() - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ }
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->ComposeStyle(aStyleRule, aPropertiesToSkip);
+ }
+ }
+
+ MOZ_ASSERT(playState == PlayState(),
+ "Play state should not change during the course of compositing");
+ mFinishedAtLastComposeStyle = (playState == AnimationPlayState::Finished);
+}
+
+void
+Animation::NotifyEffectTimingUpdated()
+{
+ MOZ_ASSERT(mEffect,
+ "We should only update timing effect when we have a target "
+ "effect");
+ UpdateTiming(Animation::SeekFlag::NoSeek,
+ Animation::SyncNotifyFlag::Async);
+}
+
+// https://w3c.github.io/web-animations/#play-an-animation
+void
+Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior)
+{
+ AutoMutationBatchForAnimation mb(*this);
+
+ bool abortedPause = mPendingState == PendingState::PausePending;
+
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ if (mPlaybackRate > 0.0 &&
+ (currentTime.IsNull() ||
+ (aLimitBehavior == LimitBehavior::AutoRewind &&
+ (currentTime.Value() < TimeDuration() ||
+ currentTime.Value() >= EffectEnd())))) {
+ mHoldTime.SetValue(TimeDuration(0));
+ } else if (mPlaybackRate < 0.0 &&
+ (currentTime.IsNull() ||
+ (aLimitBehavior == LimitBehavior::AutoRewind &&
+ (currentTime.Value() <= TimeDuration() ||
+ currentTime.Value() > EffectEnd())))) {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mHoldTime.SetValue(TimeDuration(EffectEnd()));
+ } else if (mPlaybackRate == 0.0 && currentTime.IsNull()) {
+ mHoldTime.SetValue(TimeDuration(0));
+ }
+
+ bool reuseReadyPromise = false;
+ if (mPendingState != PendingState::NotPending) {
+ CancelPendingTasks();
+ reuseReadyPromise = true;
+ }
+
+ // If the hold time is null then we're either already playing normally (and
+ // we can ignore this call) or we aborted a pending pause operation (in which
+ // case, for consistency, we need to go through the motions of doing an
+ // asynchronous start even though we already have a resolved start time).
+ if (mHoldTime.IsNull() && !abortedPause) {
+ return;
+ }
+
+ // Clear the start time until we resolve a new one. We do this except
+ // for the case where we are aborting a pause and don't have a hold time.
+ //
+ // If we're aborting a pause and *do* have a hold time (e.g. because
+ // the animation is finished or we just applied the auto-rewind behavior
+ // above) we should respect it by clearing the start time. If we *don't*
+ // have a hold time we should keep the current start time so that the
+ // the animation continues moving uninterrupted by the aborted pause.
+ //
+ // (If we're not aborting a pause, mHoldTime must be resolved by now
+ // or else we would have returned above.)
+ if (!mHoldTime.IsNull()) {
+ mStartTime.SetNull();
+ }
+
+ if (!reuseReadyPromise) {
+ // Clear ready promise. We'll create a new one lazily.
+ mReady = nullptr;
+ }
+
+ mPendingState = PendingState::PlayPending;
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPlayPending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+// https://w3c.github.io/web-animations/#pause-an-animation
+void
+Animation::PauseNoUpdate(ErrorResult& aRv)
+{
+ if (IsPausedOrPausing()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ // If we are transitioning from idle, fill in the current time
+ if (GetCurrentTime().IsNull()) {
+ if (mPlaybackRate >= 0.0) {
+ mHoldTime.SetValue(TimeDuration(0));
+ } else {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mHoldTime.SetValue(TimeDuration(EffectEnd()));
+ }
+ }
+
+ bool reuseReadyPromise = false;
+ if (mPendingState == PendingState::PlayPending) {
+ CancelPendingTasks();
+ reuseReadyPromise = true;
+ }
+
+ if (!reuseReadyPromise) {
+ // Clear ready promise. We'll create a new one lazily.
+ mReady = nullptr;
+ }
+
+ mPendingState = PendingState::PausePending;
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPausePending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ nsNodeUtils::AnimationChanged(this);
+ }
+}
+
+void
+Animation::ResumeAt(const TimeDuration& aReadyTime)
+{
+ // This method is only expected to be called for an animation that is
+ // waiting to play. We can easily adapt it to handle other states
+ // but it's currently not necessary.
+ MOZ_ASSERT(mPendingState == PendingState::PlayPending,
+ "Expected to resume a play-pending animation");
+ MOZ_ASSERT(mHoldTime.IsNull() != mStartTime.IsNull(),
+ "An animation in the play-pending state should have either a"
+ " resolved hold time or resolved start time (but not both)");
+
+ // If we aborted a pending pause operation we will already have a start time
+ // we should use. In all other cases, we resolve it from the ready time.
+ if (mStartTime.IsNull()) {
+ mStartTime = StartTimeFromReadyTime(aReadyTime);
+ if (mPlaybackRate != 0) {
+ mHoldTime.SetNull();
+ }
+ }
+ mPendingState = PendingState::NotPending;
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+}
+
+void
+Animation::PauseAt(const TimeDuration& aReadyTime)
+{
+ MOZ_ASSERT(mPendingState == PendingState::PausePending,
+ "Expected to pause a pause-pending animation");
+
+ if (!mStartTime.IsNull() && mHoldTime.IsNull()) {
+ mHoldTime.SetValue((aReadyTime - mStartTime.Value())
+ .MultDouble(mPlaybackRate));
+ }
+ mStartTime.SetNull();
+ mPendingState = PendingState::NotPending;
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+}
+
+void
+Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag)
+{
+ // We call UpdateFinishedState before UpdateEffect because the former
+ // can change the current time, which is used by the latter.
+ UpdateFinishedState(aSeekFlag, aSyncNotifyFlag);
+ UpdateEffect();
+
+ if (mTimeline) {
+ mTimeline->NotifyAnimationUpdated(*this);
+ }
+}
+
+// https://w3c.github.io/web-animations/#update-an-animations-finished-state
+void
+Animation::UpdateFinishedState(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag)
+{
+ Nullable<TimeDuration> currentTime = GetCurrentTime();
+ TimeDuration effectEnd = TimeDuration(EffectEnd());
+
+ if (!mStartTime.IsNull() &&
+ mPendingState == PendingState::NotPending) {
+ if (mPlaybackRate > 0.0 &&
+ !currentTime.IsNull() &&
+ currentTime.Value() >= effectEnd) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = currentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd));
+ } else {
+ mHoldTime.SetValue(effectEnd);
+ }
+ } else if (mPlaybackRate < 0.0 &&
+ !currentTime.IsNull() &&
+ currentTime.Value() <= TimeDuration()) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = currentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(std::min(mPreviousCurrentTime.Value(),
+ TimeDuration(0)));
+ } else {
+ mHoldTime.SetValue(0);
+ }
+ } else if (mPlaybackRate != 0.0 &&
+ !currentTime.IsNull() &&
+ mTimeline &&
+ !mTimeline->GetCurrentTime().IsNull()) {
+ if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) {
+ mStartTime.SetValue(mTimeline->GetCurrentTime().Value() -
+ (mHoldTime.Value().MultDouble(1 / mPlaybackRate)));
+ }
+ mHoldTime.SetNull();
+ }
+ }
+
+ bool currentFinishedState = PlayState() == AnimationPlayState::Finished;
+ if (currentFinishedState && !mFinishedIsResolved) {
+ DoFinishNotification(aSyncNotifyFlag);
+ } else if (!currentFinishedState && mFinishedIsResolved) {
+ ResetFinishedPromise();
+ }
+ // We must recalculate the current time to take account of any mHoldTime
+ // changes the code above made.
+ mPreviousCurrentTime = GetCurrentTime();
+}
+
+void
+Animation::UpdateEffect()
+{
+ if (mEffect) {
+ UpdateRelevance();
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->NotifyAnimationTimingUpdated();
+ }
+ }
+}
+
+void
+Animation::FlushStyle() const
+{
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ doc->FlushPendingNotifications(Flush_Style);
+ }
+}
+
+void
+Animation::PostUpdate()
+{
+ if (!mEffect) {
+ return;
+ }
+
+ KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (!keyframeEffect) {
+ return;
+ }
+
+ Maybe<NonOwningAnimationTarget> target = keyframeEffect->GetTarget();
+ if (!target) {
+ return;
+ }
+
+ nsPresContext* presContext = keyframeEffect->GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ presContext->EffectCompositor()
+ ->RequestRestyle(target->mElement,
+ target->mPseudoType,
+ EffectCompositor::RestyleType::Layer,
+ CascadeLevel());
+}
+
+void
+Animation::CancelPendingTasks()
+{
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ nsIDocument* doc = GetRenderedDocument();
+ if (doc) {
+ PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker();
+ if (tracker) {
+ if (mPendingState == PendingState::PlayPending) {
+ tracker->RemovePlayPending(*this);
+ } else {
+ tracker->RemovePausePending(*this);
+ }
+ }
+ }
+
+ mPendingState = PendingState::NotPending;
+ mPendingReadyTime.SetNull();
+}
+
+// https://w3c.github.io/web-animations/#reset-an-animations-pending-tasks
+void
+Animation::ResetPendingTasks()
+{
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ CancelPendingTasks();
+ if (mReady) {
+ mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+}
+
+bool
+Animation::IsPossiblyOrphanedPendingAnimation() const
+{
+ // Check if we are pending but might never start because we are not being
+ // tracked.
+ //
+ // This covers the following cases:
+ //
+ // * We started playing but our effect's target element was orphaned
+ // or bound to a different document.
+ // (note that for the case of our effect changing we should handle
+ // that in SetEffect)
+ // * We started playing but our timeline became inactive.
+ // In this case the pending animation tracker will drop us from its hashmap
+ // when we have been painted.
+ // * When we started playing we couldn't find a PendingAnimationTracker to
+ // register with (perhaps the effect had no document) so we simply
+ // set mPendingState in PlayNoUpdate and relied on this method to catch us
+ // on the next tick.
+
+ // If we're not pending we're ok.
+ if (mPendingState == PendingState::NotPending) {
+ return false;
+ }
+
+ // If we have a pending ready time then we will be started on the next
+ // tick.
+ if (!mPendingReadyTime.IsNull()) {
+ return false;
+ }
+
+ // If we don't have an active timeline then we shouldn't start until
+ // we do.
+ if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) {
+ return false;
+ }
+
+ // If we have no rendered document, or we're not in our rendered document's
+ // PendingAnimationTracker then there's a good chance no one is tracking us.
+ //
+ // If we're wrong and another document is tracking us then, at worst, we'll
+ // simply start/pause the animation one tick too soon. That's better than
+ // never starting/pausing the animation and is unlikely.
+ nsIDocument* doc = GetRenderedDocument();
+ if (!doc) {
+ return true;
+ }
+
+ PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker();
+ return !tracker ||
+ (!tracker->IsWaitingToPlay(*this) &&
+ !tracker->IsWaitingToPause(*this));
+}
+
+StickyTimeDuration
+Animation::EffectEnd() const
+{
+ if (!mEffect) {
+ return StickyTimeDuration(0);
+ }
+
+ return mEffect->SpecifiedTiming().EndTime();
+}
+
+nsIDocument*
+Animation::GetRenderedDocument() const
+{
+ if (!mEffect || !mEffect->AsKeyframeEffect()) {
+ return nullptr;
+ }
+
+ return mEffect->AsKeyframeEffect()->GetRenderedDocument();
+}
+
+void
+Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag)
+{
+ CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
+
+ if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
+ DoFinishNotificationImmediately();
+ } else if (!mFinishNotificationTask.IsPending()) {
+ RefPtr<nsRunnableMethod<Animation>> runnable =
+ NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately);
+ context->DispatchToMicroTask(do_AddRef(runnable));
+ mFinishNotificationTask = runnable.forget();
+ }
+}
+
+void
+Animation::ResetFinishedPromise()
+{
+ mFinishedIsResolved = false;
+ mFinished = nullptr;
+}
+
+void
+Animation::MaybeResolveFinishedPromise()
+{
+ if (mFinished) {
+ mFinished->MaybeResolve(this);
+ }
+ mFinishedIsResolved = true;
+}
+
+void
+Animation::DoFinishNotificationImmediately()
+{
+ mFinishNotificationTask.Revoke();
+
+ if (PlayState() != AnimationPlayState::Finished) {
+ return;
+ }
+
+ MaybeResolveFinishedPromise();
+
+ DispatchPlaybackEvent(NS_LITERAL_STRING("finish"));
+}
+
+void
+Animation::DispatchPlaybackEvent(const nsAString& aName)
+{
+ AnimationPlaybackEventInit init;
+
+ if (aName.EqualsLiteral("finish")) {
+ init.mCurrentTime = GetCurrentTimeAsDouble();
+ }
+ if (mTimeline) {
+ init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble();
+ }
+
+ RefPtr<AnimationPlaybackEvent> event =
+ AnimationPlaybackEvent::Constructor(this, aName, init);
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+bool
+Animation::IsRunningOnCompositor() const
+{
+ return mEffect &&
+ mEffect->AsKeyframeEffect() &&
+ mEffect->AsKeyframeEffect()->IsRunningOnCompositor();
+}
+
+} // namespace dom
+} // namespace mozilla