summaryrefslogtreecommitdiffstats
path: root/dom/animation
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/animation/Animation.cpp2210
-rw-r--r--dom/animation/Animation.h709
-rw-r--r--dom/animation/AnimationComparator.h32
-rw-r--r--dom/animation/AnimationEffect.cpp370
-rw-r--r--dom/animation/AnimationEffect.h117
-rw-r--r--dom/animation/AnimationEventDispatcher.cpp62
-rw-r--r--dom/animation/AnimationEventDispatcher.h407
-rw-r--r--dom/animation/AnimationPerformanceWarning.cpp81
-rw-r--r--dom/animation/AnimationPerformanceWarning.h81
-rw-r--r--dom/animation/AnimationPropertySegment.h55
-rw-r--r--dom/animation/AnimationTarget.h108
-rw-r--r--dom/animation/AnimationTimeline.cpp116
-rw-r--r--dom/animation/AnimationTimeline.h143
-rw-r--r--dom/animation/AnimationUtils.cpp145
-rw-r--r--dom/animation/AnimationUtils.h134
-rw-r--r--dom/animation/CSSAnimation.cpp376
-rw-r--r--dom/animation/CSSAnimation.h238
-rw-r--r--dom/animation/CSSPseudoElement.cpp90
-rw-r--r--dom/animation/CSSPseudoElement.h73
-rw-r--r--dom/animation/CSSTransition.cpp333
-rw-r--r--dom/animation/CSSTransition.h230
-rw-r--r--dom/animation/ComputedTiming.h71
-rw-r--r--dom/animation/DocumentTimeline.cpp311
-rw-r--r--dom/animation/DocumentTimeline.h97
-rw-r--r--dom/animation/EffectCompositor.cpp969
-rw-r--r--dom/animation/EffectCompositor.h258
-rw-r--r--dom/animation/EffectSet.cpp132
-rw-r--r--dom/animation/EffectSet.h246
-rw-r--r--dom/animation/ElementAnimationData.cpp126
-rw-r--r--dom/animation/ElementAnimationData.h262
-rw-r--r--dom/animation/Keyframe.h83
-rw-r--r--dom/animation/KeyframeEffect.cpp2117
-rw-r--r--dom/animation/KeyframeEffect.h529
-rw-r--r--dom/animation/KeyframeEffectParams.h34
-rw-r--r--dom/animation/KeyframeUtils.cpp1255
-rw-r--r--dom/animation/KeyframeUtils.h110
-rw-r--r--dom/animation/PendingAnimationTracker.cpp193
-rw-r--r--dom/animation/PendingAnimationTracker.h113
-rw-r--r--dom/animation/PostRestyleMode.h16
-rw-r--r--dom/animation/PseudoElementHashEntry.h51
-rw-r--r--dom/animation/ScrollTimeline.cpp295
-rw-r--r--dom/animation/ScrollTimeline.h281
-rw-r--r--dom/animation/ScrollTimelineAnimationTracker.cpp48
-rw-r--r--dom/animation/ScrollTimelineAnimationTracker.h58
-rw-r--r--dom/animation/TimingParams.cpp291
-rw-r--r--dom/animation/TimingParams.h265
-rw-r--r--dom/animation/ViewTimeline.cpp166
-rw-r--r--dom/animation/ViewTimeline.h86
-rw-r--r--dom/animation/moz.build79
-rw-r--r--dom/animation/test/chrome.ini29
-rw-r--r--dom/animation/test/chrome/file_animate_xrays.html18
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html40
-rw-r--r--dom/animation/test/chrome/test_animation_observers_async.html665
-rw-r--r--dom/animation/test/chrome/test_animation_observers_sync.html1587
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html1692
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html838
-rw-r--r--dom/animation/test/chrome/test_animation_properties_display.html43
-rw-r--r--dom/animation/test/chrome/test_cssanimation_missing_keyframes.html73
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html83
-rw-r--r--dom/animation/test/chrome/test_keyframe_effect_xrays.html45
-rw-r--r--dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html45
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html1516
-rw-r--r--dom/animation/test/chrome/test_simulate_compute_values_failure.html371
-rw-r--r--dom/animation/test/crashtests/1134538.html8
-rw-r--r--dom/animation/test/crashtests/1216842-1.html35
-rw-r--r--dom/animation/test/crashtests/1216842-2.html35
-rw-r--r--dom/animation/test/crashtests/1216842-3.html27
-rw-r--r--dom/animation/test/crashtests/1216842-4.html27
-rw-r--r--dom/animation/test/crashtests/1216842-5.html38
-rw-r--r--dom/animation/test/crashtests/1216842-6.html38
-rw-r--r--dom/animation/test/crashtests/1239889-1.html16
-rw-r--r--dom/animation/test/crashtests/1244595-1.html3
-rw-r--r--dom/animation/test/crashtests/1272475-1.html20
-rw-r--r--dom/animation/test/crashtests/1272475-2.html20
-rw-r--r--dom/animation/test/crashtests/1277272-1-inner.html19
-rw-r--r--dom/animation/test/crashtests/1277272-1.html25
-rw-r--r--dom/animation/test/crashtests/1278485-1.html26
-rw-r--r--dom/animation/test/crashtests/1282691-1.html23
-rw-r--r--dom/animation/test/crashtests/1291413-1.html20
-rw-r--r--dom/animation/test/crashtests/1291413-2.html21
-rw-r--r--dom/animation/test/crashtests/1304886-1.html14
-rw-r--r--dom/animation/test/crashtests/1309198-1.html40
-rw-r--r--dom/animation/test/crashtests/1322291-1.html24
-rw-r--r--dom/animation/test/crashtests/1322291-2.html31
-rw-r--r--dom/animation/test/crashtests/1322382-1.html16
-rw-r--r--dom/animation/test/crashtests/1323114-1.html12
-rw-r--r--dom/animation/test/crashtests/1323114-2.html18
-rw-r--r--dom/animation/test/crashtests/1323119-1.html13
-rw-r--r--dom/animation/test/crashtests/1324554-1.html19
-rw-r--r--dom/animation/test/crashtests/1325193-1.html18
-rw-r--r--dom/animation/test/crashtests/1330190-1.html11
-rw-r--r--dom/animation/test/crashtests/1330190-2.html36
-rw-r--r--dom/animation/test/crashtests/1330513-1.html8
-rw-r--r--dom/animation/test/crashtests/1332588-1.html25
-rw-r--r--dom/animation/test/crashtests/1333539-1.html30
-rw-r--r--dom/animation/test/crashtests/1333539-2.html38
-rw-r--r--dom/animation/test/crashtests/1334582-1.html11
-rw-r--r--dom/animation/test/crashtests/1334582-2.html11
-rw-r--r--dom/animation/test/crashtests/1334583-1.html9
-rw-r--r--dom/animation/test/crashtests/1335998-1.html28
-rw-r--r--dom/animation/test/crashtests/1343589-1.html18
-rw-r--r--dom/animation/test/crashtests/1359658-1.html33
-rw-r--r--dom/animation/test/crashtests/1373712-1.html11
-rw-r--r--dom/animation/test/crashtests/1379606-1.html21
-rw-r--r--dom/animation/test/crashtests/1393605-1.html15
-rw-r--r--dom/animation/test/crashtests/1400022-1.html10
-rw-r--r--dom/animation/test/crashtests/1401809.html14
-rw-r--r--dom/animation/test/crashtests/1411318-1.html15
-rw-r--r--dom/animation/test/crashtests/1467277-1.html6
-rw-r--r--dom/animation/test/crashtests/1468294-1.html7
-rw-r--r--dom/animation/test/crashtests/1524480-1.html37
-rw-r--r--dom/animation/test/crashtests/1575926.html24
-rw-r--r--dom/animation/test/crashtests/1585770.html22
-rw-r--r--dom/animation/test/crashtests/1604500-1.html24
-rw-r--r--dom/animation/test/crashtests/1611847.html23
-rw-r--r--dom/animation/test/crashtests/1612891-1.html15
-rw-r--r--dom/animation/test/crashtests/1612891-2.html15
-rw-r--r--dom/animation/test/crashtests/1612891-3.html10
-rw-r--r--dom/animation/test/crashtests/1633442.html15
-rw-r--r--dom/animation/test/crashtests/1633486.html20
-rw-r--r--dom/animation/test/crashtests/1656419.html23
-rw-r--r--dom/animation/test/crashtests/1699890.html13
-rw-r--r--dom/animation/test/crashtests/1706157.html19
-rw-r--r--dom/animation/test/crashtests/1714421.html8
-rw-r--r--dom/animation/test/crashtests/1807966.html13
-rw-r--r--dom/animation/test/crashtests/crashtests.list61
-rw-r--r--dom/animation/test/document-timeline/test_document-timeline.html147
-rw-r--r--dom/animation/test/document-timeline/test_request_animation_frame.html27
-rw-r--r--dom/animation/test/mochitest.ini81
-rw-r--r--dom/animation/test/mozilla/empty.html2
-rw-r--r--dom/animation/test/mozilla/file_deferred_start.html179
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_autoremove.html69
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_compositing.html137
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_get_animations.html20
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html48
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_timelines.html30
-rw-r--r--dom/animation/test/mozilla/file_discrete_animations.html122
-rw-r--r--dom/animation/test/mozilla/file_restyles.html2275
-rw-r--r--dom/animation/test/mozilla/file_transition_finish_on_compositor.html67
-rw-r--r--dom/animation/test/mozilla/test_cascade.html37
-rw-r--r--dom/animation/test/mozilla/test_cubic_bezier_limits.html168
-rw-r--r--dom/animation/test/mozilla/test_deferred_start.html21
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_autoremove.html15
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_compositing.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_get_animations.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_timelines.html16
-rw-r--r--dom/animation/test/mozilla/test_disabled_properties.html73
-rw-r--r--dom/animation/test/mozilla/test_discrete_animations.html16
-rw-r--r--dom/animation/test/mozilla/test_distance_of_basic_shape.html91
-rw-r--r--dom/animation/test/mozilla/test_distance_of_filter.html248
-rw-r--r--dom/animation/test/mozilla/test_distance_of_path_function.html140
-rw-r--r--dom/animation/test/mozilla/test_distance_of_transform.html404
-rw-r--r--dom/animation/test/mozilla/test_document_timeline_origin_time_range.html32
-rw-r--r--dom/animation/test/mozilla/test_event_listener_leaks.html43
-rw-r--r--dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html74
-rw-r--r--dom/animation/test/mozilla/test_hide_and_show.html198
-rw-r--r--dom/animation/test/mozilla/test_mainthread_synchronization_pref.html42
-rw-r--r--dom/animation/test/mozilla/test_moz_prefixed_properties.html93
-rw-r--r--dom/animation/test/mozilla/test_pending_animation_tracker.html134
-rw-r--r--dom/animation/test/mozilla/test_restyles.html22
-rw-r--r--dom/animation/test/mozilla/test_restyling_xhr_doc.html106
-rw-r--r--dom/animation/test/mozilla/test_set_easing.html36
-rw-r--r--dom/animation/test/mozilla/test_style_after_finished_on_compositor.html138
-rw-r--r--dom/animation/test/mozilla/test_transform_limits.html56
-rw-r--r--dom/animation/test/mozilla/test_transition_finish_on_compositor.html22
-rw-r--r--dom/animation/test/mozilla/test_underlying_discrete_value.html188
-rw-r--r--dom/animation/test/mozilla/test_unstyled.html54
-rw-r--r--dom/animation/test/mozilla/xhr_doc.html2
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-current-time.html123
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-start-time.html123
-rw-r--r--dom/animation/test/style/test_animation-setting-effect.html127
-rw-r--r--dom/animation/test/style/test_composite.html142
-rw-r--r--dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html43
-rw-r--r--dom/animation/test/style/test_missing-keyframe-on-compositor.html577
-rw-r--r--dom/animation/test/style/test_missing-keyframe.html110
-rw-r--r--dom/animation/test/style/test_transform-non-normalizable-rotate3d.html28
-rw-r--r--dom/animation/test/testcommon.js527
178 files changed, 30517 insertions, 0 deletions
diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp
new file mode 100644
index 0000000000..7e20dfd9e6
--- /dev/null
+++ b/dom/animation/Animation.cpp
@@ -0,0 +1,2210 @@
+/* -*- 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 "mozAutoDocUpdate.h"
+#include "mozilla/dom/AnimationBinding.h"
+#include "mozilla/dom/AnimationPlaybackEvent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/DocumentTimeline.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/AnimationEventDispatcher.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/DeclarationBlock.h"
+#include "mozilla/Maybe.h" // For Maybe
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsAnimationManager.h" // For CSSAnimation
+#include "nsComputedDOMStyle.h"
+#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
+#include "nsDOMCSSAttrDeclaration.h" // For nsDOMCSSAttributeDeclaration
+#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
+#include "nsTransitionManager.h" // For CSSTransition
+#include "PendingAnimationTracker.h" // For PendingAnimationTracker
+#include "ScrollTimelineAnimationTracker.h"
+
+namespace mozilla::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(Animation)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+JSObject* Animation::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::Animation_Binding::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) {
+ NonOwningAnimationTarget target = aAnimation.GetTargetForAnimation();
+ if (!target) {
+ return;
+ }
+
+ // For mutation observers, we use the OwnerDoc.
+ mAutoBatch.emplace(target.mElement->OwnerDoc());
+ }
+
+ private:
+ Maybe<nsAutoAnimationMutationBatch> mAutoBatch;
+};
+} // namespace
+
+// ---------------------------------------------------------------------------
+//
+// Animation interface:
+//
+// ---------------------------------------------------------------------------
+
+Animation::Animation(nsIGlobalObject* aGlobal)
+ : DOMEventTargetHelper(aGlobal),
+ mAnimationIndex(sNextAnimationIndex++),
+ mRTPCallerType(aGlobal->GetRTPCallerType()) {}
+
+Animation::~Animation() = default;
+
+/* static */
+already_AddRefed<Animation> Animation::ClonePausedAnimation(
+ nsIGlobalObject* aGlobal, const Animation& aOther, AnimationEffect& aEffect,
+ AnimationTimeline& aTimeline) {
+ // FIXME: Bug 1805950: Support printing for scroll-timeline once we resolve
+ // the spec issue.
+ if (aOther.UsingScrollTimeline()) {
+ return nullptr;
+ }
+
+ RefPtr<Animation> animation = new Animation(aGlobal);
+
+ // Setup the timeline. We always use document-timeline of the new document,
+ // even if the timeline of |aOther| is null.
+ animation->mTimeline = &aTimeline;
+
+ // Setup the playback rate.
+ animation->mPlaybackRate = aOther.mPlaybackRate;
+
+ // Setup the timing.
+ const Nullable<TimeDuration> currentTime = aOther.GetCurrentTimeAsDuration();
+ if (!aOther.GetTimeline()) {
+ // This simulates what we do in SetTimelineNoUpdate(). It's possible to
+ // preserve the progress if the previous timeline is a scroll-timeline.
+ // So for null timeline, it may have a progress and the non-null current
+ // time.
+ if (!currentTime.IsNull()) {
+ animation->SilentlySetCurrentTime(currentTime.Value());
+ }
+ animation->mPreviousCurrentTime = animation->GetCurrentTimeAsDuration();
+ } else {
+ animation->mHoldTime = currentTime;
+ if (!currentTime.IsNull()) {
+ // FIXME: Should we use |timelineTime| as previous current time here? It
+ // seems we should use animation->GetCurrentTimeAsDuration(), per
+ // UpdateFinishedState().
+ const Nullable<TimeDuration> timelineTime =
+ aTimeline.GetCurrentTimeAsDuration();
+ MOZ_ASSERT(!timelineTime.IsNull(), "Timeline not yet set");
+ animation->mPreviousCurrentTime = timelineTime;
+ }
+ }
+
+ // Setup the effect's link to this.
+ animation->mEffect = &aEffect;
+ animation->mEffect->SetAnimation(animation);
+
+ animation->mPendingState = PendingState::PausePending;
+
+ Document* doc = animation->GetRenderedDocument();
+ MOZ_ASSERT(doc,
+ "Cloning animation should already have the rendered document");
+ PendingAnimationTracker* tracker = doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPausePending(*animation);
+
+ // We expect our relevance to be the same as the orginal.
+ animation->mIsRelevant = aOther.mIsRelevant;
+
+ animation->PostUpdate();
+ animation->mTimeline->NotifyAnimationUpdated(*animation);
+ return animation.forget();
+}
+
+NonOwningAnimationTarget Animation::GetTargetForAnimation() const {
+ AnimationEffect* effect = GetEffect();
+ NonOwningAnimationTarget target;
+ if (!effect || !effect->AsKeyframeEffect()) {
+ return target;
+ }
+ return effect->AsKeyframeEffect()->GetAnimationTarget();
+}
+
+/* static */
+already_AddRefed<Animation> Animation::Constructor(
+ const GlobalObject& aGlobal, AnimationEffect* aEffect,
+ const Optional<AnimationTimeline*>& aTimeline, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ AnimationTimeline* timeline;
+ Document* document =
+ AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+
+ if (aTimeline.WasPassed()) {
+ timeline = aTimeline.Value();
+ } else {
+ if (!document) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ timeline = document->Timeline();
+ }
+
+ RefPtr<Animation> animation = new Animation(global);
+ animation->SetTimelineNoUpdate(timeline);
+ animation->SetEffectNoUpdate(aEffect);
+
+ return animation.forget();
+}
+
+void Animation::SetId(const nsAString& aId) {
+ if (mId == aId) {
+ return;
+ }
+ mId = aId;
+ MutationObservers::NotifyAnimationChanged(this);
+}
+
+void Animation::SetEffect(AnimationEffect* aEffect) {
+ SetEffectNoUpdate(aEffect);
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#setting-the-target-effect
+void Animation::SetEffectNoUpdate(AnimationEffect* aEffect) {
+ RefPtr<Animation> kungFuDeathGrip(this);
+
+ if (mEffect == aEffect) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+ bool wasRelevant = mIsRelevant;
+
+ if (mEffect) {
+ // 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) {
+ MutationObservers::NotifyAnimationRemoved(this);
+ }
+
+ // Break links with the old effect and then drop it.
+ RefPtr<AnimationEffect> oldEffect = mEffect;
+ mEffect = nullptr;
+ if (IsPartialPrerendered()) {
+ if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) {
+ oldKeyframeEffect->ResetPartialPrerendered();
+ }
+ }
+ 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<AnimationEffect> 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) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+
+ ReschedulePendingTasks();
+ }
+
+ MaybeScheduleReplacementCheck();
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+}
+
+void Animation::SetTimeline(AnimationTimeline* aTimeline) {
+ SetTimelineNoUpdate(aTimeline);
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#setting-the-timeline
+void Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) {
+ if (mTimeline == aTimeline) {
+ return;
+ }
+
+ StickyTimeDuration activeTime =
+ mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration();
+
+ const AnimationPlayState previousPlayState = PlayState();
+ const Nullable<TimeDuration> previousCurrentTime = GetCurrentTimeAsDuration();
+ // FIXME: The definition of end time in web-animation-1 is different from that
+ // in web-animation-2, which includes the start time. We are still using the
+ // definition in web-animation-1 here for now.
+ const TimeDuration endTime = TimeDuration(EffectEnd());
+ double previousProgress = 0.0;
+ if (!previousCurrentTime.IsNull() && !endTime.IsZero()) {
+ previousProgress =
+ previousCurrentTime.Value().ToSeconds() / endTime.ToSeconds();
+ }
+
+ RefPtr<AnimationTimeline> oldTimeline = mTimeline;
+ if (oldTimeline) {
+ oldTimeline->RemoveAnimation(this);
+ }
+
+ mTimeline = aTimeline;
+
+ mResetCurrentTimeOnResume = false;
+
+ if (mEffect) {
+ mEffect->UpdateNormalizedTiming();
+ }
+
+ if (mTimeline && !mTimeline->IsMonotonicallyIncreasing()) {
+ // If "to finite timeline" is true.
+
+ ApplyPendingPlaybackRate();
+ Nullable<TimeDuration> seekTime;
+ if (mPlaybackRate >= 0.0) {
+ seekTime.SetValue(TimeDuration());
+ } else {
+ seekTime.SetValue(TimeDuration(EffectEnd()));
+ }
+
+ switch (previousPlayState) {
+ case AnimationPlayState::Running:
+ case AnimationPlayState::Finished:
+ mStartTime = seekTime;
+ break;
+ case AnimationPlayState::Paused:
+ if (!previousCurrentTime.IsNull()) {
+ mResetCurrentTimeOnResume = true;
+ mStartTime.SetNull();
+ mHoldTime.SetValue(
+ TimeDuration(EffectEnd().MultDouble(previousProgress)));
+ } else {
+ mStartTime = seekTime;
+ }
+ break;
+ case AnimationPlayState::Idle:
+ default:
+ break;
+ }
+ } else if (oldTimeline && !oldTimeline->IsMonotonicallyIncreasing() &&
+ !previousCurrentTime.IsNull()) {
+ // If "from finite timeline" and previous progress is resolved.
+ SetCurrentTimeNoUpdate(
+ TimeDuration(EffectEnd().MultDouble(previousProgress)));
+ }
+
+ if (!mStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ if (!aTimeline) {
+ MaybeQueueCancelEvent(activeTime);
+ }
+
+ UpdatePendingAnimationTracker(oldTimeline, aTimeline);
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+
+ // FIXME: Bug 1799071: Check if we need to add
+ // MutationObservers::NotifyAnimationChanged(this) here.
+}
+
+// https://drafts.csswg.org/web-animations/#set-the-animation-start-time
+void Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime) {
+ // Return early if the start time will not change. However, if we
+ // are pending, then setting the start time to any value
+ // including the current value has the effect of aborting
+ // pending tasks so we should not return early in that case.
+ if (!Pending() && 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->GetCurrentTimeAsDuration();
+ }
+ if (timelineTime.IsNull() && !aNewStartTime.IsNull()) {
+ mHoldTime.SetNull();
+ }
+
+ Nullable<TimeDuration> previousCurrentTime = GetCurrentTimeAsDuration();
+
+ ApplyPendingPlaybackRate();
+ mStartTime = aNewStartTime;
+
+ mResetCurrentTimeOnResume = false;
+
+ 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()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#current-time
+Nullable<TimeDuration> Animation::GetCurrentTimeForHoldTime(
+ const Nullable<TimeDuration>& aHoldTime) const {
+ Nullable<TimeDuration> result;
+ if (!aHoldTime.IsNull()) {
+ result = aHoldTime;
+ return result;
+ }
+
+ if (mTimeline && !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timelineTime = mTimeline->GetCurrentTimeAsDuration();
+ if (!timelineTime.IsNull()) {
+ result = CurrentTimeFromTimelineTime(timelineTime.Value(),
+ mStartTime.Value(), mPlaybackRate);
+ }
+ }
+ return result;
+}
+
+// https://drafts.csswg.org/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) == GetCurrentTimeAsDuration()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ SetCurrentTimeNoUpdate(aSeekTime);
+
+ if (IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+void Animation::SetCurrentTimeNoUpdate(const TimeDuration& aSeekTime) {
+ SilentlySetCurrentTime(aSeekTime);
+
+ if (mPendingState == PendingState::PausePending) {
+ // Finish the pause operation
+ mHoldTime.SetValue(aSeekTime);
+
+ ApplyPendingPlaybackRate();
+ mStartTime.SetNull();
+
+ if (mReady) {
+ mReady->MaybeResolve(this);
+ }
+ CancelPendingTasks();
+ }
+
+ UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
+}
+
+// https://drafts.csswg.org/web-animations/#set-the-playback-rate
+void Animation::SetPlaybackRate(double aPlaybackRate) {
+ mPendingPlaybackRate.reset();
+
+ if (aPlaybackRate == mPlaybackRate) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> previousTime = GetCurrentTimeAsDuration();
+ mPlaybackRate = aPlaybackRate;
+ if (!previousTime.IsNull()) {
+ SetCurrentTime(previousTime.Value());
+ }
+
+ // In the case where GetCurrentTimeAsDuration() 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()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#seamlessly-update-the-playback-rate
+void Animation::UpdatePlaybackRate(double aPlaybackRate) {
+ if (mPendingPlaybackRate && mPendingPlaybackRate.value() == aPlaybackRate) {
+ return;
+ }
+
+ // Calculate the play state using the existing playback rate since below we
+ // want to know if the animation is _currently_ finished or not, not whether
+ // it _will_ be finished.
+ AnimationPlayState playState = PlayState();
+
+ mPendingPlaybackRate = Some(aPlaybackRate);
+
+ if (Pending()) {
+ // If we already have a pending task, there is nothing more to do since the
+ // playback rate will be applied then.
+ //
+ // However, as with the idle/paused case below, we still need to update the
+ // relevance (and effect set to make sure it only contains relevant
+ // animations) since the relevance is based on the Animation play state
+ // which incorporates the _pending_ playback rate.
+ UpdateEffect(PostRestyleMode::Never);
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ if (playState == AnimationPlayState::Idle ||
+ playState == AnimationPlayState::Paused ||
+ GetCurrentTimeAsDuration().IsNull()) {
+ // If |previous play state| is idle or paused, or the current time is
+ // unresolved, we apply any pending playback rate on animation immediately.
+ ApplyPendingPlaybackRate();
+
+ // We don't need to update timing or post an update here because:
+ //
+ // * the current time hasn't changed -- it's either unresolved or fixed
+ // with a hold time -- so the output won't have changed
+ // * the finished state won't have changed even if the sign of the
+ // playback rate changed since we're not finished (we're paused or idle)
+ // * the playback rate on layers doesn't need to be updated since we're not
+ // moving. Once we get a start time etc. we'll update the playback rate
+ // then.
+ //
+ // However we still need to update the relevance and effect set as well as
+ // notifying observers.
+ UpdateEffect(PostRestyleMode::Never);
+ if (IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ } else if (playState == AnimationPlayState::Finished) {
+ MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(),
+ "If we have no active timeline, we should be idle or paused");
+ if (aPlaybackRate != 0) {
+ // The unconstrained current time can only be unresolved if either we
+ // don't have an active timeline (and we already asserted that is not
+ // true) or we have an unresolved start time (in which case we should be
+ // paused).
+ MOZ_ASSERT(!GetUnconstrainedCurrentTime().IsNull(),
+ "Unconstrained current time should be resolved");
+ TimeDuration unconstrainedCurrentTime =
+ GetUnconstrainedCurrentTime().Value();
+ TimeDuration timelineTime = mTimeline->GetCurrentTimeAsDuration().Value();
+ mStartTime = StartTimeFromTimelineTime(
+ timelineTime, unconstrainedCurrentTime, aPlaybackRate);
+ } else {
+ mStartTime = mTimeline->GetCurrentTimeAsDuration();
+ }
+
+ ApplyPendingPlaybackRate();
+
+ // Even though we preserve the current time, we might now leave the finished
+ // state (e.g. if the playback rate changes sign) so we need to update
+ // timing.
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ PostUpdate();
+ } else {
+ ErrorResult rv;
+ Play(rv, LimitBehavior::Continue);
+ MOZ_ASSERT(!rv.Failed(),
+ "We should only fail to play when using auto-rewind behavior");
+ }
+}
+
+// https://drafts.csswg.org/web-animations/#play-state
+AnimationPlayState Animation::PlayState() const {
+ Nullable<TimeDuration> currentTime = GetCurrentTimeAsDuration();
+ if (currentTime.IsNull() && mStartTime.IsNull() && !Pending()) {
+ return AnimationPlayState::Idle;
+ }
+
+ if (mPendingState == PendingState::PausePending ||
+ (mStartTime.IsNull() && !Pending())) {
+ return AnimationPlayState::Paused;
+ }
+
+ double playbackRate = CurrentOrPendingPlaybackRate();
+ if (!currentTime.IsNull() &&
+ ((playbackRate > 0.0 && currentTime.Value() >= EffectEnd()) ||
+ (playbackRate < 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);
+ return nullptr;
+ }
+ if (!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);
+ return nullptr;
+ }
+ if (mFinishedIsResolved) {
+ MaybeResolveFinishedPromise();
+ }
+ return mFinished;
+}
+
+// https://drafts.csswg.org/web-animations/#cancel-an-animation
+void Animation::Cancel(PostRestyleMode aPostRestyle) {
+ bool newlyIdle = false;
+
+ if (PlayState() != AnimationPlayState::Idle) {
+ newlyIdle = true;
+
+ ResetPendingTasks();
+
+ if (mFinished) {
+ mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ // mFinished can already be resolved.
+ MOZ_ALWAYS_TRUE(mFinished->SetAnyPromiseIsHandled());
+ }
+ ResetFinishedPromise();
+
+ QueuePlaybackEvent(u"cancel"_ns, GetTimelineCurrentTimeAsTimeStamp());
+ }
+
+ StickyTimeDuration activeTime =
+ mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration();
+
+ mHoldTime.SetNull();
+ mStartTime.SetNull();
+
+ // Allow our effect to remove itself from the its target element's EffectSet.
+ UpdateEffect(aPostRestyle);
+
+ if (mTimeline) {
+ mTimeline->RemoveAnimation(this);
+ }
+ MaybeQueueCancelEvent(activeTime);
+
+ if (newlyIdle && aPostRestyle == PostRestyleMode::IfNeeded) {
+ PostUpdate();
+ }
+}
+
+// https://drafts.csswg.org/web-animations/#finish-an-animation
+void Animation::Finish(ErrorResult& aRv) {
+ double effectivePlaybackRate = CurrentOrPendingPlaybackRate();
+
+ if (effectivePlaybackRate == 0) {
+ return aRv.ThrowInvalidStateError(
+ "Can't finish animation with zero playback rate");
+ }
+ if (effectivePlaybackRate > 0 && EffectEnd() == TimeDuration::Forever()) {
+ return aRv.ThrowInvalidStateError("Can't finish infinite animation");
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ ApplyPendingPlaybackRate();
+
+ // Seek to the end
+ TimeDuration limit =
+ mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0);
+ bool didChange = GetCurrentTimeAsDuration() != 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->GetCurrentTimeAsDuration().IsNull()) {
+ mStartTime = StartTimeFromTimelineTime(
+ mTimeline->GetCurrentTimeAsDuration().Value(), limit, 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()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+ PostUpdate();
+}
+
+void Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior) {
+ PlayNoUpdate(aRv, aLimitBehavior);
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#reverse-an-animation
+void Animation::Reverse(ErrorResult& aRv) {
+ if (!mTimeline) {
+ return aRv.ThrowInvalidStateError(
+ "Can't reverse an animation with no associated timeline");
+ }
+ if (mTimeline->GetCurrentTimeAsDuration().IsNull()) {
+ return aRv.ThrowInvalidStateError(
+ "Can't reverse an animation associated with an inactive timeline");
+ }
+
+ double effectivePlaybackRate = CurrentOrPendingPlaybackRate();
+
+ if (effectivePlaybackRate == 0.0) {
+ return;
+ }
+
+ Maybe<double> originalPendingPlaybackRate = mPendingPlaybackRate;
+
+ mPendingPlaybackRate = Some(-effectivePlaybackRate);
+
+ Play(aRv, LimitBehavior::AutoRewind);
+
+ // If Play() threw, restore state and don't report anything to mutation
+ // observers.
+ if (aRv.Failed()) {
+ mPendingPlaybackRate = originalPendingPlaybackRate;
+ }
+
+ // Play(), above, unconditionally calls PostUpdate so we don't need to do
+ // it here.
+}
+
+void Animation::Persist() {
+ if (mReplaceState == AnimationReplaceState::Persisted) {
+ return;
+ }
+
+ bool wasRemoved = mReplaceState == AnimationReplaceState::Removed;
+
+ mReplaceState = AnimationReplaceState::Persisted;
+
+ // If the animation is not (yet) removed, there should be no side effects of
+ // persisting it.
+ if (wasRemoved) {
+ UpdateEffect(PostRestyleMode::IfNeeded);
+ PostUpdate();
+ }
+}
+
+DocGroup* Animation::GetDocGroup() {
+ Document* doc = GetRenderedDocument();
+ return doc ? doc->GetDocGroup() : nullptr;
+}
+
+// https://drafts.csswg.org/web-animations/#dom-animation-commitstyles
+void Animation::CommitStyles(ErrorResult& aRv) {
+ if (!mEffect) {
+ return;
+ }
+
+ // Take an owning reference to the keyframe effect. This will ensure that
+ // this Animation and the target element remain alive after flushing style.
+ RefPtr<KeyframeEffect> keyframeEffect = mEffect->AsKeyframeEffect();
+ if (!keyframeEffect) {
+ return;
+ }
+
+ NonOwningAnimationTarget target = keyframeEffect->GetAnimationTarget();
+ if (!target) {
+ return;
+ }
+
+ if (target.mPseudoType != PseudoStyleType::NotPseudo) {
+ return aRv.ThrowNoModificationAllowedError(
+ "Can't commit styles of a pseudo-element");
+ }
+
+ // Check it is an element with a style attribute
+ RefPtr<nsStyledElement> styledElement =
+ nsStyledElement::FromNodeOrNull(target.mElement);
+ if (!styledElement) {
+ return aRv.ThrowNoModificationAllowedError(
+ "Target is not capable of having a style attribute");
+ }
+
+ // Hold onto a strong reference to the doc in case the flush destroys it.
+ RefPtr<Document> doc = target.mElement->GetComposedDoc();
+
+ // Flush frames before checking if the target element is rendered since the
+ // result could depend on pending style changes, and IsRendered() looks at the
+ // primary frame.
+ if (doc) {
+ doc->FlushPendingNotifications(FlushType::Frames);
+ }
+ if (!target.mElement->IsRendered()) {
+ return aRv.ThrowInvalidStateError("Target is not rendered");
+ }
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(target.mElement);
+ if (!presContext) {
+ return aRv.ThrowInvalidStateError("Target is not rendered");
+ }
+
+ // Get the computed animation values
+ UniquePtr<StyleAnimationValueMap> animationValues(
+ Servo_AnimationValueMap_Create());
+ if (!presContext->EffectCompositor()->ComposeServoAnimationRuleForEffect(
+ *keyframeEffect, CascadeLevel(), animationValues.get())) {
+ NS_WARNING("Failed to compose animation style to commit");
+ return;
+ }
+
+ // Calling SetCSSDeclaration will trigger attribute setting code.
+ // Start the update now so that the old rule doesn't get used
+ // between when we mutate the declaration and when we set the new
+ // rule.
+ mozAutoDocUpdate autoUpdate(target.mElement->OwnerDoc(), true);
+
+ // Get the inline style to append to
+ RefPtr<DeclarationBlock> declarationBlock;
+ if (auto* existing = target.mElement->GetInlineStyleDeclaration()) {
+ declarationBlock = existing->EnsureMutable();
+ } else {
+ declarationBlock = new DeclarationBlock();
+ declarationBlock->SetDirty();
+ }
+
+ // Prepare the callback
+ MutationClosureData closureData;
+ closureData.mShouldBeCalled = true;
+ closureData.mElement = target.mElement;
+ DeclarationBlockMutationClosure beforeChangeClosure = {
+ nsDOMCSSAttributeDeclaration::MutationClosureFunction,
+ &closureData,
+ };
+
+ // Set the animated styles
+ bool changed = false;
+ nsCSSPropertyIDSet properties = keyframeEffect->GetPropertySet();
+ for (nsCSSPropertyID property : properties) {
+ RefPtr<StyleAnimationValue> computedValue =
+ Servo_AnimationValueMap_GetValue(animationValues.get(), property)
+ .Consume();
+ if (computedValue) {
+ changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(
+ declarationBlock->Raw(), computedValue, beforeChangeClosure);
+ }
+ }
+
+ if (!changed) {
+ MOZ_ASSERT(!closureData.mWasCalled);
+ return;
+ }
+
+ MOZ_ASSERT(closureData.mWasCalled);
+ // Update inline style declaration
+ target.mElement->SetInlineStyleDeclaration(*declarationBlock, closureData);
+}
+
+// ---------------------------------------------------------------------------
+//
+// JS wrappers for Animation interface:
+//
+// ---------------------------------------------------------------------------
+
+Nullable<double> Animation::GetStartTimeAsDouble() const {
+ return AnimationUtils::TimeDurationToDouble(mStartTime, mRTPCallerType);
+}
+
+void Animation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) {
+ return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime));
+}
+
+Nullable<double> Animation::GetCurrentTimeAsDouble() const {
+ return AnimationUtils::TimeDurationToDouble(GetCurrentTimeAsDuration(),
+ mRTPCallerType);
+}
+
+void Animation::SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
+ ErrorResult& aRv) {
+ if (aCurrentTime.IsNull()) {
+ if (!GetCurrentTimeAsDuration().IsNull()) {
+ aRv.ThrowTypeError(
+ "Current time is resolved but trying to set it to an unresolved "
+ "time");
+ }
+ 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->GetCurrentTimeAsDuration().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.
+ TimeDuration currentTime = mTimeline->GetCurrentTimeAsDuration().Value();
+ if (currentTime < mPendingReadyTime.Value()) {
+ mPendingReadyTime.SetValue(currentTime);
+ }
+ FinishPendingAt(mPendingReadyTime.Value());
+ mPendingReadyTime.SetNull();
+ }
+
+ if (IsPossiblyOrphanedPendingAnimation()) {
+ MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(),
+ "Orphaned pending animations should have an active timeline");
+ FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value());
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);
+
+ // Check for changes to whether or not this animation is replaceable.
+ bool isReplaceable = IsReplaceable();
+ if (isReplaceable && !mWasReplaceableAtLastTick) {
+ ScheduleReplacementCheck();
+ }
+ mWasReplaceableAtLastTick = isReplaceable;
+
+ if (!mEffect) {
+ return;
+ }
+
+ // Update layers if we are newly finished.
+ KeyframeEffect* 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 (!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 (!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->GetCurrentTimeAsDuration().IsNull()) {
+ NS_WARNING("Failed to trigger an animation with an active timeline");
+ return;
+ }
+
+ FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value());
+}
+
+bool Animation::TryTriggerNowForFiniteTimeline() {
+ // 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 (!Pending()) {
+ return true;
+ }
+
+ MOZ_ASSERT(mTimeline && !mTimeline->IsMonotonicallyIncreasing());
+
+ // It's possible that the primary frame or the scrollable frame is not ready
+ // when setting up this animation. So we don't finish pending right now. In
+ // this case, the timeline is inactive so it is still pending. The caller
+ // should handle this case by trying this later once the scrollable frame is
+ // ready.
+ const auto currentTime = mTimeline->GetCurrentTimeAsDuration();
+ if (currentTime.IsNull()) {
+ return false;
+ }
+
+ FinishPendingAt(currentTime.Value());
+ return true;
+}
+
+Nullable<TimeDuration> Animation::GetCurrentOrPendingStartTime() const {
+ Nullable<TimeDuration> result;
+
+ // If we have a pending playback rate, work out what start time we will use
+ // when we come to updating that playback rate.
+ //
+ // This logic roughly shadows that in ResumeAt but is just different enough
+ // that it is difficult to extract out the common functionality (and
+ // extracting that functionality out would make it harder to match ResumeAt up
+ // against the spec).
+ if (mPendingPlaybackRate && !mPendingReadyTime.IsNull() &&
+ !mStartTime.IsNull()) {
+ // If we have a hold time, use it as the current time to match.
+ TimeDuration currentTimeToMatch =
+ !mHoldTime.IsNull()
+ ? mHoldTime.Value()
+ : CurrentTimeFromTimelineTime(mPendingReadyTime.Value(),
+ mStartTime.Value(), mPlaybackRate);
+
+ result = StartTimeFromTimelineTime(
+ mPendingReadyTime.Value(), currentTimeToMatch, *mPendingPlaybackRate);
+ return 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 = StartTimeFromTimelineTime(mPendingReadyTime.Value(),
+ mHoldTime.Value(), mPlaybackRate);
+
+ return result;
+}
+
+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:
+ // current 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 {
+ TimeDuration delay =
+ mEffect ? mEffect->NormalizedTiming().Delay() : TimeDuration();
+ return AnimationTimeToTimeStamp(aElapsedTime + delay);
+}
+
+// https://drafts.csswg.org/web-animations/#silently-set-the-current-time
+void Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) {
+ // TODO: Bug 1762238: Introduce "valid seek time" after introducing
+ // CSSNumberish time values.
+ // https://drafts.csswg.org/web-animations-2/#silently-set-the-current-time
+
+ if (!mHoldTime.IsNull() || mStartTime.IsNull() || !mTimeline ||
+ mTimeline->GetCurrentTimeAsDuration().IsNull() || mPlaybackRate == 0.0) {
+ mHoldTime.SetValue(aSeekTime);
+ } else {
+ mStartTime =
+ StartTimeFromTimelineTime(mTimeline->GetCurrentTimeAsDuration().Value(),
+ aSeekTime, mPlaybackRate);
+ }
+
+ if (!mTimeline || mTimeline->GetCurrentTimeAsDuration().IsNull()) {
+ mStartTime.SetNull();
+ }
+
+ mPreviousCurrentTime.SetNull();
+ mResetCurrentTimeOnResume = false;
+}
+
+bool Animation::ShouldBeSynchronizedWithMainThread(
+ const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning) const {
+ // Only synchronize playing animations
+ if (!IsPlaying()) {
+ return false;
+ }
+
+ // Currently only transform animations need to be synchronized
+ if (!aPropertySet.Intersects(nsCSSPropertyIDSet::TransformLikeProperties())) {
+ return false;
+ }
+
+ KeyframeEffect* keyframeEffect =
+ mEffect ? mEffect->AsKeyframeEffect() : nullptr;
+ if (!keyframeEffect) {
+ return false;
+ }
+
+ // Are we starting at the same time as other geometric animations?
+ // We check this before calling ShouldBlockAsyncTransformAnimations, partly
+ // because it's cheaper, but also because it's often the most useful thing
+ // to know when you're debugging performance.
+ // Note: |mSyncWithGeometricAnimations| wouldn't be set if the geometric
+ // animations use scroll-timeline.
+ if (StaticPrefs::
+ dom_animations_mainthread_synchronization_with_geometric_animations() &&
+ mSyncWithGeometricAnimations &&
+ keyframeEffect->HasAnimationOfPropertySet(
+ nsCSSPropertyIDSet::TransformLikeProperties())) {
+ aPerformanceWarning =
+ AnimationPerformanceWarning::Type::TransformWithSyncGeometricAnimations;
+ return true;
+ }
+
+ return keyframeEffect->ShouldBlockAsyncTransformAnimations(
+ aFrame, aPropertySet, aPerformanceWarning);
+}
+
+void Animation::UpdateRelevance() {
+ bool wasRelevant = mIsRelevant;
+ mIsRelevant = mReplaceState != AnimationReplaceState::Removed &&
+ (HasCurrentEffect() || IsInEffect());
+
+ // Notify animation observers.
+ if (wasRelevant && !mIsRelevant) {
+ MutationObservers::NotifyAnimationRemoved(this);
+ } else if (!wasRelevant && mIsRelevant) {
+ MutationObservers::NotifyAnimationAdded(this);
+ }
+}
+
+template <class T>
+bool IsMarkupAnimation(T* aAnimation) {
+ return aAnimation && aAnimation->IsTiedToMarkup();
+}
+
+// https://drafts.csswg.org/web-animations/#replaceable-animation
+bool Animation::IsReplaceable() const {
+ // We never replace CSS animations or CSS transitions since they are managed
+ // by CSS.
+ if (IsMarkupAnimation(AsCSSAnimation()) ||
+ IsMarkupAnimation(AsCSSTransition())) {
+ return false;
+ }
+
+ // Only finished animations can be replaced.
+ if (PlayState() != AnimationPlayState::Finished) {
+ return false;
+ }
+
+ // Already removed animations cannot be replaced.
+ if (ReplaceState() == AnimationReplaceState::Removed) {
+ return false;
+ }
+
+ // We can only replace an animation if we know that, uninterfered, it would
+ // never start playing again. That excludes any animations on timelines that
+ // aren't monotonically increasing.
+ //
+ // If we don't have any timeline at all, then we can't be in the finished
+ // state (since we need both a resolved start time and current time for that)
+ // and will have already returned false above.
+ //
+ // (However, if it ever does become possible to be finished without a timeline
+ // then we will want to return false here since it probably suggests an
+ // animation being driven directly by script, in which case we can't assume
+ // anything about how they will behave.)
+ if (!GetTimeline() || !GetTimeline()->TracksWallclockTime()) {
+ return false;
+ }
+
+ // If the animation doesn't have an effect then we can't determine if it is
+ // filling or not so just leave it alone.
+ if (!GetEffect()) {
+ return false;
+ }
+
+ // At the time of writing we only know about KeyframeEffects. If we introduce
+ // other types of effects we will need to decide if they are replaceable or
+ // not.
+ MOZ_ASSERT(GetEffect()->AsKeyframeEffect(),
+ "Effect should be a keyframe effect");
+
+ // We only replace animations that are filling.
+ if (GetEffect()->GetComputedTiming().mProgress.IsNull()) {
+ return false;
+ }
+
+ // We should only replace animations with a target element (since otherwise
+ // what other effects would we consider when determining if they are covered
+ // or not?).
+ if (!GetEffect()->AsKeyframeEffect()->GetAnimationTarget()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool Animation::IsRemovable() const {
+ return ReplaceState() == AnimationReplaceState::Active && IsReplaceable();
+}
+
+void Animation::ScheduleReplacementCheck() {
+ MOZ_ASSERT(
+ IsReplaceable(),
+ "Should only schedule a replacement check for a replaceable animation");
+
+ // If IsReplaceable() is true, the following should also hold
+ MOZ_ASSERT(GetEffect());
+ MOZ_ASSERT(GetEffect()->AsKeyframeEffect());
+
+ NonOwningAnimationTarget target =
+ GetEffect()->AsKeyframeEffect()->GetAnimationTarget();
+
+ MOZ_ASSERT(target);
+
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(target.mElement);
+ if (presContext) {
+ presContext->EffectCompositor()->NoteElementForReducing(target);
+ }
+}
+
+void Animation::MaybeScheduleReplacementCheck() {
+ if (!IsReplaceable()) {
+ return;
+ }
+
+ ScheduleReplacementCheck();
+}
+
+void Animation::Remove() {
+ MOZ_ASSERT(IsRemovable(),
+ "Should not be trying to remove an effect that is not removable");
+
+ mReplaceState = AnimationReplaceState::Removed;
+
+ UpdateEffect(PostRestyleMode::IfNeeded);
+ PostUpdate();
+
+ QueuePlaybackEvent(u"remove"_ns, GetTimelineCurrentTimeAsTimeStamp());
+}
+
+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) {
+ // Cancelled transitions no longer have an owning element. To be strictly
+ // correct we should store a strong reference to the owning element
+ // so that if we arrive here while sorting cancel events, we can sort
+ // them in the correct order.
+ //
+ // However, given that cancel events are almost always queued
+ // synchronously in some deterministic manner, we can be fairly sure
+ // that cancel events will be dispatched in a deterministic order
+ // (which is our only hard requirement until specs say otherwise).
+ // Furthermore, we only reach here when we have events with equal
+ // timestamps so this is an edge case we can probably ignore for now.
+ 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::WillComposeStyle() {
+ mFinishedAtLastComposeStyle = (PlayState() == AnimationPlayState::Finished);
+
+ MOZ_ASSERT(mEffect);
+
+ KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->WillComposeStyle();
+ }
+}
+
+void Animation::ComposeStyle(StyleAnimationValueMap& aComposeResult,
+ const nsCSSPropertyIDSet& aPropertiesToSkip) {
+ if (!mEffect) {
+ 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
+ // GetCurrentTimeAsDuration. 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.
+ bool pending = Pending();
+ {
+ AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime);
+
+ if (pending && mHoldTime.IsNull() && !mStartTime.IsNull()) {
+ Nullable<TimeDuration> timeToUse = mPendingReadyTime;
+ if (timeToUse.IsNull() && mTimeline && mTimeline->TracksWallclockTime()) {
+ timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now());
+ }
+ if (!timeToUse.IsNull()) {
+ mHoldTime = CurrentTimeFromTimelineTime(
+ timeToUse.Value(), mStartTime.Value(), mPlaybackRate);
+ }
+ }
+
+ KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip);
+ }
+ }
+
+ MOZ_ASSERT(
+ pending == Pending(),
+ "Pending state should not change during the course of compositing");
+}
+
+void Animation::NotifyEffectTimingUpdated() {
+ MOZ_ASSERT(mEffect,
+ "We should only update effect timing when we have a target "
+ "effect");
+ UpdateTiming(Animation::SeekFlag::NoSeek, Animation::SyncNotifyFlag::Async);
+}
+
+void Animation::NotifyEffectPropertiesUpdated() {
+ MOZ_ASSERT(mEffect,
+ "We should only update effect properties when we have a target "
+ "effect");
+
+ MaybeScheduleReplacementCheck();
+}
+
+void Animation::NotifyEffectTargetUpdated() {
+ MOZ_ASSERT(mEffect,
+ "We should only update the effect target when we have a target "
+ "effect");
+
+ MaybeScheduleReplacementCheck();
+}
+
+void Animation::NotifyGeometricAnimationsStartingThisFrame() {
+ if (!IsNewlyStarted() || !mEffect) {
+ return;
+ }
+
+ mSyncWithGeometricAnimations = true;
+}
+
+// https://drafts.csswg.org/web-animations/#play-an-animation
+void Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) {
+ AutoMutationBatchForAnimation mb(*this);
+
+ const bool isAutoRewind = aLimitBehavior == LimitBehavior::AutoRewind;
+ const bool abortedPause = mPendingState == PendingState::PausePending;
+ double effectivePlaybackRate = CurrentOrPendingPlaybackRate();
+
+ Nullable<TimeDuration> currentTime = GetCurrentTimeAsDuration();
+ if (mResetCurrentTimeOnResume) {
+ currentTime.SetNull();
+ mResetCurrentTimeOnResume = false;
+ }
+
+ Nullable<TimeDuration> seekTime;
+ if (isAutoRewind) {
+ if (effectivePlaybackRate >= 0.0 &&
+ (currentTime.IsNull() || currentTime.Value() < TimeDuration() ||
+ currentTime.Value() >= EffectEnd())) {
+ seekTime.SetValue(TimeDuration());
+ } else if (effectivePlaybackRate < 0.0 &&
+ (currentTime.IsNull() || currentTime.Value() <= TimeDuration() ||
+ currentTime.Value() > EffectEnd())) {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ return aRv.ThrowInvalidStateError(
+ "Can't rewind animation with infinite effect end");
+ }
+ seekTime.SetValue(TimeDuration(EffectEnd()));
+ }
+ }
+
+ if (seekTime.IsNull() && mStartTime.IsNull() && currentTime.IsNull()) {
+ seekTime.SetValue(TimeDuration());
+ }
+
+ if (!seekTime.IsNull()) {
+ if (HasFiniteTimeline()) {
+ mStartTime = seekTime;
+ mHoldTime.SetNull();
+ ApplyPendingPlaybackRate();
+ } else {
+ mHoldTime = seekTime;
+ }
+ }
+
+ bool reuseReadyPromise = false;
+ if (mPendingState != PendingState::NotPending) {
+ CancelPendingTasks();
+ reuseReadyPromise = true;
+ }
+
+ // If the hold time is null then we're already playing normally and,
+ // typically, we can bail out here.
+ //
+ // However, there are two cases where we can't do that:
+ //
+ // (a) If we just aborted a pause. In this case, for consistency, we need to
+ // go through the motions of doing an asynchronous start.
+ //
+ // (b) If we have timing changes (specifically a change to the playbackRate)
+ // that should be applied asynchronously.
+ //
+ if (mHoldTime.IsNull() && seekTime.IsNull() && !abortedPause &&
+ !mPendingPlaybackRate) {
+ 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;
+
+ // Clear flag that causes us to sync transform animations with the main
+ // thread for now. We'll set this when we go to set up compositor
+ // animations if it applies.
+ mSyncWithGeometricAnimations = false;
+
+ if (HasFiniteTimeline()) {
+ // Always schedule a task even if we would like to let this animation
+ // immedidately ready, per spec.
+ // https://drafts.csswg.org/web-animations/#playing-an-animation-section
+ if (Document* doc = GetRenderedDocument()) {
+ doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
+ } // else: we fail to track this animation, so let the scroll frame to
+ // trigger it when ticking.
+ } else {
+ if (Document* doc = GetRenderedDocument()) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPlayPending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+}
+
+// https://drafts.csswg.org/web-animations/#pause-an-animation
+void Animation::Pause(ErrorResult& aRv) {
+ if (IsPausedOrPausing()) {
+ return;
+ }
+
+ AutoMutationBatchForAnimation mb(*this);
+
+ Nullable<TimeDuration> seekTime;
+ // If we are transitioning from idle, fill in the current time
+ if (GetCurrentTimeAsDuration().IsNull()) {
+ if (mPlaybackRate >= 0.0) {
+ seekTime.SetValue(TimeDuration(0));
+ } else {
+ if (EffectEnd() == TimeDuration::Forever()) {
+ return aRv.ThrowInvalidStateError("Can't seek to infinite effect end");
+ }
+ seekTime.SetValue(TimeDuration(EffectEnd()));
+ }
+ }
+
+ if (!seekTime.IsNull()) {
+ if (HasFiniteTimeline()) {
+ mStartTime = seekTime;
+ } else {
+ mHoldTime = seekTime;
+ }
+ }
+
+ 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;
+
+ if (HasFiniteTimeline()) {
+ // Always schedule a task even if we would like to let this animation
+ // immedidately ready, per spec.
+ // https://drafts.csswg.org/web-animations/#playing-an-animation-section
+ if (Document* doc = GetRenderedDocument()) {
+ doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
+ } // else: we fail to track this animation, so let the scroll frame to
+ // trigger it when ticking.
+ } else {
+ if (Document* doc = GetRenderedDocument()) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ tracker->AddPausePending(*this);
+ } else {
+ TriggerOnNextTick(Nullable<TimeDuration>());
+ }
+ }
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+ if (IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+
+ PostUpdate();
+}
+
+// https://drafts.csswg.org/web-animations/#play-an-animation
+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");
+
+ AutoMutationBatchForAnimation mb(*this);
+ bool hadPendingPlaybackRate = mPendingPlaybackRate.isSome();
+
+ if (!mHoldTime.IsNull()) {
+ // The hold time is set, so we don't need any special handling to preserve
+ // the current time.
+ ApplyPendingPlaybackRate();
+ mStartTime =
+ StartTimeFromTimelineTime(aReadyTime, mHoldTime.Value(), mPlaybackRate);
+ if (mPlaybackRate != 0) {
+ mHoldTime.SetNull();
+ }
+ } else if (!mStartTime.IsNull() && mPendingPlaybackRate) {
+ // Apply any pending playback rate, preserving the current time.
+ TimeDuration currentTimeToMatch = CurrentTimeFromTimelineTime(
+ aReadyTime, mStartTime.Value(), mPlaybackRate);
+ ApplyPendingPlaybackRate();
+ mStartTime = StartTimeFromTimelineTime(aReadyTime, currentTimeToMatch,
+ mPlaybackRate);
+ if (mPlaybackRate == 0) {
+ mHoldTime.SetValue(currentTimeToMatch);
+ }
+ }
+
+ mPendingState = PendingState::NotPending;
+
+ UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);
+
+ // If we had a pending playback rate, we will have now applied it so we need
+ // to notify observers.
+ if (hadPendingPlaybackRate && IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(this);
+ }
+
+ 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 = CurrentTimeFromTimelineTime(aReadyTime, mStartTime.Value(),
+ mPlaybackRate);
+ }
+ ApplyPendingPlaybackRate();
+ 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(PostRestyleMode::IfNeeded);
+
+ if (mTimeline) {
+ mTimeline->NotifyAnimationUpdated(*this);
+ }
+}
+
+// https://drafts.csswg.org/web-animations/#update-an-animations-finished-state
+void Animation::UpdateFinishedState(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag) {
+ Nullable<TimeDuration> unconstrainedCurrentTime =
+ aSeekFlag == SeekFlag::NoSeek ? GetUnconstrainedCurrentTime()
+ : GetCurrentTimeAsDuration();
+ TimeDuration effectEnd = TimeDuration(EffectEnd());
+
+ if (!unconstrainedCurrentTime.IsNull() && !mStartTime.IsNull() &&
+ mPendingState == PendingState::NotPending) {
+ if (mPlaybackRate > 0.0 && unconstrainedCurrentTime.Value() >= effectEnd) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = unconstrainedCurrentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd));
+ } else {
+ mHoldTime.SetValue(effectEnd);
+ }
+ } else if (mPlaybackRate < 0.0 &&
+ unconstrainedCurrentTime.Value() <= TimeDuration()) {
+ if (aSeekFlag == SeekFlag::DidSeek) {
+ mHoldTime = unconstrainedCurrentTime;
+ } else if (!mPreviousCurrentTime.IsNull()) {
+ mHoldTime.SetValue(
+ std::min(mPreviousCurrentTime.Value(), TimeDuration(0)));
+ } else {
+ mHoldTime.SetValue(0);
+ }
+ } else if (mPlaybackRate != 0.0 && mTimeline &&
+ !mTimeline->GetCurrentTimeAsDuration().IsNull()) {
+ if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) {
+ mStartTime = StartTimeFromTimelineTime(
+ mTimeline->GetCurrentTimeAsDuration().Value(), mHoldTime.Value(),
+ mPlaybackRate);
+ }
+ mHoldTime.SetNull();
+ }
+ }
+
+ // We must recalculate the current time to take account of any mHoldTime
+ // changes the code above made.
+ mPreviousCurrentTime = GetCurrentTimeAsDuration();
+
+ bool currentFinishedState = PlayState() == AnimationPlayState::Finished;
+ if (currentFinishedState && !mFinishedIsResolved) {
+ DoFinishNotification(aSyncNotifyFlag);
+ } else if (!currentFinishedState && mFinishedIsResolved) {
+ ResetFinishedPromise();
+ }
+}
+
+void Animation::UpdateEffect(PostRestyleMode aPostRestyle) {
+ if (mEffect) {
+ UpdateRelevance();
+
+ KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (keyframeEffect) {
+ keyframeEffect->NotifyAnimationTimingUpdated(aPostRestyle);
+ }
+ }
+}
+
+void Animation::FlushUnanimatedStyle() const {
+ if (Document* doc = GetRenderedDocument()) {
+ doc->FlushPendingNotifications(
+ ChangesToFlush(FlushType::Style, false /* flush animations */));
+ }
+}
+
+void Animation::PostUpdate() {
+ if (!mEffect) {
+ return;
+ }
+
+ KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
+ if (!keyframeEffect) {
+ return;
+ }
+ keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+void Animation::CancelPendingTasks() {
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ if (Document* doc = GetRenderedDocument()) {
+ PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker();
+ if (tracker) {
+ if (mPendingState == PendingState::PlayPending) {
+ tracker->RemovePlayPending(*this);
+ } else {
+ tracker->RemovePausePending(*this);
+ }
+ }
+ }
+
+ mPendingState = PendingState::NotPending;
+ mPendingReadyTime.SetNull();
+}
+
+// https://drafts.csswg.org/web-animations/#reset-an-animations-pending-tasks
+void Animation::ResetPendingTasks() {
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ CancelPendingTasks();
+ ApplyPendingPlaybackRate();
+
+ if (mReady) {
+ mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ MOZ_ALWAYS_TRUE(mReady->SetAnyPromiseIsHandled());
+ mReady = nullptr;
+ }
+}
+
+void Animation::ReschedulePendingTasks() {
+ if (mPendingState == PendingState::NotPending) {
+ return;
+ }
+
+ mPendingReadyTime.SetNull();
+
+ if (Document* doc = GetRenderedDocument()) {
+ PendingAnimationTracker* tracker =
+ doc->GetOrCreatePendingAnimationTracker();
+ if (mPendingState == PendingState::PlayPending &&
+ !tracker->IsWaitingToPlay(*this)) {
+ tracker->AddPlayPending(*this);
+ } else if (mPendingState == PendingState::PausePending &&
+ !tracker->IsWaitingToPause(*this)) {
+ tracker->AddPausePending(*this);
+ }
+ }
+}
+
+// https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary
+/* static*/ Animation::ProgressTimelinePosition
+Animation::AtProgressTimelineBoundary(
+ const Nullable<TimeDuration>& aTimelineDuration,
+ const Nullable<TimeDuration>& aCurrentTime,
+ const TimeDuration& aEffectStartTime, const double aPlaybackRate) {
+ // Based on changed defined in: https://github.com/w3c/csswg-drafts/pull/6702
+ // 1. If any of the following conditions are true:
+ // * the associated animation's timeline is not a progress-based timeline,
+ // or
+ // * the associated animation's timeline duration is unresolved or zero,
+ // or
+ // * the animation's playback rate is zero
+ // return false
+ // Note: We can detect a progress-based timeline by relying on the fact that
+ // monotonic timelines (i.e. non-progress-based timelines) have an unresolved
+ // timeline duration.
+ if (aTimelineDuration.IsNull() || aTimelineDuration.Value().IsZero() ||
+ aPlaybackRate == 0.0) {
+ return ProgressTimelinePosition::NotBoundary;
+ }
+
+ // 2. Let effective start time be the animation's start time if resolved, or
+ // zero otherwise.
+ const TimeDuration& effectiveStartTime = aEffectStartTime;
+
+ // 3. Let effective timeline time be (animation's current time / animation's
+ // playback rate) + effective start time.
+ // Note: we use zero if the current time is unresolved. See the spec issue:
+ // https://github.com/w3c/csswg-drafts/issues/7458
+ const TimeDuration effectiveTimelineTime =
+ (aCurrentTime.IsNull()
+ ? TimeDuration()
+ : aCurrentTime.Value().MultDouble(1.0 / aPlaybackRate)) +
+ effectiveStartTime;
+
+ // 4. Let effective timeline progress be (effective timeline time / timeline
+ // duration)
+ // 5. If effective timeline progress is 0 or 1, return true,
+ // We avoid the division here but it is effectively the same as 4 & 5 above.
+ return effectiveTimelineTime.IsZero() ||
+ (AnimationUtils::IsWithinAnimationTimeTolerance(
+ effectiveTimelineTime, aTimelineDuration.Value()))
+ ? ProgressTimelinePosition::Boundary
+ : ProgressTimelinePosition::NotBoundary;
+}
+
+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/ScrollTimelineAnimationTracker to register with
+ // (perhaps the effect had no document) so we may
+ // 1. simply set mPendingState in PlayNoUpdate and relied on this method to
+ // catch us on the next tick, or
+ // 2. rely on the scroll frame to tick this animation and catch us in this
+ // method.
+
+ // 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->GetCurrentTimeAsDuration().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.
+ Document* 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->NormalizedTiming().EndTime();
+}
+
+Document* Animation::GetRenderedDocument() const {
+ if (!mEffect || !mEffect->AsKeyframeEffect()) {
+ return nullptr;
+ }
+
+ return mEffect->AsKeyframeEffect()->GetRenderedDocument();
+}
+
+Document* Animation::GetTimelineDocument() const {
+ return mTimeline ? mTimeline->GetDocument() : nullptr;
+}
+
+void Animation::UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline,
+ AnimationTimeline* aNewTimeline) {
+ // If we are still in pending, we may have to move this animation into the
+ // correct animation tracker.
+ Document* doc = GetRenderedDocument();
+ if (!doc || !Pending()) {
+ return;
+ }
+
+ const bool fromFiniteTimeline =
+ aOldTimeline && !aOldTimeline->IsMonotonicallyIncreasing();
+ const bool toFiniteTimeline =
+ aNewTimeline && !aNewTimeline->IsMonotonicallyIncreasing();
+ if (fromFiniteTimeline == toFiniteTimeline) {
+ return;
+ }
+
+ const bool isPlayPending = mPendingState == PendingState::PlayPending;
+ if (toFiniteTimeline) {
+ // From null/document-timeline to scroll-timeline
+ if (auto* tracker = doc->GetPendingAnimationTracker()) {
+ if (isPlayPending) {
+ tracker->RemovePlayPending(*this);
+ } else {
+ tracker->RemovePausePending(*this);
+ }
+ }
+
+ doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
+ } else {
+ // From scroll-timeline to null/document-timeline
+ if (auto* tracker = doc->GetScrollTimelineAnimationTracker()) {
+ tracker->RemovePending(*this);
+ }
+
+ auto* tracker = doc->GetOrCreatePendingAnimationTracker();
+ if (isPlayPending) {
+ tracker->AddPlayPending(*this);
+ } else {
+ tracker->AddPausePending(*this);
+ }
+ }
+}
+
+class AsyncFinishNotification : public MicroTaskRunnable {
+ public:
+ explicit AsyncFinishNotification(Animation* aAnimation)
+ : MicroTaskRunnable(), mAnimation(aAnimation) {}
+
+ virtual void Run(AutoSlowOperation& aAso) override {
+ mAnimation->DoFinishNotificationImmediately(this);
+ mAnimation = nullptr;
+ }
+
+ virtual bool Suppressed() override {
+ nsIGlobalObject* global = mAnimation->GetOwnerGlobal();
+ return global && global->IsInSyncOperation();
+ }
+
+ private:
+ RefPtr<Animation> mAnimation;
+};
+
+void Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) {
+ CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
+
+ if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
+ DoFinishNotificationImmediately();
+ } else if (!mFinishNotificationTask) {
+ RefPtr<MicroTaskRunnable> runnable = new AsyncFinishNotification(this);
+ context->DispatchToMicroTask(do_AddRef(runnable));
+ mFinishNotificationTask = std::move(runnable);
+ }
+}
+
+void Animation::ResetFinishedPromise() {
+ mFinishedIsResolved = false;
+ mFinished = nullptr;
+}
+
+void Animation::MaybeResolveFinishedPromise() {
+ if (mFinished) {
+ mFinished->MaybeResolve(this);
+ }
+ mFinishedIsResolved = true;
+}
+
+void Animation::DoFinishNotificationImmediately(MicroTaskRunnable* aAsync) {
+ if (aAsync && aAsync != mFinishNotificationTask) {
+ return;
+ }
+
+ mFinishNotificationTask = nullptr;
+
+ if (PlayState() != AnimationPlayState::Finished) {
+ return;
+ }
+
+ MaybeResolveFinishedPromise();
+
+ QueuePlaybackEvent(u"finish"_ns, AnimationTimeToTimeStamp(EffectEnd()));
+}
+
+void Animation::QueuePlaybackEvent(const nsAString& aName,
+ TimeStamp&& aScheduledEventTime) {
+ // Use document for timing.
+ // https://drafts.csswg.org/web-animations-1/#document-for-timing
+ Document* doc = GetTimelineDocument();
+ if (!doc) {
+ return;
+ }
+
+ nsPresContext* presContext = doc->GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ AnimationPlaybackEventInit init;
+
+ if (aName.EqualsLiteral("finish") || aName.EqualsLiteral("remove")) {
+ init.mCurrentTime = GetCurrentTimeAsDouble();
+ }
+ if (mTimeline) {
+ init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble();
+ }
+
+ RefPtr<AnimationPlaybackEvent> event =
+ AnimationPlaybackEvent::Constructor(this, aName, init);
+ event->SetTrusted(true);
+
+ presContext->AnimationEventDispatcher()->QueueEvent(AnimationEventInfo(
+ aName, std::move(event), std::move(aScheduledEventTime), this));
+}
+
+bool Animation::IsRunningOnCompositor() const {
+ return mEffect && mEffect->AsKeyframeEffect() &&
+ mEffect->AsKeyframeEffect()->IsRunningOnCompositor();
+}
+
+bool Animation::HasCurrentEffect() const {
+ return GetEffect() && GetEffect()->IsCurrent();
+}
+
+bool Animation::IsInEffect() const {
+ return GetEffect() && GetEffect()->IsInEffect();
+}
+
+void Animation::SetHiddenByContentVisibility(bool hidden) {
+ if (mHiddenByContentVisibility == hidden) {
+ return;
+ }
+
+ mHiddenByContentVisibility = hidden;
+
+ if (!GetTimeline()) {
+ return;
+ }
+
+ GetTimeline()->NotifyAnimationContentVisibilityChanged(this, !hidden);
+}
+
+StickyTimeDuration Animation::IntervalStartTime(
+ const StickyTimeDuration& aActiveDuration) const {
+ MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),
+ "Should be called for CSS animations or transitions");
+ static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
+ return std::max(
+ std::min(StickyTimeDuration(-mEffect->NormalizedTiming().Delay()),
+ aActiveDuration),
+ zeroDuration);
+}
+
+// Later side of the elapsed time range reported in CSS Animations and CSS
+// Transitions events.
+//
+// https://drafts.csswg.org/css-animations-2/#interval-end
+// https://drafts.csswg.org/css-transitions-2/#interval-end
+StickyTimeDuration Animation::IntervalEndTime(
+ const StickyTimeDuration& aActiveDuration) const {
+ MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),
+ "Should be called for CSS animations or transitions");
+
+ static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
+ const StickyTimeDuration& effectEnd = EffectEnd();
+
+ // If both "associated effect end" and "start delay" are Infinity, we skip it
+ // because we will get NaN when computing "Infinity - Infinity", and
+ // using NaN in std::min or std::max is undefined.
+ if (MOZ_UNLIKELY(effectEnd == TimeDuration::Forever() &&
+ effectEnd == mEffect->NormalizedTiming().Delay())) {
+ // Note: If we use TimeDuration::Forever(), within our animation event
+ // handling, we'd end up turning that into a null TimeStamp which can causes
+ // errors if we try to do any arithmetic with it. Given that we should never
+ // end up _using_ the interval end time. So returning zeroDuration here is
+ // probably fine.
+ return zeroDuration;
+ }
+
+ return std::max(std::min(effectEnd - mEffect->NormalizedTiming().Delay(),
+ aActiveDuration),
+ zeroDuration);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h
new file mode 100644
index 0000000000..bbe2ab7a3e
--- /dev/null
+++ b/dom/animation/Animation.h
@@ -0,0 +1,709 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_Animation_h
+#define mozilla_dom_Animation_h
+
+#include "X11UndefineNone.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel
+#include "mozilla/LinkedList.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/PostRestyleMode.h"
+#include "mozilla/StickyTimeDuration.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration
+#include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState
+#include "mozilla/dom/AnimationTimeline.h"
+
+struct JSContext;
+class nsCSSPropertyIDSet;
+class nsIFrame;
+class nsIGlobalObject;
+
+namespace mozilla {
+
+struct AnimationRule;
+class MicroTaskRunnable;
+
+namespace dom {
+
+class AnimationEffect;
+class AsyncFinishNotification;
+class CSSAnimation;
+class CSSTransition;
+class Document;
+class Promise;
+
+class Animation : public DOMEventTargetHelper,
+ public LinkedListElement<Animation> {
+ protected:
+ virtual ~Animation();
+
+ public:
+ explicit Animation(nsIGlobalObject* aGlobal);
+
+ // Constructs a copy of |aOther| with a new effect and timeline.
+ // This is only intended to be used while making a static clone of a document
+ // during printing, and does not assume that |aOther| is in the same document
+ // as any of the other arguments.
+ static already_AddRefed<Animation> ClonePausedAnimation(
+ nsIGlobalObject* aGlobal, const Animation& aOther,
+ AnimationEffect& aEffect, AnimationTimeline& aTimeline);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation, DOMEventTargetHelper)
+
+ nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); }
+
+ /**
+ * Utility function to get the target (pseudo-)element associated with an
+ * animation.
+ */
+ NonOwningAnimationTarget GetTargetForAnimation() const;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual CSSAnimation* AsCSSAnimation() { return nullptr; }
+ virtual const CSSAnimation* AsCSSAnimation() const { return nullptr; }
+ virtual CSSTransition* AsCSSTransition() { return nullptr; }
+ virtual const CSSTransition* AsCSSTransition() const { return nullptr; }
+
+ /**
+ * Flag to pass to Play to indicate whether or not it should automatically
+ * rewind the current time to the start point if the animation is finished.
+ * For regular calls to play() from script we should do this, but when a CSS
+ * animation's animation-play-state changes we shouldn't rewind the animation.
+ */
+ enum class LimitBehavior { AutoRewind, Continue };
+
+ // Animation interface methods
+ static already_AddRefed<Animation> Constructor(
+ const GlobalObject& aGlobal, AnimationEffect* aEffect,
+ const Optional<AnimationTimeline*>& aTimeline, ErrorResult& aRv);
+
+ void GetId(nsAString& aResult) const { aResult = mId; }
+ void SetId(const nsAString& aId);
+
+ AnimationEffect* GetEffect() const { return mEffect; }
+ virtual void SetEffect(AnimationEffect* aEffect);
+ void SetEffectNoUpdate(AnimationEffect* aEffect);
+
+ // FIXME: Bug 1676794. This is a tentative solution before we implement
+ // ScrollTimeline interface. If the timeline is scroll/view timeline, we
+ // return null. Once we implement ScrollTimeline interface, we can drop this.
+ already_AddRefed<AnimationTimeline> GetTimelineFromJS() const {
+ return mTimeline && mTimeline->IsScrollTimeline() ? nullptr
+ : do_AddRef(mTimeline);
+ }
+ void SetTimelineFromJS(AnimationTimeline* aTimeline) {
+ SetTimeline(aTimeline);
+ }
+
+ AnimationTimeline* GetTimeline() const { return mTimeline; }
+ void SetTimeline(AnimationTimeline* aTimeline);
+ void SetTimelineNoUpdate(AnimationTimeline* aTimeline);
+
+ Nullable<TimeDuration> GetStartTime() const { return mStartTime; }
+ Nullable<double> GetStartTimeAsDouble() const;
+ void SetStartTime(const Nullable<TimeDuration>& aNewStartTime);
+ virtual void SetStartTimeAsDouble(const Nullable<double>& aStartTime);
+
+ // This is deliberately _not_ called GetCurrentTime since that would clash
+ // with a macro defined in winbase.h
+ Nullable<TimeDuration> GetCurrentTimeAsDuration() const {
+ return GetCurrentTimeForHoldTime(mHoldTime);
+ }
+ Nullable<double> GetCurrentTimeAsDouble() const;
+ void SetCurrentTime(const TimeDuration& aSeekTime);
+ void SetCurrentTimeNoUpdate(const TimeDuration& aSeekTime);
+ void SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
+ ErrorResult& aRv);
+
+ double PlaybackRate() const { return mPlaybackRate; }
+ void SetPlaybackRate(double aPlaybackRate);
+
+ AnimationPlayState PlayState() const;
+ virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); }
+
+ bool Pending() const { return mPendingState != PendingState::NotPending; }
+ virtual bool PendingFromJS() const { return Pending(); }
+ AnimationReplaceState ReplaceState() const { return mReplaceState; }
+
+ virtual Promise* GetReady(ErrorResult& aRv);
+ Promise* GetFinished(ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(finish);
+ IMPL_EVENT_HANDLER(cancel);
+ IMPL_EVENT_HANDLER(remove);
+
+ void Cancel(PostRestyleMode aPostRestyle = PostRestyleMode::IfNeeded);
+
+ void Finish(ErrorResult& aRv);
+
+ void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior);
+ virtual void PlayFromJS(ErrorResult& aRv) {
+ Play(aRv, LimitBehavior::AutoRewind);
+ }
+
+ void Pause(ErrorResult& aRv);
+ virtual void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
+
+ void UpdatePlaybackRate(double aPlaybackRate);
+ virtual void Reverse(ErrorResult& aRv);
+
+ void Persist();
+ MOZ_CAN_RUN_SCRIPT void CommitStyles(ErrorResult& aRv);
+
+ bool IsRunningOnCompositor() const;
+
+ virtual void Tick();
+ bool NeedsTicks() const {
+ return Pending() ||
+ (PlayState() == AnimationPlayState::Running &&
+ // An animation with a zero playback rate doesn't need ticks even if
+ // it is running since it effectively behaves as if it is paused.
+ //
+ // It's important we return false in this case since a zero playback
+ // rate animation in the before or after phase that doesn't fill
+ // won't be relevant and hence won't be returned by GetAnimations().
+ // We don't want its timeline to keep it alive (which would happen
+ // if we return true) since otherwise it will effectively be leaked.
+ PlaybackRate() != 0.0) ||
+ // Always return true for not idle animations attached to not
+ // monotonically increasing timelines even if the animation is
+ // finished. This is required to accommodate cases where timeline
+ // ticks back in time.
+ (mTimeline && !mTimeline->IsMonotonicallyIncreasing() &&
+ PlayState() != AnimationPlayState::Idle);
+ }
+
+ /**
+ * Set the time to use for starting or pausing a pending animation.
+ *
+ * Typically, when an animation is played, it does not start immediately but
+ * is added to a table of pending animations on the document of its effect.
+ * In the meantime it sets its hold time to the time from which playback
+ * should begin.
+ *
+ * When the document finishes painting, any pending animations in its table
+ * are marked as being ready to start by calling TriggerOnNextTick.
+ * The moment when the paint completed is also recorded, converted to a
+ * timeline time, and passed to StartOnTick. This is so that when these
+ * animations do start, they can be timed from the point when painting
+ * completed.
+ *
+ * After calling TriggerOnNextTick, animations remain in the pending state
+ * until the next refresh driver tick. At that time they transition out of
+ * the pending state using the time passed to TriggerOnNextTick as the
+ * effective time at which they resumed.
+ *
+ * This approach means that any setup time required for performing the
+ * initial paint of an animation such as layerization is not deducted from
+ * the running time of the animation. Without this we can easily drop the
+ * first few frames of an animation, or, on slower devices, the whole
+ * animation.
+ *
+ * Furthermore:
+ *
+ * - Starting the animation immediately when painting finishes is problematic
+ * because the start time of the animation will be ahead of its timeline
+ * (since the timeline time is based on the refresh driver time).
+ * That's a problem because the animation is playing but its timing
+ * suggests it starts in the future. We could update the timeline to match
+ * the start time of the animation but then we'd also have to update the
+ * timing and style of all animations connected to that timeline or else be
+ * stuck in an inconsistent state until the next refresh driver tick.
+ *
+ * - If we simply use the refresh driver time on its next tick, the lag
+ * between triggering an animation and its effective start is unacceptably
+ * long.
+ *
+ * For pausing, we apply the same asynchronous approach. This is so that we
+ * synchronize with animations that are running on the compositor. Otherwise
+ * if the main thread lags behind the compositor there will be a noticeable
+ * jump backwards when the main thread takes over. Even though main thread
+ * animations could be paused immediately, we do it asynchronously for
+ * consistency and so that animations paused together end up in step.
+ *
+ * Note that the caller of this method is responsible for removing the
+ * animation from any PendingAnimationTracker it may have been added to.
+ */
+ void TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime);
+ /**
+ * For the monotonically increasing timeline, we use this only for testing:
+ * Start or pause a pending animation using the current timeline time. This
+ * is used to support existing tests that expect animations to begin
+ * immediately. Ideally we would rewrite the those tests and get rid of this
+ * method, but there are a lot of them.
+ *
+ * As with TriggerOnNextTick, the caller of this method is responsible for
+ * removing the animation from any PendingAnimationTracker it may have been
+ * added to.
+ */
+ void TriggerNow();
+ /**
+ * For the non-monotonically increasing timeline (e.g. ScrollTimeline), we try
+ * to trigger it in ScrollTimelineAnimationTracker by this method. This uses
+ * the current scroll position as the ready time. Return true if we don't need
+ * to trigger it or we trigger it successfully.
+ */
+ bool TryTriggerNowForFiniteTimeline();
+ /**
+ * When TriggerOnNextTick is called, we store the ready time but we don't
+ * apply it until the next tick. In the meantime, GetStartTime() will return
+ * null.
+ *
+ * However, if we build layer animations again before the next tick, we
+ * should initialize them with the start time that GetStartTime() will return
+ * on the next tick.
+ *
+ * If we were to simply set the start time of layer animations to null, their
+ * start time would be updated to the current wallclock time when rendering
+ * finishes, thus making them out of sync with the start time stored here.
+ * This, in turn, will make the animation jump backwards when we build
+ * animations on the next tick and apply the start time stored here.
+ *
+ * This method returns the start time, if resolved. Otherwise, if we have
+ * a pending ready time, it returns the corresponding start time. If neither
+ * of those are available, it returns null.
+ */
+ Nullable<TimeDuration> GetCurrentOrPendingStartTime() const;
+
+ /**
+ * As with the start time, we should use the pending playback rate when
+ * producing layer animations.
+ */
+ double CurrentOrPendingPlaybackRate() const {
+ return mPendingPlaybackRate.valueOr(mPlaybackRate);
+ }
+ bool HasPendingPlaybackRate() const { return mPendingPlaybackRate.isSome(); }
+
+ /**
+ * The following relationship from the definition of the 'current time' is
+ * re-used in many algorithms so we extract it here into a static method that
+ * can be re-used:
+ *
+ * current time = (timeline time - start time) * playback rate
+ *
+ * As per https://drafts.csswg.org/web-animations-1/#current-time
+ */
+ static TimeDuration CurrentTimeFromTimelineTime(
+ const TimeDuration& aTimelineTime, const TimeDuration& aStartTime,
+ float aPlaybackRate) {
+ return (aTimelineTime - aStartTime).MultDouble(aPlaybackRate);
+ }
+
+ /**
+ * As with calculating the current time, we often need to calculate a start
+ * time from a current time. The following method simply inverts the current
+ * time relationship.
+ *
+ * In each case where this is used, the desired behavior for playbackRate ==
+ * 0 is to return the specified timeline time (often referred to as the ready
+ * time).
+ */
+ static TimeDuration StartTimeFromTimelineTime(
+ const TimeDuration& aTimelineTime, const TimeDuration& aCurrentTime,
+ float aPlaybackRate) {
+ TimeDuration result = aTimelineTime;
+ if (aPlaybackRate == 0) {
+ return result;
+ }
+
+ result -= aCurrentTime.MultDouble(1.0 / aPlaybackRate);
+ return result;
+ }
+
+ /**
+ * Converts a time in the timescale of this Animation's currentTime, to a
+ * TimeStamp. Returns a null TimeStamp if the conversion cannot be performed
+ * because of the current state of this Animation (e.g. it has no timeline, a
+ * zero playbackRate, an unresolved start time etc.) or the value of the time
+ * passed-in (e.g. an infinite time).
+ */
+ TimeStamp AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const;
+
+ // Converts an AnimationEvent's elapsedTime value to an equivalent TimeStamp
+ // that can be used to sort events by when they occurred.
+ TimeStamp ElapsedTimeToTimeStamp(
+ const StickyTimeDuration& aElapsedTime) const;
+
+ bool IsPausedOrPausing() const {
+ return PlayState() == AnimationPlayState::Paused;
+ }
+
+ bool HasCurrentEffect() const;
+ bool IsInEffect() const;
+
+ bool IsPlaying() const {
+ return mPlaybackRate != 0.0 && mTimeline &&
+ !mTimeline->GetCurrentTimeAsDuration().IsNull() &&
+ PlayState() == AnimationPlayState::Running;
+ }
+
+ bool ShouldBeSynchronizedWithMainThread(
+ const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const;
+
+ bool IsRelevant() const { return mIsRelevant; }
+ void UpdateRelevance();
+
+ // https://drafts.csswg.org/web-animations-1/#replaceable-animation
+ bool IsReplaceable() const;
+
+ /**
+ * Returns true if this Animation satisfies the requirements for being
+ * removed when it is replaced.
+ *
+ * Returning true does not imply this animation _should_ be removed.
+ * Determining that depends on the other effects in the same EffectSet to
+ * which this animation's effect, if any, contributes.
+ */
+ bool IsRemovable() const;
+
+ /**
+ * Make this animation's target effect no-longer part of the effect stack
+ * while preserving its timing information.
+ */
+ void Remove();
+
+ /**
+ * Returns true if this Animation has a lower composite order than aOther.
+ */
+ bool HasLowerCompositeOrderThan(const Animation& aOther) const;
+
+ /**
+ * Returns the level at which the effect(s) associated with this Animation
+ * are applied to the CSS cascade.
+ */
+ virtual EffectCompositor::CascadeLevel CascadeLevel() const {
+ return EffectCompositor::CascadeLevel::Animations;
+ }
+
+ /**
+ * Returns true if this animation does not currently need to update
+ * style on the main thread (e.g. because it is empty, or is
+ * running on the compositor).
+ */
+ bool CanThrottle() const;
+
+ /**
+ * Updates various bits of state that we need to update as the result of
+ * running ComposeStyle().
+ * See the comment of KeyframeEffect::WillComposeStyle for more detail.
+ */
+ void WillComposeStyle();
+
+ /**
+ * Updates |aComposeResult| with the animation values of this animation's
+ * effect, if any.
+ * Any properties contained in |aPropertiesToSkip| will not be added or
+ * updated in |aComposeResult|.
+ */
+ void ComposeStyle(StyleAnimationValueMap& aComposeResult,
+ const nsCSSPropertyIDSet& aPropertiesToSkip);
+
+ void NotifyEffectTimingUpdated();
+ void NotifyEffectPropertiesUpdated();
+ void NotifyEffectTargetUpdated();
+ void NotifyGeometricAnimationsStartingThisFrame();
+
+ /**
+ * Reschedule pending pause or pending play tasks when updating the target
+ * effect.
+ *
+ * If we are pending, we will either be registered in the pending animation
+ * tracker and have a null pending ready time, or, after our effect has been
+ * painted, we will be removed from the tracker and assigned a pending ready
+ * time.
+ *
+ * When the target effect is updated, we'll typically need to repaint so for
+ * the latter case where we already have a pending ready time, clear it and
+ * put ourselves back in the pending animation tracker.
+ */
+ void ReschedulePendingTasks();
+
+ /**
+ * Used by subclasses to synchronously queue a cancel event in situations
+ * where the Animation may have been cancelled.
+ *
+ * We need to do this synchronously because after a CSS animation/transition
+ * is canceled, it will be released by its owning element and may not still
+ * exist when we would normally go to queue events on the next tick.
+ */
+ virtual void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime){};
+
+ Maybe<uint32_t>& CachedChildIndexRef() { return mCachedChildIndex; }
+
+ void SetPartialPrerendered(uint64_t aIdOnCompositor) {
+ mIdOnCompositor = aIdOnCompositor;
+ mIsPartialPrerendered = true;
+ }
+ bool IsPartialPrerendered() const { return mIsPartialPrerendered; }
+ uint64_t IdOnCompositor() const { return mIdOnCompositor; }
+ /**
+ * Needs to be called when the pre-rendered animation is going to no longer
+ * run on the compositor.
+ */
+ void ResetPartialPrerendered() {
+ MOZ_ASSERT(mIsPartialPrerendered);
+ mIsPartialPrerendered = false;
+ mIdOnCompositor = 0;
+ }
+ /**
+ * Called via NotifyJankedAnimations IPC call from the compositor to update
+ * pre-rendered area on the main-thread.
+ */
+ void UpdatePartialPrerendered() {
+ ResetPartialPrerendered();
+ PostUpdate();
+ }
+
+ bool UsingScrollTimeline() const {
+ return mTimeline && mTimeline->IsScrollTimeline();
+ }
+
+ /**
+ * Returns true if this is at the progress timeline boundary.
+ * https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary
+ */
+ enum class ProgressTimelinePosition : uint8_t { Boundary, NotBoundary };
+ static ProgressTimelinePosition AtProgressTimelineBoundary(
+ const Nullable<TimeDuration>& aTimelineDuration,
+ const Nullable<TimeDuration>& aCurrentTime,
+ const TimeDuration& aEffectStartTime, const double aPlaybackRate);
+ ProgressTimelinePosition AtProgressTimelineBoundary() const {
+ Nullable<TimeDuration> currentTime = GetUnconstrainedCurrentTime();
+ return AtProgressTimelineBoundary(
+ mTimeline ? mTimeline->TimelineDuration() : nullptr,
+ // Set unlimited current time based on the first matching condition:
+ // 1. start time is resolved:
+ // (timeline time - start time) × playback rate
+ // 2. Otherwise:
+ // animation’s current time
+ !currentTime.IsNull() ? currentTime : GetCurrentTimeAsDuration(),
+ mStartTime.IsNull() ? TimeDuration() : mStartTime.Value(),
+ mPlaybackRate);
+ }
+
+ void SetHiddenByContentVisibility(bool hidden);
+ bool IsHiddenByContentVisibility() const {
+ return mHiddenByContentVisibility;
+ }
+
+ DocGroup* GetDocGroup();
+
+ protected:
+ void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
+ void CancelNoUpdate();
+ void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior);
+ void ResumeAt(const TimeDuration& aReadyTime);
+ void PauseAt(const TimeDuration& aReadyTime);
+ void FinishPendingAt(const TimeDuration& aReadyTime) {
+ if (mPendingState == PendingState::PlayPending) {
+ ResumeAt(aReadyTime);
+ } else if (mPendingState == PendingState::PausePending) {
+ PauseAt(aReadyTime);
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "Can't finish pending if we're not in a pending state");
+ }
+ }
+ void ApplyPendingPlaybackRate() {
+ if (mPendingPlaybackRate) {
+ mPlaybackRate = *mPendingPlaybackRate;
+ mPendingPlaybackRate.reset();
+ }
+ }
+
+ /**
+ * Finishing behavior depends on if changes to timing occurred due
+ * to a seek or regular playback.
+ */
+ enum class SeekFlag { NoSeek, DidSeek };
+
+ enum class SyncNotifyFlag { Sync, Async };
+
+ virtual void UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag);
+ void UpdateFinishedState(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag);
+ void UpdateEffect(PostRestyleMode aPostRestyle);
+ /**
+ * Flush all pending styles other than throttled animation styles (e.g.
+ * animations running on the compositor).
+ */
+ void FlushUnanimatedStyle() const;
+ void PostUpdate();
+ void ResetFinishedPromise();
+ void MaybeResolveFinishedPromise();
+ void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag);
+ friend class AsyncFinishNotification;
+ void DoFinishNotificationImmediately(MicroTaskRunnable* aAsync = nullptr);
+ void QueuePlaybackEvent(const nsAString& aName,
+ TimeStamp&& aScheduledEventTime);
+
+ /**
+ * Remove this animation from the pending animation tracker and reset
+ * mPendingState as necessary. The caller is responsible for resolving or
+ * aborting the mReady promise as necessary.
+ */
+ void CancelPendingTasks();
+
+ /**
+ * Performs the same steps as CancelPendingTasks and also rejects and
+ * recreates the ready promise if the animation was pending.
+ */
+ void ResetPendingTasks();
+
+ /**
+ * Returns true if this animation is not only play-pending, but has
+ * yet to be given a pending ready time. This roughly corresponds to
+ * animations that are waiting to be painted (since we set the pending
+ * ready time at the end of painting). Identifying such animations is
+ * useful because in some cases animations that are painted together
+ * may need to be synchronized.
+ *
+ * We don't, however, want to include animations with a fixed start time such
+ * as animations that are simply having their playbackRate updated or which
+ * are resuming from an aborted pause.
+ */
+ bool IsNewlyStarted() const {
+ return mPendingState == PendingState::PlayPending &&
+ mPendingReadyTime.IsNull() && mStartTime.IsNull();
+ }
+ bool IsPossiblyOrphanedPendingAnimation() const;
+ StickyTimeDuration EffectEnd() const;
+
+ Nullable<TimeDuration> GetCurrentTimeForHoldTime(
+ const Nullable<TimeDuration>& aHoldTime) const;
+ Nullable<TimeDuration> GetUnconstrainedCurrentTime() const {
+ return GetCurrentTimeForHoldTime(Nullable<TimeDuration>());
+ }
+
+ void ScheduleReplacementCheck();
+ void MaybeScheduleReplacementCheck();
+
+ // Earlier side of the elapsed time range reported in CSS Animations and CSS
+ // Transitions events.
+ //
+ // https://drafts.csswg.org/css-animations-2/#interval-start
+ // https://drafts.csswg.org/css-transitions-2/#interval-start
+ StickyTimeDuration IntervalStartTime(
+ const StickyTimeDuration& aActiveDuration) const;
+
+ // Later side of the elapsed time range reported in CSS Animations and CSS
+ // Transitions events.
+ //
+ // https://drafts.csswg.org/css-animations-2/#interval-end
+ // https://drafts.csswg.org/css-transitions-2/#interval-end
+ StickyTimeDuration IntervalEndTime(
+ const StickyTimeDuration& aActiveDuration) const;
+
+ TimeStamp GetTimelineCurrentTimeAsTimeStamp() const {
+ return mTimeline ? mTimeline->GetCurrentTimeAsTimeStamp() : TimeStamp();
+ }
+
+ Document* GetRenderedDocument() const;
+ Document* GetTimelineDocument() const;
+
+ bool HasFiniteTimeline() const {
+ return mTimeline && !mTimeline->IsMonotonicallyIncreasing();
+ }
+
+ void UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline,
+ AnimationTimeline* aNewTimeline);
+
+ RefPtr<AnimationTimeline> mTimeline;
+ RefPtr<AnimationEffect> mEffect;
+ // The beginning of the delay period.
+ Nullable<TimeDuration> mStartTime; // Timeline timescale
+ Nullable<TimeDuration> mHoldTime; // Animation timescale
+ Nullable<TimeDuration> mPendingReadyTime; // Timeline timescale
+ Nullable<TimeDuration> mPreviousCurrentTime; // Animation timescale
+ double mPlaybackRate = 1.0;
+ Maybe<double> mPendingPlaybackRate;
+
+ // A Promise that is replaced on each call to Play()
+ // and fulfilled when Play() is successfully completed.
+ // This object is lazily created by GetReady.
+ // See http://drafts.csswg.org/web-animations/#current-ready-promise
+ RefPtr<Promise> mReady;
+
+ // A Promise that is resolved when we reach the end of the effect, or
+ // 0 when playing backwards. The Promise is replaced if the animation is
+ // finished but then a state change makes it not finished.
+ // This object is lazily created by GetFinished.
+ // See http://drafts.csswg.org/web-animations/#current-finished-promise
+ RefPtr<Promise> mFinished;
+
+ static uint64_t sNextAnimationIndex;
+
+ // The relative position of this animation within the global animation list.
+ //
+ // Note that subclasses such as CSSTransition and CSSAnimation may repurpose
+ // this member to implement their own brand of sorting. As a result, it is
+ // possible for two different objects to have the same index.
+ uint64_t mAnimationIndex;
+
+ // While ordering Animation objects for event dispatch, the index of the
+ // target node in its parent may be cached in mCachedChildIndex.
+ Maybe<uint32_t> mCachedChildIndex;
+
+ // Indicates if the animation is in the pending state (and what state it is
+ // waiting to enter when it finished pending). We use this rather than
+ // checking if this animation is tracked by a PendingAnimationTracker because
+ // the animation will continue to be pending even after it has been removed
+ // from the PendingAnimationTracker while it is waiting for the next tick
+ // (see TriggerOnNextTick for details).
+ enum class PendingState : uint8_t { NotPending, PlayPending, PausePending };
+ PendingState mPendingState = PendingState::NotPending;
+
+ // Handling of this animation's target effect when filling while finished.
+ AnimationReplaceState mReplaceState = AnimationReplaceState::Active;
+
+ bool mFinishedAtLastComposeStyle = false;
+ bool mWasReplaceableAtLastTick = false;
+
+ bool mHiddenByContentVisibility = false;
+
+ // Indicates that the animation should be exposed in an element's
+ // getAnimations() list.
+ bool mIsRelevant = false;
+
+ // True if mFinished is resolved or would be resolved if mFinished has
+ // yet to be created. This is not set when mFinished is rejected since
+ // in that case mFinished is immediately reset to represent a new current
+ // finished promise.
+ bool mFinishedIsResolved = false;
+
+ // True if this animation was triggered at the same time as one or more
+ // geometric animations and hence we should run any transform animations on
+ // the main thread.
+ bool mSyncWithGeometricAnimations = false;
+
+ RefPtr<MicroTaskRunnable> mFinishNotificationTask;
+
+ nsString mId;
+
+ bool mResetCurrentTimeOnResume = false;
+
+ // Whether the Animation is System, ResistFingerprinting, or neither
+ RTPCallerType mRTPCallerType;
+
+ private:
+ // The id for this animaiton on the compositor.
+ uint64_t mIdOnCompositor = 0;
+ bool mIsPartialPrerendered = false;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Animation_h
diff --git a/dom/animation/AnimationComparator.h b/dom/animation/AnimationComparator.h
new file mode 100644
index 0000000000..080eb42475
--- /dev/null
+++ b/dom/animation/AnimationComparator.h
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+#ifndef mozilla_AnimationComparator_h
+#define mozilla_AnimationComparator_h
+
+#include "mozilla/dom/Animation.h"
+
+namespace mozilla {
+
+// Although this file is called AnimationComparator, we don't actually
+// implement AnimationComparator (to compare const Animation& parameters)
+// since it's not actually needed (yet).
+
+template <typename AnimationPtrType>
+class AnimationPtrComparator {
+ public:
+ bool Equals(const AnimationPtrType& a, const AnimationPtrType& b) const {
+ return a == b;
+ }
+
+ bool LessThan(const AnimationPtrType& a, const AnimationPtrType& b) const {
+ return a->HasLowerCompositeOrderThan(*b);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AnimationComparator_h
diff --git a/dom/animation/AnimationEffect.cpp b/dom/animation/AnimationEffect.cpp
new file mode 100644
index 0000000000..6ef8c30d49
--- /dev/null
+++ b/dom/animation/AnimationEffect.cpp
@@ -0,0 +1,370 @@
+/* -*- 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/AnimationEffect.h"
+#include "mozilla/dom/AnimationEffectBinding.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/FloatingPoint.h"
+#include "nsDOMMutationObserver.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationEffect)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffect)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mAnimation)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffect)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mAnimation)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffect)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffect)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffect)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+AnimationEffect::AnimationEffect(Document* aDocument, TimingParams&& aTiming)
+ : mDocument(aDocument), mTiming(std::move(aTiming)) {
+ mRTPCallerType = mDocument->GetScopeObject()->GetRTPCallerType();
+}
+
+AnimationEffect::~AnimationEffect() = default;
+
+nsISupports* AnimationEffect::GetParentObject() const {
+ return ToSupports(mDocument);
+}
+
+// https://drafts.csswg.org/web-animations/#current
+bool AnimationEffect::IsCurrent() const {
+ if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
+ return false;
+ }
+
+ ComputedTiming computedTiming = GetComputedTiming();
+ if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) {
+ return true;
+ }
+
+ return (mAnimation->PlaybackRate() > 0 &&
+ computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) ||
+ (mAnimation->PlaybackRate() < 0 &&
+ computedTiming.mPhase == ComputedTiming::AnimationPhase::After);
+}
+
+// https://drafts.csswg.org/web-animations/#in-effect
+bool AnimationEffect::IsInEffect() const {
+ ComputedTiming computedTiming = GetComputedTiming();
+ return !computedTiming.mProgress.IsNull();
+}
+
+void AnimationEffect::SetSpecifiedTiming(TimingParams&& aTiming) {
+ if (mTiming == aTiming) {
+ return;
+ }
+
+ mTiming = aTiming;
+
+ UpdateNormalizedTiming();
+
+ if (mAnimation) {
+ Maybe<nsAutoAnimationMutationBatch> mb;
+ if (AsKeyframeEffect() && AsKeyframeEffect()->GetAnimationTarget()) {
+ mb.emplace(AsKeyframeEffect()->GetAnimationTarget().mElement->OwnerDoc());
+ }
+
+ mAnimation->NotifyEffectTimingUpdated();
+
+ if (mAnimation->IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(mAnimation);
+ }
+
+ if (AsKeyframeEffect()) {
+ AsKeyframeEffect()->RequestRestyle(EffectCompositor::RestyleType::Layer);
+ }
+ }
+
+ // For keyframe effects, NotifyEffectTimingUpdated above will eventually
+ // cause KeyframeEffect::NotifyAnimationTimingUpdated to be called so it can
+ // update its registration with the target element as necessary.
+}
+
+ComputedTiming AnimationEffect::GetComputedTimingAt(
+ const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
+ double aPlaybackRate,
+ Animation::ProgressTimelinePosition aProgressTimelinePosition) {
+ static const StickyTimeDuration zeroDuration;
+
+ // Always return the same object to benefit from return-value optimization.
+ ComputedTiming result;
+
+ if (aTiming.Duration()) {
+ MOZ_ASSERT(aTiming.Duration().ref() >= zeroDuration,
+ "Iteration duration should be positive");
+ result.mDuration = aTiming.Duration().ref();
+ }
+
+ MOZ_ASSERT(aTiming.Iterations() >= 0.0 && !std::isnan(aTiming.Iterations()),
+ "mIterations should be nonnegative & finite, as ensured by "
+ "ValidateIterations or CSSParser");
+ result.mIterations = aTiming.Iterations();
+
+ MOZ_ASSERT(aTiming.IterationStart() >= 0.0,
+ "mIterationStart should be nonnegative, as ensured by "
+ "ValidateIterationStart");
+ result.mIterationStart = aTiming.IterationStart();
+
+ result.mActiveDuration = aTiming.ActiveDuration();
+ result.mEndTime = aTiming.EndTime();
+ result.mFill = aTiming.Fill() == dom::FillMode::Auto ? dom::FillMode::None
+ : aTiming.Fill();
+
+ // The default constructor for ComputedTiming sets all other members to
+ // values consistent with an animation that has not been sampled.
+ if (aLocalTime.IsNull()) {
+ return result;
+ }
+ const TimeDuration& localTime = aLocalTime.Value();
+ const bool atProgressTimelineBoundary =
+ aProgressTimelinePosition ==
+ Animation::ProgressTimelinePosition::Boundary;
+
+ StickyTimeDuration beforeActiveBoundary = aTiming.CalcBeforeActiveBoundary();
+ StickyTimeDuration activeAfterBoundary = aTiming.CalcActiveAfterBoundary();
+
+ if (localTime > activeAfterBoundary ||
+ (aPlaybackRate >= 0 && localTime == activeAfterBoundary &&
+ !atProgressTimelineBoundary)) {
+ result.mPhase = ComputedTiming::AnimationPhase::After;
+ if (!result.FillsForwards()) {
+ // The animation isn't active or filling at this time.
+ return result;
+ }
+ result.mActiveTime =
+ std::max(std::min(StickyTimeDuration(localTime - aTiming.Delay()),
+ result.mActiveDuration),
+ zeroDuration);
+ } else if (localTime < beforeActiveBoundary ||
+ (aPlaybackRate < 0 && localTime == beforeActiveBoundary &&
+ !atProgressTimelineBoundary)) {
+ result.mPhase = ComputedTiming::AnimationPhase::Before;
+ if (!result.FillsBackwards()) {
+ // The animation isn't active or filling at this time.
+ return result;
+ }
+ result.mActiveTime =
+ std::max(StickyTimeDuration(localTime - aTiming.Delay()), zeroDuration);
+ } else {
+ // Note: For progress-based timeline, it's possible to have a zero active
+ // duration with active phase.
+ result.mPhase = ComputedTiming::AnimationPhase::Active;
+ result.mActiveTime = localTime - aTiming.Delay();
+ }
+
+ // Convert active time to a multiple of iterations.
+ // https://drafts.csswg.org/web-animations/#overall-progress
+ double overallProgress;
+ if (!result.mDuration) {
+ overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before
+ ? 0.0
+ : result.mIterations;
+ } else {
+ overallProgress = result.mActiveTime / result.mDuration;
+ }
+
+ // Factor in iteration start offset.
+ if (std::isfinite(overallProgress)) {
+ overallProgress += result.mIterationStart;
+ }
+
+ // Determine the 0-based index of the current iteration.
+ // https://drafts.csswg.org/web-animations/#current-iteration
+ result.mCurrentIteration =
+ (result.mIterations >= double(UINT64_MAX) &&
+ result.mPhase == ComputedTiming::AnimationPhase::After) ||
+ overallProgress >= double(UINT64_MAX)
+ ? UINT64_MAX // In GetComputedTimingDictionary(),
+ // we will convert this into Infinity
+ : static_cast<uint64_t>(std::max(overallProgress, 0.0));
+
+ // Convert the overall progress to a fraction of a single iteration--the
+ // simply iteration progress.
+ // https://drafts.csswg.org/web-animations/#simple-iteration-progress
+ double progress = std::isfinite(overallProgress)
+ ? fmod(overallProgress, 1.0)
+ : fmod(result.mIterationStart, 1.0);
+
+ // When we are at the end of the active interval and the end of an iteration
+ // we need to report the end of the final iteration and not the start of the
+ // next iteration. We *don't* want to do this, however, when we have
+ // a zero-iteration animation.
+ if (progress == 0.0 &&
+ (result.mPhase == ComputedTiming::AnimationPhase::After ||
+ result.mPhase == ComputedTiming::AnimationPhase::Active) &&
+ result.mActiveTime == result.mActiveDuration &&
+ result.mIterations != 0.0) {
+ // The only way we can reach the end of the active interval and have
+ // a progress of zero and a current iteration of zero, is if we have a
+ // zero iteration count -- something we should have detected above.
+ MOZ_ASSERT(result.mCurrentIteration != 0,
+ "Should not have zero current iteration");
+ progress = 1.0;
+ if (result.mCurrentIteration != UINT64_MAX) {
+ result.mCurrentIteration--;
+ }
+ }
+
+ // Factor in the direction.
+ bool thisIterationReverse = false;
+ switch (aTiming.Direction()) {
+ case PlaybackDirection::Normal:
+ thisIterationReverse = false;
+ break;
+ case PlaybackDirection::Reverse:
+ thisIterationReverse = true;
+ break;
+ case PlaybackDirection::Alternate:
+ thisIterationReverse = (result.mCurrentIteration & 1) == 1;
+ break;
+ case PlaybackDirection::Alternate_reverse:
+ thisIterationReverse = (result.mCurrentIteration & 1) == 0;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown PlaybackDirection type");
+ }
+ if (thisIterationReverse) {
+ progress = 1.0 - progress;
+ }
+
+ // Calculate the 'before flag' which we use when applying step timing
+ // functions.
+ if ((result.mPhase == ComputedTiming::AnimationPhase::After &&
+ thisIterationReverse) ||
+ (result.mPhase == ComputedTiming::AnimationPhase::Before &&
+ !thisIterationReverse)) {
+ result.mBeforeFlag = true;
+ }
+
+ // Apply the easing.
+ if (const auto& fn = aTiming.TimingFunction()) {
+ progress = fn->At(progress, result.mBeforeFlag);
+ }
+
+ MOZ_ASSERT(std::isfinite(progress), "Progress value should be finite");
+ result.mProgress.SetValue(progress);
+ return result;
+}
+
+ComputedTiming AnimationEffect::GetComputedTiming(
+ const TimingParams* aTiming) const {
+ const double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
+ const auto progressTimelinePosition =
+ mAnimation ? mAnimation->AtProgressTimelineBoundary()
+ : Animation::ProgressTimelinePosition::NotBoundary;
+ return GetComputedTimingAt(GetLocalTime(),
+ aTiming ? *aTiming : NormalizedTiming(),
+ playbackRate, progressTimelinePosition);
+}
+
+// Helper function for generating an (Computed)EffectTiming dictionary
+static void GetEffectTimingDictionary(const TimingParams& aTiming,
+ EffectTiming& aRetVal) {
+ aRetVal.mDelay = aTiming.Delay().ToMilliseconds();
+ aRetVal.mEndDelay = aTiming.EndDelay().ToMilliseconds();
+ aRetVal.mFill = aTiming.Fill();
+ aRetVal.mIterationStart = aTiming.IterationStart();
+ aRetVal.mIterations = aTiming.Iterations();
+ if (aTiming.Duration()) {
+ aRetVal.mDuration.SetAsUnrestrictedDouble() =
+ aTiming.Duration()->ToMilliseconds();
+ }
+ aRetVal.mDirection = aTiming.Direction();
+ if (aTiming.TimingFunction()) {
+ aRetVal.mEasing.Truncate();
+ aTiming.TimingFunction()->AppendToString(aRetVal.mEasing);
+ }
+}
+
+void AnimationEffect::GetTiming(EffectTiming& aRetVal) const {
+ GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
+}
+
+void AnimationEffect::GetComputedTimingAsDict(
+ ComputedEffectTiming& aRetVal) const {
+ // Specified timing
+ GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
+
+ // Computed timing
+ double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
+ const Nullable<TimeDuration> currentTime = GetLocalTime();
+ const auto progressTimelinePosition =
+ mAnimation ? mAnimation->AtProgressTimelineBoundary()
+ : Animation::ProgressTimelinePosition::NotBoundary;
+ ComputedTiming computedTiming = GetComputedTimingAt(
+ currentTime, SpecifiedTiming(), playbackRate, progressTimelinePosition);
+
+ aRetVal.mDuration.SetAsUnrestrictedDouble() =
+ computedTiming.mDuration.ToMilliseconds();
+ aRetVal.mFill = computedTiming.mFill;
+ aRetVal.mActiveDuration = computedTiming.mActiveDuration.ToMilliseconds();
+ aRetVal.mEndTime = computedTiming.mEndTime.ToMilliseconds();
+ aRetVal.mLocalTime =
+ AnimationUtils::TimeDurationToDouble(currentTime, mRTPCallerType);
+ aRetVal.mProgress = computedTiming.mProgress;
+
+ if (!aRetVal.mProgress.IsNull()) {
+ // Convert the returned currentIteration into Infinity if we set
+ // (uint64_t) computedTiming.mCurrentIteration to UINT64_MAX
+ double iteration =
+ computedTiming.mCurrentIteration == UINT64_MAX
+ ? PositiveInfinity<double>()
+ : static_cast<double>(computedTiming.mCurrentIteration);
+ aRetVal.mCurrentIteration.SetValue(iteration);
+ }
+}
+
+void AnimationEffect::UpdateTiming(const OptionalEffectTiming& aTiming,
+ ErrorResult& aRv) {
+ TimingParams timing =
+ TimingParams::MergeOptionalEffectTiming(mTiming, aTiming, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SetSpecifiedTiming(std::move(timing));
+}
+
+void AnimationEffect::UpdateNormalizedTiming() {
+ mNormalizedTiming.reset();
+
+ if (!mAnimation || !mAnimation->UsingScrollTimeline()) {
+ return;
+ }
+
+ // Since `mAnimation` has a scroll timeline, we can be sure `GetTimeline()`
+ // and `TimelineDuration()` will not return null.
+ mNormalizedTiming.emplace(
+ mTiming.Normalize(mAnimation->GetTimeline()->TimelineDuration().Value()));
+}
+
+Nullable<TimeDuration> AnimationEffect::GetLocalTime() const {
+ // Since the *animation* start time is currently always zero, the local
+ // time is equal to the parent time.
+ Nullable<TimeDuration> result;
+ if (mAnimation) {
+ result = mAnimation->GetCurrentTimeAsDuration();
+ }
+ return result;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/AnimationEffect.h b/dom/animation/AnimationEffect.h
new file mode 100644
index 0000000000..7dbd51c2bf
--- /dev/null
+++ b/dom/animation/AnimationEffect.h
@@ -0,0 +1,117 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AnimationEffect_h
+#define mozilla_dom_AnimationEffect_h
+
+#include "mozilla/ComputedTiming.h"
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/ScrollTimeline.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/TimingParams.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class KeyframeEffect;
+struct ComputedEffectTiming;
+struct EffectTiming;
+struct OptionalEffectTiming;
+class Document;
+
+class AnimationEffect : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationEffect)
+
+ AnimationEffect(Document* aDocument, TimingParams&& aTiming);
+
+ virtual KeyframeEffect* AsKeyframeEffect() { return nullptr; }
+
+ nsISupports* GetParentObject() const;
+
+ bool IsCurrent() const;
+ bool IsInEffect() const;
+ bool HasFiniteActiveDuration() const {
+ return NormalizedTiming().ActiveDuration() != TimeDuration::Forever();
+ }
+
+ // AnimationEffect interface
+ virtual void GetTiming(EffectTiming& aRetVal) const;
+ virtual void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const;
+ virtual void UpdateTiming(const OptionalEffectTiming& aTiming,
+ ErrorResult& aRv);
+
+ const TimingParams& SpecifiedTiming() const { return mTiming; }
+ void SetSpecifiedTiming(TimingParams&& aTiming);
+
+ const TimingParams& NormalizedTiming() const {
+ MOZ_ASSERT((mAnimation && mAnimation->UsingScrollTimeline() &&
+ mNormalizedTiming) ||
+ !mNormalizedTiming,
+ "We do normalization only for progress-based timeline");
+ return mNormalizedTiming ? *mNormalizedTiming : mTiming;
+ }
+
+ // There are 3 conditions where we have to update the normalized timing:
+ // 1. mAnimation is changed, or
+ // 2. the timeline of mAnimation is changed, or
+ // 3. mTiming is changed.
+ void UpdateNormalizedTiming();
+
+ // This function takes as input the timing parameters of an animation and
+ // returns the computed timing at the specified local time.
+ //
+ // The local time may be null in which case only static parameters such as the
+ // active duration are calculated. All other members of the returned object
+ // are given a null/initial value.
+ //
+ // This function returns a null mProgress member of the return value
+ // if the animation should not be run
+ // (because it is not currently active and is not filling at this time).
+ static ComputedTiming GetComputedTimingAt(
+ const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
+ double aPlaybackRate,
+ Animation::ProgressTimelinePosition aProgressTimelinePosition);
+ // Shortcut that gets the computed timing using the current local time as
+ // calculated from the timeline time.
+ ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;
+
+ virtual void SetAnimation(Animation* aAnimation) = 0;
+ Animation* GetAnimation() const { return mAnimation; };
+
+ /**
+ * Returns true if this effect animates one of the properties we consider
+ * geometric properties, e.g. properties such as 'width' or 'margin-left'
+ * that we try to synchronize with transform animations, on a valid target
+ * element.
+ */
+ virtual bool AffectsGeometry() const = 0;
+
+ protected:
+ virtual ~AnimationEffect();
+
+ Nullable<TimeDuration> GetLocalTime() const;
+
+ protected:
+ RefPtr<Document> mDocument;
+ RefPtr<Animation> mAnimation;
+ TimingParams mTiming;
+ Maybe<TimingParams> mNormalizedTiming;
+
+ // Whether the Animation is System, ResistFingerprinting, or neither
+ enum RTPCallerType mRTPCallerType;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationEffect_h
diff --git a/dom/animation/AnimationEventDispatcher.cpp b/dom/animation/AnimationEventDispatcher.cpp
new file mode 100644
index 0000000000..b4c55cddce
--- /dev/null
+++ b/dom/animation/AnimationEventDispatcher.cpp
@@ -0,0 +1,62 @@
+/* -*- 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/AnimationEventDispatcher.h"
+
+#include "mozilla/EventDispatcher.h"
+#include "nsPresContext.h"
+#include "nsRefreshDriver.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEventDispatcher)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEventDispatcher)
+ tmp->ClearEventQueue();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEventDispatcher)
+ for (auto& info : tmp->mPendingEvents) {
+ ImplCycleCollectionTraverse(
+ cb, info.mTarget,
+ "mozilla::AnimationEventDispatcher.mPendingEvents.mTarget");
+ ImplCycleCollectionTraverse(
+ cb, info.mAnimation,
+ "mozilla::AnimationEventDispatcher.mPendingEvents.mAnimation");
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+void AnimationEventDispatcher::Disconnect() {
+ if (mIsObserving) {
+ MOZ_ASSERT(mPresContext && mPresContext->RefreshDriver(),
+ "The pres context and the refresh driver should be still "
+ "alive if we haven't disassociated from the refresh driver");
+ mPresContext->RefreshDriver()->CancelPendingAnimationEvents(this);
+ mIsObserving = false;
+ }
+ mPresContext = nullptr;
+}
+
+void AnimationEventDispatcher::QueueEvent(AnimationEventInfo&& aEvent) {
+ mPendingEvents.AppendElement(std::move(aEvent));
+ mIsSorted = false;
+ ScheduleDispatch();
+}
+
+void AnimationEventDispatcher::QueueEvents(
+ nsTArray<AnimationEventInfo>&& aEvents) {
+ mPendingEvents.AppendElements(std::move(aEvents));
+ mIsSorted = false;
+ ScheduleDispatch();
+}
+
+void AnimationEventDispatcher::ScheduleDispatch() {
+ MOZ_ASSERT(mPresContext, "The pres context should be valid");
+ if (!mIsObserving) {
+ mPresContext->RefreshDriver()->ScheduleAnimationEventDispatch(this);
+ mIsObserving = true;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/animation/AnimationEventDispatcher.h b/dom/animation/AnimationEventDispatcher.h
new file mode 100644
index 0000000000..3fe0663829
--- /dev/null
+++ b/dom/animation/AnimationEventDispatcher.h
@@ -0,0 +1,407 @@
+/* -*- 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/. */
+
+#ifndef mozilla_AnimationEventDispatcher_h
+#define mozilla_AnimationEventDispatcher_h
+
+#include <algorithm> // For <std::stable_sort>
+#include "mozilla/AnimationComparator.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/AnimationEffect.h"
+#include "mozilla/dom/AnimationPlaybackEvent.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "nsCSSProps.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsPresContext.h"
+
+class nsRefreshDriver;
+
+namespace geckoprofiler::markers {
+
+using namespace mozilla;
+
+struct CSSAnimationMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("CSSAnimation");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ const nsCString& aName,
+ const nsCString& aTarget,
+ nsCSSPropertyIDSet aPropertySet) {
+ aWriter.StringProperty("Name", aName);
+ aWriter.StringProperty("Target", aTarget);
+ nsAutoCString properties;
+ nsAutoCString oncompositor;
+ for (nsCSSPropertyID property : aPropertySet) {
+ if (!properties.IsEmpty()) {
+ properties.AppendLiteral(", ");
+ oncompositor.AppendLiteral(", ");
+ }
+ properties.Append(nsCSSProps::GetStringValue(property));
+ oncompositor.Append(nsCSSProps::PropHasFlags(
+ property, CSSPropFlags::CanAnimateOnCompositor)
+ ? "true"
+ : "false");
+ }
+
+ aWriter.StringProperty("properties", properties);
+ aWriter.StringProperty("oncompositor", oncompositor);
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyFormatSearchable("Name", MS::Format::String,
+ MS::Searchable::Searchable);
+ schema.AddKeyLabelFormat("properties", "Animated Properties",
+ MS::Format::String);
+ schema.AddKeyLabelFormat("oncompositor", "Can Run on Compositor",
+ MS::Format::String);
+ schema.AddKeyFormat("Target", MS::Format::String);
+ schema.SetChartLabel("{marker.data.Name}");
+ schema.SetTableLabel(
+ "{marker.name} - {marker.data.Name}: {marker.data.properties}");
+ return schema;
+ }
+};
+
+struct CSSTransitionMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("CSSTransition");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ const nsCString& aTarget,
+ nsCSSPropertyID aProperty, bool aCanceled) {
+ aWriter.StringProperty("Target", aTarget);
+ aWriter.StringProperty("property", nsCSSProps::GetStringValue(aProperty));
+ aWriter.BoolProperty("oncompositor",
+ nsCSSProps::PropHasFlags(
+ aProperty, CSSPropFlags::CanAnimateOnCompositor));
+ if (aCanceled) {
+ aWriter.BoolProperty("Canceled", aCanceled);
+ }
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyLabelFormat("property", "Animated Property",
+ MS::Format::String);
+ schema.AddKeyLabelFormat("oncompositor", "Can Run on Compositor",
+ MS::Format::String);
+ schema.AddKeyFormat("Canceled", MS::Format::String);
+ schema.AddKeyFormat("Target", MS::Format::String);
+ schema.SetChartLabel("{marker.data.property}");
+ schema.SetTableLabel("{marker.name} - {marker.data.property}");
+ return schema;
+ }
+};
+
+} // namespace geckoprofiler::markers
+
+namespace mozilla {
+
+struct AnimationEventInfo {
+ RefPtr<dom::EventTarget> mTarget;
+ RefPtr<dom::Animation> mAnimation;
+ TimeStamp mScheduledEventTimeStamp;
+
+ typedef Variant<InternalTransitionEvent, InternalAnimationEvent,
+ RefPtr<dom::AnimationPlaybackEvent>>
+ EventVariant;
+ EventVariant mEvent;
+
+ // For CSS animation events
+ AnimationEventInfo(nsAtom* aAnimationName,
+ const NonOwningAnimationTarget& aTarget,
+ EventMessage aMessage, double aElapsedTime,
+ const TimeStamp& aScheduledEventTimeStamp,
+ dom::Animation* aAnimation)
+ : mTarget(aTarget.mElement),
+ mAnimation(aAnimation),
+ mScheduledEventTimeStamp(aScheduledEventTimeStamp),
+ mEvent(EventVariant(InternalAnimationEvent(true, aMessage))) {
+ InternalAnimationEvent& event = mEvent.as<InternalAnimationEvent>();
+
+ aAnimationName->ToString(event.mAnimationName);
+ // XXX Looks like nobody initialize WidgetEvent::time
+ event.mElapsedTime = aElapsedTime;
+ event.mPseudoElement =
+ nsCSSPseudoElements::PseudoTypeAsString(aTarget.mPseudoType);
+
+ if ((aMessage == eAnimationCancel || aMessage == eAnimationEnd ||
+ aMessage == eAnimationIteration) &&
+ profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString name;
+ aAnimationName->ToUTF8String(name);
+
+ const TimeStamp startTime = [&] {
+ if (aMessage == eAnimationIteration) {
+ if (auto* effect = aAnimation->GetEffect()) {
+ return aScheduledEventTimeStamp -
+ TimeDuration(effect->GetComputedTiming().mDuration);
+ }
+ }
+ return aScheduledEventTimeStamp -
+ TimeDuration::FromSeconds(aElapsedTime);
+ }();
+
+ nsCSSPropertyIDSet propertySet;
+ nsAutoString target;
+ if (dom::AnimationEffect* effect = aAnimation->GetEffect()) {
+ if (dom::KeyframeEffect* keyFrameEffect = effect->AsKeyframeEffect()) {
+ keyFrameEffect->GetTarget()->Describe(target, true);
+ for (const AnimationProperty& property :
+ keyFrameEffect->Properties()) {
+ propertySet.AddProperty(property.mProperty);
+ }
+ }
+ }
+
+ PROFILER_MARKER(
+ aMessage == eAnimationIteration
+ ? ProfilerString8View("CSS animation iteration")
+ : ProfilerString8View("CSS animation"),
+ DOM,
+ MarkerOptions(
+ MarkerTiming::Interval(startTime, aScheduledEventTimeStamp),
+ aAnimation->GetOwner()
+ ? MarkerInnerWindowId(aAnimation->GetOwner()->WindowID())
+ : MarkerInnerWindowId::NoId()),
+ CSSAnimationMarker, name, NS_ConvertUTF16toUTF8(target), propertySet);
+ }
+ }
+
+ // For CSS transition events
+ AnimationEventInfo(nsCSSPropertyID aProperty,
+ const NonOwningAnimationTarget& aTarget,
+ EventMessage aMessage, double aElapsedTime,
+ const TimeStamp& aScheduledEventTimeStamp,
+ dom::Animation* aAnimation)
+ : mTarget(aTarget.mElement),
+ mAnimation(aAnimation),
+ mScheduledEventTimeStamp(aScheduledEventTimeStamp),
+ mEvent(EventVariant(InternalTransitionEvent(true, aMessage))) {
+ InternalTransitionEvent& event = mEvent.as<InternalTransitionEvent>();
+
+ event.mPropertyName =
+ NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(aProperty));
+ // XXX Looks like nobody initialize WidgetEvent::time
+ event.mElapsedTime = aElapsedTime;
+ event.mPseudoElement =
+ nsCSSPseudoElements::PseudoTypeAsString(aTarget.mPseudoType);
+
+ if ((aMessage == eTransitionEnd || aMessage == eTransitionCancel) &&
+ profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoString target;
+ if (dom::AnimationEffect* effect = aAnimation->GetEffect()) {
+ if (dom::KeyframeEffect* keyFrameEffect = effect->AsKeyframeEffect()) {
+ keyFrameEffect->GetTarget()->Describe(target, true);
+ }
+ }
+ PROFILER_MARKER(
+ "CSS transition", DOM,
+ MarkerOptions(
+ MarkerTiming::Interval(
+ aScheduledEventTimeStamp -
+ TimeDuration::FromSeconds(aElapsedTime),
+ aScheduledEventTimeStamp),
+ aAnimation->GetOwner()
+ ? MarkerInnerWindowId(aAnimation->GetOwner()->WindowID())
+ : MarkerInnerWindowId::NoId()),
+ CSSTransitionMarker, NS_ConvertUTF16toUTF8(target), aProperty,
+ aMessage == eTransitionCancel);
+ }
+ }
+
+ // For web animation events
+ AnimationEventInfo(const nsAString& aName,
+ RefPtr<dom::AnimationPlaybackEvent>&& aEvent,
+ TimeStamp&& aScheduledEventTimeStamp,
+ dom::Animation* aAnimation)
+ : mTarget(aAnimation),
+ mAnimation(aAnimation),
+ mScheduledEventTimeStamp(std::move(aScheduledEventTimeStamp)),
+ mEvent(std::move(aEvent)) {}
+
+ AnimationEventInfo(const AnimationEventInfo& aOther) = delete;
+ AnimationEventInfo& operator=(const AnimationEventInfo& aOther) = delete;
+ AnimationEventInfo(AnimationEventInfo&& aOther) = default;
+ AnimationEventInfo& operator=(AnimationEventInfo&& aOther) = default;
+
+ bool IsWebAnimationEvent() const {
+ return mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>();
+ }
+
+#ifdef DEBUG
+ bool IsStale() const {
+ const WidgetEvent* widgetEvent = AsWidgetEvent();
+ return widgetEvent->mFlags.mIsBeingDispatched ||
+ widgetEvent->mFlags.mDispatchedAtLeastOnce;
+ }
+
+ const WidgetEvent* AsWidgetEvent() const {
+ return const_cast<AnimationEventInfo*>(this)->AsWidgetEvent();
+ }
+#endif
+
+ WidgetEvent* AsWidgetEvent() {
+ if (mEvent.is<InternalTransitionEvent>()) {
+ return &mEvent.as<InternalTransitionEvent>();
+ }
+ if (mEvent.is<InternalAnimationEvent>()) {
+ return &mEvent.as<InternalAnimationEvent>();
+ }
+ if (mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>()) {
+ return mEvent.as<RefPtr<dom::AnimationPlaybackEvent>>()->WidgetEventPtr();
+ }
+
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unexpected event type");
+ return nullptr;
+ }
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch(nsPresContext* aPresContext) {
+ RefPtr<dom::EventTarget> target = mTarget;
+ if (mEvent.is<RefPtr<dom::AnimationPlaybackEvent>>()) {
+ auto playbackEvent = mEvent.as<RefPtr<dom::AnimationPlaybackEvent>>();
+ EventDispatcher::DispatchDOMEvent(target, nullptr /* WidgetEvent */,
+ playbackEvent, aPresContext,
+ nullptr /* nsEventStatus */);
+ return;
+ }
+
+ MOZ_ASSERT(mEvent.is<InternalTransitionEvent>() ||
+ mEvent.is<InternalAnimationEvent>());
+
+ if (mEvent.is<InternalTransitionEvent>() && target->IsNode()) {
+ nsPIDOMWindowInner* inner =
+ target->AsNode()->OwnerDoc()->GetInnerWindow();
+ if (inner && !inner->HasTransitionEventListeners()) {
+ MOZ_ASSERT(AsWidgetEvent()->mMessage == eTransitionStart ||
+ AsWidgetEvent()->mMessage == eTransitionRun ||
+ AsWidgetEvent()->mMessage == eTransitionEnd ||
+ AsWidgetEvent()->mMessage == eTransitionCancel);
+ return;
+ }
+ }
+
+ EventDispatcher::Dispatch(target, aPresContext, AsWidgetEvent());
+ }
+};
+
+class AnimationEventDispatcher final {
+ public:
+ explicit AnimationEventDispatcher(nsPresContext* aPresContext)
+ : mPresContext(aPresContext), mIsSorted(true), mIsObserving(false) {}
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEventDispatcher)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(AnimationEventDispatcher)
+
+ void Disconnect();
+
+ void QueueEvent(AnimationEventInfo&& aEvent);
+ void QueueEvents(nsTArray<AnimationEventInfo>&& aEvents);
+
+ // This will call SortEvents automatically if it has not already been
+ // called.
+ void DispatchEvents() {
+ mIsObserving = false;
+ if (!mPresContext || mPendingEvents.IsEmpty()) {
+ return;
+ }
+
+ SortEvents();
+
+ EventArray events = std::move(mPendingEvents);
+ // mIsSorted will be set to true by SortEvents above, and we leave it
+ // that way since mPendingEvents is now empty
+ for (AnimationEventInfo& info : events) {
+ MOZ_ASSERT(!info.IsStale(), "The event shouldn't be stale");
+ info.Dispatch(mPresContext);
+
+ // Bail out if our mPresContext was nullified due to destroying the pres
+ // context.
+ if (!mPresContext) {
+ break;
+ }
+ }
+ }
+
+ void ClearEventQueue() {
+ mPendingEvents.Clear();
+ mIsSorted = true;
+ }
+ bool HasQueuedEvents() const { return !mPendingEvents.IsEmpty(); }
+
+ private:
+#ifndef DEBUG
+ ~AnimationEventDispatcher() = default;
+#else
+ ~AnimationEventDispatcher() {
+ MOZ_ASSERT(!mIsObserving,
+ "AnimationEventDispatcher should have disassociated from "
+ "nsRefreshDriver");
+ }
+#endif
+
+ class AnimationEventInfoLessThan {
+ public:
+ bool operator()(const AnimationEventInfo& a,
+ const AnimationEventInfo& b) const {
+ if (a.mScheduledEventTimeStamp != b.mScheduledEventTimeStamp) {
+ // Null timestamps sort first
+ if (a.mScheduledEventTimeStamp.IsNull() ||
+ b.mScheduledEventTimeStamp.IsNull()) {
+ return a.mScheduledEventTimeStamp.IsNull();
+ } else {
+ return a.mScheduledEventTimeStamp < b.mScheduledEventTimeStamp;
+ }
+ }
+
+ // Events in the Web Animations spec are prior to CSS events.
+ if (a.IsWebAnimationEvent() != b.IsWebAnimationEvent()) {
+ return a.IsWebAnimationEvent();
+ }
+
+ AnimationPtrComparator<RefPtr<dom::Animation>> comparator;
+ return comparator.LessThan(a.mAnimation, b.mAnimation);
+ }
+ };
+
+ // Sort all pending CSS animation/transition events by scheduled event time
+ // and composite order.
+ // https://drafts.csswg.org/web-animations/#update-animations-and-send-events
+ void SortEvents() {
+ if (mIsSorted) {
+ return;
+ }
+
+ for (auto& pending : mPendingEvents) {
+ pending.mAnimation->CachedChildIndexRef().reset();
+ }
+
+ // FIXME: Replace with mPendingEvents.StableSort when bug 1147091 is
+ // fixed.
+ std::stable_sort(mPendingEvents.begin(), mPendingEvents.end(),
+ AnimationEventInfoLessThan());
+ mIsSorted = true;
+ }
+ void ScheduleDispatch();
+
+ nsPresContext* mPresContext;
+ typedef nsTArray<AnimationEventInfo> EventArray;
+ EventArray mPendingEvents;
+ bool mIsSorted;
+ bool mIsObserving;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AnimationEventDispatcher_h
diff --git a/dom/animation/AnimationPerformanceWarning.cpp b/dom/animation/AnimationPerformanceWarning.cpp
new file mode 100644
index 0000000000..e57beff51f
--- /dev/null
+++ b/dom/animation/AnimationPerformanceWarning.cpp
@@ -0,0 +1,81 @@
+/* -*- 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 "AnimationPerformanceWarning.h"
+
+#include "nsContentUtils.h"
+
+namespace mozilla {
+
+template <uint32_t N>
+nsresult AnimationPerformanceWarning::ToLocalizedStringWithIntParams(
+ const char* aKey, nsAString& aLocalizedString) const {
+ AutoTArray<nsString, N> strings;
+
+ MOZ_DIAGNOSTIC_ASSERT(mParams->Length() == N);
+ for (size_t i = 0, n = mParams->Length(); i < n; i++) {
+ strings.AppendElement()->AppendInt((*mParams)[i]);
+ }
+
+ return nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eLAYOUT_PROPERTIES, aKey, strings, aLocalizedString);
+}
+
+bool AnimationPerformanceWarning::ToLocalizedString(
+ nsAString& aLocalizedString) const {
+ const char* key = nullptr;
+
+ switch (mType) {
+ case Type::ContentTooLarge:
+ MOZ_ASSERT(mParams && mParams->Length() == 6,
+ "Parameter's length should be 6 for ContentTooLarge2");
+
+ return NS_SUCCEEDED(ToLocalizedStringWithIntParams<6>(
+ "CompositorAnimationWarningContentTooLarge2", aLocalizedString));
+ case Type::ContentTooLargeArea:
+ MOZ_ASSERT(mParams && mParams->Length() == 2,
+ "Parameter's length should be 2 for ContentTooLargeArea");
+
+ return NS_SUCCEEDED(ToLocalizedStringWithIntParams<2>(
+ "CompositorAnimationWarningContentTooLargeArea", aLocalizedString));
+ case Type::TransformBackfaceVisibilityHidden:
+ key = "CompositorAnimationWarningTransformBackfaceVisibilityHidden";
+ break;
+ case Type::TransformSVG:
+ key = "CompositorAnimationWarningTransformSVG";
+ break;
+ case Type::TransformWithGeometricProperties:
+ key = "CompositorAnimationWarningTransformWithGeometricProperties";
+ break;
+ case Type::TransformWithSyncGeometricAnimations:
+ key = "CompositorAnimationWarningTransformWithSyncGeometricAnimations";
+ break;
+ case Type::TransformFrameInactive:
+ key = "CompositorAnimationWarningTransformFrameInactive";
+ break;
+ case Type::TransformIsBlockedByImportantRules:
+ key = "CompositorAnimationWarningTransformIsBlockedByImportantRules";
+ break;
+ case Type::OpacityFrameInactive:
+ key = "CompositorAnimationWarningOpacityFrameInactive";
+ break;
+ case Type::HasRenderingObserver:
+ key = "CompositorAnimationWarningHasRenderingObserver";
+ break;
+ case Type::HasCurrentColor:
+ key = "CompositorAnimationWarningHasCurrentColor";
+ break;
+ case Type::None:
+ MOZ_ASSERT_UNREACHABLE("Uninitialized type shouldn't be used");
+ return false;
+ }
+
+ nsresult rv = nsContentUtils::GetLocalizedString(
+ nsContentUtils::eLAYOUT_PROPERTIES, key, aLocalizedString);
+ return NS_SUCCEEDED(rv);
+}
+
+} // namespace mozilla
diff --git a/dom/animation/AnimationPerformanceWarning.h b/dom/animation/AnimationPerformanceWarning.h
new file mode 100644
index 0000000000..e59eade9e6
--- /dev/null
+++ b/dom/animation/AnimationPerformanceWarning.h
@@ -0,0 +1,81 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AnimationPerformanceWarning_h
+#define mozilla_dom_AnimationPerformanceWarning_h
+
+#include <initializer_list>
+
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+// Represents the reason why we can't run the CSS property on the compositor.
+struct AnimationPerformanceWarning {
+ enum class Type : uint8_t {
+ None,
+ ContentTooLarge,
+ ContentTooLargeArea,
+ TransformBackfaceVisibilityHidden,
+ TransformSVG,
+ TransformWithGeometricProperties,
+ TransformWithSyncGeometricAnimations,
+ TransformFrameInactive,
+ TransformIsBlockedByImportantRules,
+ OpacityFrameInactive,
+ HasRenderingObserver,
+ HasCurrentColor,
+ };
+
+ explicit AnimationPerformanceWarning(Type aType) : mType(aType) {
+ MOZ_ASSERT(mType != Type::None);
+ }
+
+ AnimationPerformanceWarning(Type aType,
+ std::initializer_list<int32_t> aParams)
+ : mType(aType) {
+ MOZ_ASSERT(mType != Type::None);
+ // FIXME: Once std::initializer_list::size() become a constexpr function,
+ // we should use static_assert here.
+ MOZ_ASSERT(aParams.size() <= kMaxParamsForLocalization,
+ "The length of parameters should be less than "
+ "kMaxParamsForLocalization");
+ mParams.emplace(aParams);
+ }
+
+ // Maximum number of parameters passed to
+ // nsContentUtils::FormatLocalizedString to localize warning messages.
+ //
+ // NOTE: This constexpr can't be forward declared, so if you want to use
+ // this variable, please include this header file directly.
+ // This value is the same as the limit of nsStringBundle::FormatString.
+ // See the implementation of nsStringBundle::FormatString.
+ static constexpr uint8_t kMaxParamsForLocalization = 10;
+
+ // Indicates why this property could not be animated on the compositor.
+ Type mType;
+
+ // Optional parameters that may be used for localization.
+ Maybe<CopyableTArray<int32_t>> mParams;
+
+ bool ToLocalizedString(nsAString& aLocalizedString) const;
+ template <uint32_t N>
+ nsresult ToLocalizedStringWithIntParams(const char* aKey,
+ nsAString& aLocalizedString) const;
+
+ bool operator==(const AnimationPerformanceWarning& aOther) const {
+ return mType == aOther.mType && mParams == aOther.mParams;
+ }
+ bool operator!=(const AnimationPerformanceWarning& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationPerformanceWarning_h
diff --git a/dom/animation/AnimationPropertySegment.h b/dom/animation/AnimationPropertySegment.h
new file mode 100644
index 0000000000..fb4ea7ee26
--- /dev/null
+++ b/dom/animation/AnimationPropertySegment.h
@@ -0,0 +1,55 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AnimationPropertySegment_h
+#define mozilla_dom_AnimationPropertySegment_h
+
+#include "mozilla/ServoStyleConsts.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StyleAnimationValue.h" // For AnimationValue
+#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For CompositeOperation
+
+namespace mozilla {
+
+struct AnimationPropertySegment {
+ float mFromKey, mToKey;
+ // NOTE: In the case that no keyframe for 0 or 1 offset is specified
+ // the unit of mFromValue or mToValue is eUnit_Null.
+ AnimationValue mFromValue, mToValue;
+
+ Maybe<StyleComputedTimingFunction> mTimingFunction;
+ dom::CompositeOperation mFromComposite = dom::CompositeOperation::Replace;
+ dom::CompositeOperation mToComposite = dom::CompositeOperation::Replace;
+
+ bool HasReplaceableValues() const {
+ return HasReplaceableFromValue() && HasReplaceableToValue();
+ }
+
+ bool HasReplaceableFromValue() const {
+ return !mFromValue.IsNull() &&
+ mFromComposite == dom::CompositeOperation::Replace;
+ }
+
+ bool HasReplaceableToValue() const {
+ return !mToValue.IsNull() &&
+ mToComposite == dom::CompositeOperation::Replace;
+ }
+
+ bool operator==(const AnimationPropertySegment& aOther) const {
+ return mFromKey == aOther.mFromKey && mToKey == aOther.mToKey &&
+ mFromValue == aOther.mFromValue && mToValue == aOther.mToValue &&
+ mTimingFunction == aOther.mTimingFunction &&
+ mFromComposite == aOther.mFromComposite &&
+ mToComposite == aOther.mToComposite;
+ }
+ bool operator!=(const AnimationPropertySegment& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_AnimationPropertySegment_h
diff --git a/dom/animation/AnimationTarget.h b/dom/animation/AnimationTarget.h
new file mode 100644
index 0000000000..3a0dacb9aa
--- /dev/null
+++ b/dom/animation/AnimationTarget.h
@@ -0,0 +1,108 @@
+/* -*- 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/. */
+
+#ifndef mozilla_AnimationTarget_h
+#define mozilla_AnimationTarget_h
+
+#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
+#include "mozilla/HashFunctions.h" // For HashNumber, AddToHash
+#include "mozilla/HashTable.h" // For DefaultHasher, PointerHasher
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "nsCSSPseudoElements.h"
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+struct OwningAnimationTarget {
+ OwningAnimationTarget() = default;
+
+ OwningAnimationTarget(dom::Element* aElement, PseudoStyleType aType)
+ : mElement(aElement), mPseudoType(aType) {}
+
+ explicit OwningAnimationTarget(dom::Element* aElement) : mElement(aElement) {}
+
+ bool operator==(const OwningAnimationTarget& aOther) const {
+ return mElement == aOther.mElement && mPseudoType == aOther.mPseudoType;
+ }
+
+ explicit operator bool() const { return !!mElement; }
+
+ // mElement represents the parent element of a pseudo-element, not the
+ // generated content element.
+ RefPtr<dom::Element> mElement;
+ PseudoStyleType mPseudoType = PseudoStyleType::NotPseudo;
+};
+
+struct NonOwningAnimationTarget {
+ NonOwningAnimationTarget() = default;
+
+ NonOwningAnimationTarget(dom::Element* aElement, PseudoStyleType aType)
+ : mElement(aElement), mPseudoType(aType) {}
+
+ explicit NonOwningAnimationTarget(const OwningAnimationTarget& aOther)
+ : mElement(aOther.mElement), mPseudoType(aOther.mPseudoType) {}
+
+ bool operator==(const NonOwningAnimationTarget& aOther) const {
+ return mElement == aOther.mElement && mPseudoType == aOther.mPseudoType;
+ }
+
+ NonOwningAnimationTarget& operator=(const OwningAnimationTarget& aOther) {
+ mElement = aOther.mElement;
+ mPseudoType = aOther.mPseudoType;
+ return *this;
+ }
+
+ explicit operator bool() const { return !!mElement; }
+
+ // mElement represents the parent element of a pseudo-element, not the
+ // generated content element.
+ dom::Element* MOZ_NON_OWNING_REF mElement = nullptr;
+ PseudoStyleType mPseudoType = PseudoStyleType::NotPseudo;
+};
+
+// Helper functions for cycle-collecting Maybe<OwningAnimationTarget>
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback,
+ Maybe<OwningAnimationTarget>& aTarget, const char* aName,
+ uint32_t aFlags = 0) {
+ if (aTarget) {
+ ImplCycleCollectionTraverse(aCallback, aTarget->mElement, aName, aFlags);
+ }
+}
+
+inline void ImplCycleCollectionUnlink(Maybe<OwningAnimationTarget>& aTarget) {
+ if (aTarget) {
+ ImplCycleCollectionUnlink(aTarget->mElement);
+ }
+}
+
+// A DefaultHasher specialization for OwningAnimationTarget.
+template <>
+struct DefaultHasher<OwningAnimationTarget> {
+ using Key = OwningAnimationTarget;
+ using Lookup = OwningAnimationTarget;
+ using PtrHasher = PointerHasher<dom::Element*>;
+
+ static HashNumber hash(const Lookup& aLookup) {
+ return AddToHash(PtrHasher::hash(aLookup.mElement.get()),
+ static_cast<uint8_t>(aLookup.mPseudoType));
+ }
+
+ static bool match(const Key& aKey, const Lookup& aLookup) {
+ return PtrHasher::match(aKey.mElement.get(), aLookup.mElement.get()) &&
+ aKey.mPseudoType == aLookup.mPseudoType;
+ }
+
+ static void rekey(Key& aKey, Key&& aNewKey) { aKey = std::move(aNewKey); }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AnimationTarget_h
diff --git a/dom/animation/AnimationTimeline.cpp b/dom/animation/AnimationTimeline.cpp
new file mode 100644
index 0000000000..8be0554683
--- /dev/null
+++ b/dom/animation/AnimationTimeline.cpp
@@ -0,0 +1,116 @@
+/* -*- 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 "AnimationTimeline.h"
+#include "mozilla/AnimationComparator.h"
+#include "mozilla/dom/Animation.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationTimeline)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationTimeline)
+ tmp->mAnimationOrder.clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mAnimations)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationTimeline)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mAnimations)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationTimeline)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationTimeline)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationTimeline)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+AnimationTimeline::AnimationTimeline(nsIGlobalObject* aWindow,
+ RTPCallerType aRTPCallerType)
+ : mWindow(aWindow), mRTPCallerType(aRTPCallerType) {
+ MOZ_ASSERT(mWindow);
+}
+
+AnimationTimeline::~AnimationTimeline() { mAnimationOrder.clear(); }
+
+bool AnimationTimeline::Tick() {
+ bool needsTicks = false;
+
+ nsTArray<Animation*> animationsToRemove;
+
+ for (Animation* animation = mAnimationOrder.getFirst(); animation;
+ animation =
+ static_cast<LinkedListElement<Animation>*>(animation)->getNext()) {
+ MOZ_ASSERT(mAnimations.Contains(animation),
+ "The sampling order list should be a subset of the hashset");
+ MOZ_ASSERT(!animation->IsHiddenByContentVisibility(),
+ "The sampling order list should not contain any animations "
+ "that are hidden by content-visibility");
+
+ // Skip any animations that are longer need associated with this timeline.
+ if (animation->GetTimeline() != this) {
+ // If animation has some other timeline, it better not be also in the
+ // animation list of this timeline object!
+ MOZ_ASSERT(!animation->GetTimeline());
+ animationsToRemove.AppendElement(animation);
+ continue;
+ }
+
+ needsTicks |= animation->NeedsTicks();
+ // Even if |animation| doesn't need future ticks, we should still
+ // Tick it this time around since it might just need a one-off tick in
+ // order to dispatch events.
+ animation->Tick();
+
+ if (!animation->NeedsTicks()) {
+ animationsToRemove.AppendElement(animation);
+ }
+ }
+
+ for (Animation* animation : animationsToRemove) {
+ RemoveAnimation(animation);
+ }
+
+ return needsTicks;
+}
+
+void AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation) {
+ if (mAnimations.EnsureInserted(&aAnimation)) {
+ if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) {
+ aAnimation.GetTimeline()->RemoveAnimation(&aAnimation);
+ }
+ if (!aAnimation.IsHiddenByContentVisibility()) {
+ mAnimationOrder.insertBack(&aAnimation);
+ }
+ }
+}
+
+void AnimationTimeline::RemoveAnimation(Animation* aAnimation) {
+ MOZ_ASSERT(!aAnimation->GetTimeline() || aAnimation->GetTimeline() == this);
+ if (static_cast<LinkedListElement<Animation>*>(aAnimation)->isInList()) {
+ MOZ_ASSERT(mAnimations.Contains(aAnimation),
+ "The sampling order list should be a subset of the hashset");
+ static_cast<LinkedListElement<Animation>*>(aAnimation)->remove();
+ }
+ mAnimations.Remove(aAnimation);
+}
+
+void AnimationTimeline::NotifyAnimationContentVisibilityChanged(
+ Animation* aAnimation, bool aIsVisible) {
+ bool inList =
+ static_cast<LinkedListElement<Animation>*>(aAnimation)->isInList();
+ MOZ_ASSERT(!inList || mAnimations.Contains(aAnimation),
+ "The sampling order list should be a subset of the hashset");
+ if (aIsVisible && !inList && mAnimations.Contains(aAnimation)) {
+ mAnimationOrder.insertBack(aAnimation);
+ } else if (!aIsVisible && inList) {
+ static_cast<LinkedListElement<Animation>*>(aAnimation)->remove();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/AnimationTimeline.h b/dom/animation/AnimationTimeline.h
new file mode 100644
index 0000000000..715cb28969
--- /dev/null
+++ b/dom/animation/AnimationTimeline.h
@@ -0,0 +1,143 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AnimationTimeline_h
+#define mozilla_dom_AnimationTimeline_h
+
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "js/TypeDecls.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/Attributes.h"
+#include "nsHashKeys.h"
+#include "nsIGlobalObject.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom {
+
+class Animation;
+class Document;
+class ScrollTimeline;
+
+class AnimationTimeline : public nsISupports, public nsWrapperCache {
+ public:
+ explicit AnimationTimeline(nsIGlobalObject* aWindow,
+ RTPCallerType aRTPCallerType);
+
+ protected:
+ virtual ~AnimationTimeline();
+
+ // Tick animations and may remove them from the list if we don't need to
+ // tick it. Return true if any animations need to be ticked.
+ bool Tick();
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationTimeline)
+
+ nsIGlobalObject* GetParentObject() const { return mWindow; }
+
+ // AnimationTimeline methods
+ virtual Nullable<TimeDuration> GetCurrentTimeAsDuration() const = 0;
+
+ // Wrapper functions for AnimationTimeline DOM methods when called from
+ // script.
+ Nullable<double> GetCurrentTimeAsDouble() const {
+ return AnimationUtils::TimeDurationToDouble(GetCurrentTimeAsDuration(),
+ mRTPCallerType);
+ }
+
+ TimeStamp GetCurrentTimeAsTimeStamp() const {
+ Nullable<TimeDuration> currentTime = GetCurrentTimeAsDuration();
+ return !currentTime.IsNull() ? ToTimeStamp(currentTime.Value())
+ : TimeStamp();
+ }
+
+ /**
+ * Returns true if the times returned by GetCurrentTimeAsDuration() are
+ * convertible to and from wallclock-based TimeStamp (e.g. from
+ * TimeStamp::Now()) values using ToTimelineTime() and ToTimeStamp().
+ *
+ * Typically this is true, but it will be false in the case when this
+ * timeline has no refresh driver or is tied to a refresh driver under test
+ * control.
+ */
+ virtual bool TracksWallclockTime() const = 0;
+
+ /**
+ * Converts a TimeStamp to the equivalent value in timeline time.
+ * Note that when TracksWallclockTime() is false, there is no correspondence
+ * between timeline time and wallclock time. In such a case, passing a
+ * timestamp from TimeStamp::Now() to this method will not return a
+ * meaningful result.
+ */
+ virtual Nullable<TimeDuration> ToTimelineTime(
+ const TimeStamp& aTimeStamp) const = 0;
+
+ virtual TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const = 0;
+
+ /**
+ * Inform this timeline that |aAnimation| which is or was observing the
+ * timeline, has been updated. This serves as both the means to associate
+ * AND disassociate animations with a timeline. The timeline itself will
+ * determine if it needs to begin, continue or stop tracking this animation.
+ */
+ virtual void NotifyAnimationUpdated(Animation& aAnimation);
+
+ /**
+ * Returns true if any CSS animations, CSS transitions or Web animations are
+ * currently associated with this timeline. As soon as an animation is
+ * applied to an element it is associated with the timeline even if it has a
+ * delayed start, so this includes animations that may not be active for some
+ * time.
+ */
+ bool HasAnimations() const { return !mAnimations.IsEmpty(); }
+
+ virtual void RemoveAnimation(Animation* aAnimation);
+ virtual void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
+ bool aIsVisible);
+
+ virtual Document* GetDocument() const = 0;
+
+ virtual bool IsMonotonicallyIncreasing() const = 0;
+
+ RTPCallerType GetRTPCallerType() const { return mRTPCallerType; }
+
+ virtual bool IsScrollTimeline() const { return false; }
+ virtual const ScrollTimeline* AsScrollTimeline() const { return nullptr; }
+ virtual bool IsViewTimeline() const { return false; }
+
+ // For a monotonic timeline, there is no upper bound on current time, and
+ // timeline duration is unresolved. For a non-monotonic (e.g. scroll)
+ // timeline, the duration has a fixed upper bound.
+ //
+ // https://drafts.csswg.org/web-animations-2/#timeline-duration
+ virtual Nullable<TimeDuration> TimelineDuration() const { return nullptr; }
+
+ protected:
+ nsCOMPtr<nsIGlobalObject> mWindow;
+
+ // Animations observing this timeline
+ //
+ // We store them in (a) a hashset for quick lookup, and (b) a LinkedList
+ // to maintain a fixed sampling order. Animations that are hidden by
+ // `content-visibility` are not sampled and will only be in the hashset.
+ // The LinkedList should always be a subset of the hashset.
+ //
+ // The hashset keeps a strong reference to each animation since
+ // dealing with addref/release with LinkedList is difficult.
+ typedef nsTHashSet<nsRefPtrHashKey<dom::Animation>> AnimationSet;
+ AnimationSet mAnimations;
+ LinkedList<dom::Animation> mAnimationOrder;
+
+ // Whether the Timeline is System, ResistFingerprinting, or neither
+ enum RTPCallerType mRTPCallerType;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AnimationTimeline_h
diff --git a/dom/animation/AnimationUtils.cpp b/dom/animation/AnimationUtils.cpp
new file mode 100644
index 0000000000..8d00d9ebcf
--- /dev/null
+++ b/dom/animation/AnimationUtils.cpp
@@ -0,0 +1,145 @@
+/* -*- 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 "AnimationUtils.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/EffectSet.h"
+#include "nsDebug.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsString.h"
+#include "xpcpublic.h" // For xpc::NativeGlobal
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+/* static */
+void AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage,
+ const nsIContent* aContent) {
+ if (aContent) {
+ aMessage.AppendLiteral(" [");
+ aMessage.Append(nsAtomCString(aContent->NodeInfo()->NameAtom()));
+
+ nsAtom* id = aContent->GetID();
+ if (id) {
+ aMessage.AppendLiteral(" with id '");
+ aMessage.Append(nsAtomCString(aContent->GetID()));
+ aMessage.Append('\'');
+ }
+ aMessage.Append(']');
+ }
+ aMessage.Append('\n');
+ printf_stderr("%s", aMessage.get());
+}
+
+/* static */
+Document* AnimationUtils::GetCurrentRealmDocument(JSContext* aCx) {
+ nsGlobalWindowInner* win = xpc::CurrentWindowOrNull(aCx);
+ if (!win) {
+ return nullptr;
+ }
+ return win->GetDoc();
+}
+
+/* static */
+Document* AnimationUtils::GetDocumentFromGlobal(JSObject* aGlobalObject) {
+ nsGlobalWindowInner* win = xpc::WindowOrNull(aGlobalObject);
+ if (!win) {
+ return nullptr;
+ }
+ return win->GetDoc();
+}
+
+/* static */
+bool AnimationUtils::FrameHasAnimatedScale(const nsIFrame* aFrame) {
+ EffectSet* effectSet = EffectSet::GetForFrame(
+ aFrame, nsCSSPropertyIDSet::TransformLikeProperties());
+ if (!effectSet) {
+ return false;
+ }
+
+ for (const dom::KeyframeEffect* effect : *effectSet) {
+ if (effect->ContainsAnimatedScale(aFrame)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* static */
+bool AnimationUtils::HasCurrentTransitions(const Element* aElement,
+ PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(aElement);
+
+ EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType);
+ if (!effectSet) {
+ return false;
+ }
+
+ for (const dom::KeyframeEffect* effect : *effectSet) {
+ // If |effect| is current, it must have an associated Animation
+ // so we don't need to null-check the result of GetAnimation().
+ if (effect->IsCurrent() && effect->GetAnimation()->AsCSSTransition()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*static*/ Element* AnimationUtils::GetElementForRestyle(
+ Element* aElement, PseudoStyleType aPseudoType) {
+ if (aPseudoType == PseudoStyleType::NotPseudo) {
+ return aElement;
+ }
+
+ if (aPseudoType == PseudoStyleType::before) {
+ return nsLayoutUtils::GetBeforePseudo(aElement);
+ }
+
+ if (aPseudoType == PseudoStyleType::after) {
+ return nsLayoutUtils::GetAfterPseudo(aElement);
+ }
+
+ if (aPseudoType == PseudoStyleType::marker) {
+ return nsLayoutUtils::GetMarkerPseudo(aElement);
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "Should not try to get the element to restyle for a pseudo other that "
+ ":before, :after or ::marker");
+ return nullptr;
+}
+
+/*static*/ std::pair<const Element*, PseudoStyleType>
+AnimationUtils::GetElementPseudoPair(const Element* aElementOrPseudo) {
+ MOZ_ASSERT(aElementOrPseudo);
+
+ if (aElementOrPseudo->IsGeneratedContentContainerForBefore()) {
+ return {aElementOrPseudo->GetParent()->AsElement(),
+ PseudoStyleType::before};
+ }
+
+ if (aElementOrPseudo->IsGeneratedContentContainerForAfter()) {
+ return {aElementOrPseudo->GetParent()->AsElement(), PseudoStyleType::after};
+ }
+
+ if (aElementOrPseudo->IsGeneratedContentContainerForMarker()) {
+ return {aElementOrPseudo->GetParent()->AsElement(),
+ PseudoStyleType::marker};
+ }
+
+ return {aElementOrPseudo, PseudoStyleType::NotPseudo};
+}
+
+} // namespace mozilla
diff --git a/dom/animation/AnimationUtils.h b/dom/animation/AnimationUtils.h
new file mode 100644
index 0000000000..0d8c53afa0
--- /dev/null
+++ b/dom/animation/AnimationUtils.h
@@ -0,0 +1,134 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AnimationUtils_h
+#define mozilla_dom_AnimationUtils_h
+
+#include "mozilla/PseudoStyleType.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsRFPService.h"
+#include "nsStringFwd.h"
+
+class nsIContent;
+class nsIFrame;
+struct JSContext;
+
+namespace mozilla {
+
+class EffectSet;
+
+namespace dom {
+class Document;
+class Element;
+} // namespace dom
+
+class AnimationUtils {
+ public:
+ using Document = dom::Document;
+
+ static dom::Nullable<double> TimeDurationToDouble(
+ const dom::Nullable<TimeDuration>& aTime, RTPCallerType aRTPCallerType) {
+ dom::Nullable<double> result;
+
+ if (!aTime.IsNull()) {
+ // 0 is an inappropriate mixin for this this area; however CSS Animations
+ // needs to have it's Time Reduction Logic refactored, so it's currently
+ // only clamping for RFP mode. RFP mode gives a much lower time precision,
+ // so we accept the security leak here for now
+ result.SetValue(nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly(
+ aTime.Value().ToMilliseconds(), 0, aRTPCallerType));
+ }
+
+ return result;
+ }
+
+ static dom::Nullable<TimeDuration> DoubleToTimeDuration(
+ const dom::Nullable<double>& aTime) {
+ dom::Nullable<TimeDuration> result;
+
+ if (!aTime.IsNull()) {
+ result.SetValue(TimeDuration::FromMilliseconds(aTime.Value()));
+ }
+
+ return result;
+ }
+
+ static void LogAsyncAnimationFailure(nsCString& aMessage,
+ const nsIContent* aContent = nullptr);
+
+ /**
+ * Get the document from the JS context to use when parsing CSS properties.
+ */
+ static Document* GetCurrentRealmDocument(JSContext* aCx);
+
+ /**
+ * Get the document from the global object, or nullptr if the document has
+ * no window, to use when constructing DOM object without entering the
+ * target window's compartment (see KeyframeEffect constructor).
+ */
+ static Document* GetDocumentFromGlobal(JSObject* aGlobalObject);
+
+ /**
+ * Returns true if the given frame has an animated scale.
+ */
+ static bool FrameHasAnimatedScale(const nsIFrame* aFrame);
+
+ /**
+ * Returns true if the given (pseudo-)element has any transitions that are
+ * current (playing or waiting to play) or in effect (e.g. filling forwards).
+ */
+ static bool HasCurrentTransitions(const dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ /**
+ * Returns true if this pseudo style type is supported by animations.
+ * Note: This doesn't include PseudoStyleType::NotPseudo.
+ */
+ static bool IsSupportedPseudoForAnimations(PseudoStyleType aType) {
+ // FIXME: Bug 1615469: Support first-line and first-letter for Animation.
+ return aType == PseudoStyleType::before ||
+ aType == PseudoStyleType::after || aType == PseudoStyleType::marker;
+ }
+
+ /**
+ * Returns true if the difference between |aFirst| and |aSecond| is within
+ * the animation time tolerance (i.e. 1 microsecond).
+ */
+ static bool IsWithinAnimationTimeTolerance(const TimeDuration& aFirst,
+ const TimeDuration& aSecond) {
+ if (aFirst == TimeDuration::Forever() ||
+ aSecond == TimeDuration::Forever()) {
+ return aFirst == aSecond;
+ }
+
+ TimeDuration diff = aFirst >= aSecond ? aFirst - aSecond : aSecond - aFirst;
+ return diff <= TimeDuration::FromMicroseconds(1);
+ }
+
+ // Returns the target element for restyling.
+ //
+ // If |aPseudoType| is ::after, ::before or ::marker, returns the generated
+ // content element of which |aElement| is the parent. If |aPseudoType| is any
+ // other pseudo type (other than PseudoStyleType::NotPseudo) returns nullptr.
+ // Otherwise, returns |aElement|.
+ static dom::Element* GetElementForRestyle(dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ // Returns the pair of |Element, PseudoStyleType| from an element which could
+ // be an element or a pseudo element (i.e. an element used for restyling and
+ // DOM tree).
+ //
+ // Animation module usually uses a pair of (Element*, PseudoStyleType) to
+ // represent the animation target, and the |Element| in the pair is the
+ // generated content container if it's a pseudo element.
+ static std::pair<const dom::Element*, PseudoStyleType> GetElementPseudoPair(
+ const dom::Element* aElementOrPseudo);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/animation/CSSAnimation.cpp b/dom/animation/CSSAnimation.cpp
new file mode 100644
index 0000000000..ed48d24806
--- /dev/null
+++ b/dom/animation/CSSAnimation.cpp
@@ -0,0 +1,376 @@
+/* -*- 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 "CSSAnimation.h"
+
+#include "mozilla/AnimationEventDispatcher.h"
+#include "mozilla/dom/CSSAnimationBinding.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/TimeStamp.h"
+#include "nsPresContext.h"
+
+namespace mozilla::dom {
+
+using AnimationPhase = ComputedTiming::AnimationPhase;
+
+JSObject* CSSAnimation::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
+ Animation::SetEffect(aEffect);
+
+ AddOverriddenProperties(CSSAnimationProperties::Effect);
+}
+
+void CSSAnimation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) {
+ // Note that we always compare with the paused state since for the purposes
+ // of determining if play control is being overridden or not, we want to
+ // treat the finished state as running.
+ bool wasPaused = PlayState() == AnimationPlayState::Paused;
+
+ Animation::SetStartTimeAsDouble(aStartTime);
+
+ bool isPaused = PlayState() == AnimationPlayState::Paused;
+
+ if (wasPaused != isPaused) {
+ AddOverriddenProperties(CSSAnimationProperties::PlayState);
+ }
+}
+
+mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
+ FlushUnanimatedStyle();
+ return Animation::GetReady(aRv);
+}
+
+void CSSAnimation::Reverse(ErrorResult& aRv) {
+ // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in
+ // the paused state.
+ bool wasPaused = PlayState() == AnimationPlayState::Paused;
+
+ Animation::Reverse(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ bool isPaused = PlayState() == AnimationPlayState::Paused;
+
+ if (wasPaused != isPaused) {
+ AddOverriddenProperties(CSSAnimationProperties::PlayState);
+ }
+}
+
+AnimationPlayState CSSAnimation::PlayStateFromJS() const {
+ // Flush style to ensure that any properties controlling animation state
+ // (e.g. animation-play-state) are fully updated.
+ FlushUnanimatedStyle();
+ return Animation::PlayStateFromJS();
+}
+
+bool CSSAnimation::PendingFromJS() const {
+ // Flush style since, for example, if the animation-play-state was just
+ // changed its possible we should now be pending.
+ FlushUnanimatedStyle();
+ return Animation::PendingFromJS();
+}
+
+void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
+ // Note that flushing style below might trigger calls to
+ // PlayFromStyle()/PauseFromStyle() on this object.
+ FlushUnanimatedStyle();
+ Animation::PlayFromJS(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ AddOverriddenProperties(CSSAnimationProperties::PlayState);
+}
+
+void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
+ Animation::PauseFromJS(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ AddOverriddenProperties(CSSAnimationProperties::PlayState);
+}
+
+void CSSAnimation::PlayFromStyle() {
+ ErrorResult rv;
+ Animation::Play(rv, Animation::LimitBehavior::Continue);
+ // play() should not throw when LimitBehavior is Continue
+ MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
+}
+
+void CSSAnimation::PauseFromStyle() {
+ ErrorResult rv;
+ Animation::Pause(rv);
+ // pause() should only throw when *all* of the following conditions are true:
+ // - we are in the idle state, and
+ // - we have a negative playback rate, and
+ // - we have an infinitely repeating animation
+ // The first two conditions will never happen under regular style processing
+ // but could happen if an author made modifications to the Animation object
+ // and then updated animation-play-state. It's an unusual case and there's
+ // no obvious way to pass on the exception information so we just silently
+ // fail for now.
+ if (rv.Failed()) {
+ NS_WARNING("Unexpected exception pausing animation - silently failing");
+ }
+}
+
+void CSSAnimation::Tick() {
+ Animation::Tick();
+ QueueEvents();
+}
+
+bool CSSAnimation::HasLowerCompositeOrderThan(
+ const CSSAnimation& aOther) const {
+ MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
+ "Should only be called for CSS animations that are sorted "
+ "as CSS animations (i.e. tied to CSS markup)");
+
+ // 0. Object-equality case
+ if (&aOther == this) {
+ return false;
+ }
+
+ // 1. Sort by document order
+ if (!mOwningElement.Equals(aOther.mOwningElement)) {
+ return mOwningElement.LessThan(
+ const_cast<CSSAnimation*>(this)->CachedChildIndexRef(),
+ aOther.mOwningElement,
+ const_cast<CSSAnimation*>(&aOther)->CachedChildIndexRef());
+ }
+
+ // 2. (Same element and pseudo): Sort by position in animation-name
+ return mAnimationIndex < aOther.mAnimationIndex;
+}
+
+void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
+ // If the animation is pending, we ignore animation events until we finish
+ // pending.
+ if (mPendingState != PendingState::NotPending) {
+ return;
+ }
+
+ // CSS animations dispatch events at their owning element. This allows
+ // script to repurpose a CSS animation to target a different element,
+ // to use a group effect (which has no obvious "target element"), or
+ // to remove the animation effect altogether whilst still getting
+ // animation events.
+ //
+ // It does mean, however, that for a CSS animation that has no owning
+ // element (e.g. it was created using the CSSAnimation constructor or
+ // disassociated from CSS) no events are fired. If it becomes desirable
+ // for these animations to still fire events we should spec the concept
+ // of the "original owning element" or "event target" and allow script
+ // to set it when creating a CSSAnimation object.
+ if (!mOwningElement.IsSet()) {
+ return;
+ }
+
+ nsPresContext* presContext = mOwningElement.GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ uint64_t currentIteration = 0;
+ ComputedTiming::AnimationPhase currentPhase;
+ StickyTimeDuration intervalStartTime;
+ StickyTimeDuration intervalEndTime;
+ StickyTimeDuration iterationStartTime;
+
+ if (!mEffect) {
+ currentPhase =
+ GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
+ if (currentPhase == mPreviousPhase) {
+ return;
+ }
+ } else {
+ ComputedTiming computedTiming = mEffect->GetComputedTiming();
+ currentPhase = computedTiming.mPhase;
+ currentIteration = computedTiming.mCurrentIteration;
+ if (currentPhase == mPreviousPhase &&
+ currentIteration == mPreviousIteration) {
+ return;
+ }
+ intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
+ intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
+
+ uint64_t iterationBoundary = mPreviousIteration > currentIteration
+ ? currentIteration + 1
+ : currentIteration;
+ double multiplier = iterationBoundary - computedTiming.mIterationStart;
+ if (multiplier != 0.0) {
+ iterationStartTime = computedTiming.mDuration.MultDouble(multiplier);
+ }
+ }
+
+ TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
+ TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
+ TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
+
+ AutoTArray<AnimationEventInfo, 2> events;
+
+ auto appendAnimationEvent = [&](EventMessage aMessage,
+ const StickyTimeDuration& aElapsedTime,
+ const TimeStamp& aScheduledEventTimeStamp) {
+ double elapsedTime = aElapsedTime.ToSeconds();
+ if (aMessage == eAnimationCancel) {
+ // 0 is an inappropriate value for this callsite. What we need to do is
+ // use a single random value for all increasing times reportable.
+ // That is to say, whenever elapsedTime goes negative (because an
+ // animation restarts, something rewinds the animation, or otherwise)
+ // a new random value for the mix-in must be generated.
+ elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
+ elapsedTime, 0, mRTPCallerType);
+ }
+ events.AppendElement(
+ AnimationEventInfo(mAnimationName, mOwningElement.Target(), aMessage,
+ elapsedTime, aScheduledEventTimeStamp, this));
+ };
+
+ // Handle cancel event first
+ if ((mPreviousPhase != AnimationPhase::Idle &&
+ mPreviousPhase != AnimationPhase::After) &&
+ currentPhase == AnimationPhase::Idle) {
+ appendAnimationEvent(eAnimationCancel, aActiveTime,
+ GetTimelineCurrentTimeAsTimeStamp());
+ }
+
+ switch (mPreviousPhase) {
+ case AnimationPhase::Idle:
+ case AnimationPhase::Before:
+ if (currentPhase == AnimationPhase::Active) {
+ appendAnimationEvent(eAnimationStart, intervalStartTime,
+ startTimeStamp);
+ } else if (currentPhase == AnimationPhase::After) {
+ appendAnimationEvent(eAnimationStart, intervalStartTime,
+ startTimeStamp);
+ appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
+ }
+ break;
+ case AnimationPhase::Active:
+ if (currentPhase == AnimationPhase::Before) {
+ appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
+ } else if (currentPhase == AnimationPhase::Active) {
+ // The currentIteration must have changed or element we would have
+ // returned early above.
+ MOZ_ASSERT(currentIteration != mPreviousIteration);
+ appendAnimationEvent(eAnimationIteration, iterationStartTime,
+ iterationTimeStamp);
+ } else if (currentPhase == AnimationPhase::After) {
+ appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
+ }
+ break;
+ case AnimationPhase::After:
+ if (currentPhase == AnimationPhase::Before) {
+ appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
+ appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
+ } else if (currentPhase == AnimationPhase::Active) {
+ appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
+ }
+ break;
+ }
+ mPreviousPhase = currentPhase;
+ mPreviousIteration = currentIteration;
+
+ if (!events.IsEmpty()) {
+ presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
+ }
+}
+
+void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag) {
+ if (mNeedsNewAnimationIndexWhenRun &&
+ PlayState() != AnimationPlayState::Idle) {
+ mAnimationIndex = sNextAnimationIndex++;
+ mNeedsNewAnimationIndexWhenRun = false;
+ }
+
+ Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
+}
+
+/////////////////////// CSSAnimationKeyframeEffect ////////////////////////
+
+void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
+ MaybeFlushUnanimatedStyle();
+ KeyframeEffect::GetTiming(aRetVal);
+}
+
+void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
+ ComputedEffectTiming& aRetVal) const {
+ MaybeFlushUnanimatedStyle();
+ KeyframeEffect::GetComputedTimingAsDict(aRetVal);
+}
+
+void CSSAnimationKeyframeEffect::UpdateTiming(
+ const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
+ KeyframeEffect::UpdateTiming(aTiming, aRv);
+
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
+ CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
+ if (aTiming.mDuration.WasPassed()) {
+ updatedProperties |= CSSAnimationProperties::Duration;
+ }
+ if (aTiming.mIterations.WasPassed()) {
+ updatedProperties |= CSSAnimationProperties::IterationCount;
+ }
+ if (aTiming.mDirection.WasPassed()) {
+ updatedProperties |= CSSAnimationProperties::Direction;
+ }
+ if (aTiming.mDelay.WasPassed()) {
+ updatedProperties |= CSSAnimationProperties::Delay;
+ }
+ if (aTiming.mFill.WasPassed()) {
+ updatedProperties |= CSSAnimationProperties::FillMode;
+ }
+
+ cssAnimation->AddOverriddenProperties(updatedProperties);
+ }
+}
+
+void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv) {
+ KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv);
+
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
+ cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes);
+ }
+}
+
+void CSSAnimationKeyframeEffect::SetComposite(
+ const CompositeOperation& aComposite) {
+ KeyframeEffect::SetComposite(aComposite);
+
+ if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
+ cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Composition);
+ }
+}
+
+void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
+ if (!GetOwningCSSAnimation()) {
+ return;
+ }
+
+ if (dom::Document* doc = GetRenderedDocument()) {
+ doc->FlushPendingNotifications(
+ ChangesToFlush(FlushType::Style, false /* flush animations */));
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/CSSAnimation.h b/dom/animation/CSSAnimation.h
new file mode 100644
index 0000000000..e1c45c49b9
--- /dev/null
+++ b/dom/animation/CSSAnimation.h
@@ -0,0 +1,238 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_CSSAnimation_h
+#define mozilla_dom_CSSAnimation_h
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "AnimationCommon.h"
+
+namespace mozilla {
+// Properties of CSS Animations that can be overridden by the Web Animations API
+// in a manner that means we should ignore subsequent changes to markup for that
+// property.
+enum class CSSAnimationProperties {
+ None = 0,
+ Keyframes = 1 << 0,
+ Duration = 1 << 1,
+ IterationCount = 1 << 2,
+ Direction = 1 << 3,
+ Delay = 1 << 4,
+ FillMode = 1 << 5,
+ Composition = 1 << 6,
+ Effect = Keyframes | Duration | IterationCount | Direction | Delay |
+ FillMode | Composition,
+ PlayState = 1 << 7,
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CSSAnimationProperties)
+
+namespace dom {
+
+class CSSAnimation final : public Animation {
+ public:
+ explicit CSSAnimation(nsIGlobalObject* aGlobal, nsAtom* aAnimationName)
+ : dom::Animation(aGlobal),
+ mAnimationName(aAnimationName),
+ mNeedsNewAnimationIndexWhenRun(false),
+ mPreviousPhase(ComputedTiming::AnimationPhase::Idle),
+ mPreviousIteration(0) {
+ // We might need to drop this assertion once we add a script-accessible
+ // constructor but for animations generated from CSS markup the
+ // animation-name should never be empty.
+ MOZ_ASSERT(mAnimationName != nsGkAtoms::_empty,
+ "animation-name should not be 'none'");
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ CSSAnimation* AsCSSAnimation() override { return this; }
+ const CSSAnimation* AsCSSAnimation() const override { return this; }
+
+ // CSSAnimation interface
+ void GetAnimationName(nsString& aRetVal) const {
+ mAnimationName->ToString(aRetVal);
+ }
+
+ nsAtom* AnimationName() const { return mAnimationName; }
+
+ // Animation interface overrides
+ void SetEffect(AnimationEffect* aEffect) override;
+ void SetStartTimeAsDouble(const Nullable<double>& aStartTime) override;
+ Promise* GetReady(ErrorResult& aRv) override;
+ void Reverse(ErrorResult& aRv) override;
+
+ // NOTE: tabbrowser.xml currently relies on the fact that reading the
+ // currentTime of a CSSAnimation does *not* flush style (whereas reading the
+ // playState does). If CSS Animations 2 specifies that reading currentTime
+ // also flushes style we will need to find another way to detect canceled
+ // animations in tabbrowser.xml. On the other hand, if CSS Animations 2
+ // specifies that reading playState does *not* flush style (and we drop the
+ // following override), then we should update tabbrowser.xml to check
+ // the playState instead.
+ AnimationPlayState PlayStateFromJS() const override;
+ bool PendingFromJS() const override;
+ void PlayFromJS(ErrorResult& aRv) override;
+ void PauseFromJS(ErrorResult& aRv) override;
+
+ void PlayFromStyle();
+ void PauseFromStyle();
+ void CancelFromStyle(PostRestyleMode aPostRestyle) {
+ // When an animation is disassociated with style it enters an odd state
+ // where its composite order is undefined until it first transitions
+ // out of the idle state.
+ //
+ // Even if the composite order isn't defined we don't want it to be random
+ // in case we need to determine the order to dispatch events associated
+ // with an animation in this state. To solve this we treat the animation as
+ // if it had been added to the end of the global animation list so that
+ // its sort order is defined. We'll update this index again once the
+ // animation leaves the idle state.
+ mAnimationIndex = sNextAnimationIndex++;
+ mNeedsNewAnimationIndexWhenRun = true;
+
+ Animation::Cancel(aPostRestyle);
+
+ // We need to do this *after* calling Cancel() since
+ // Cancel() might synchronously trigger a cancel event for which
+ // we need an owning element to target the event at.
+ mOwningElement = OwningElementRef();
+ }
+
+ void Tick() override;
+ void QueueEvents(
+ const StickyTimeDuration& aActiveTime = StickyTimeDuration());
+
+ bool HasLowerCompositeOrderThan(const CSSAnimation& aOther) const;
+
+ void SetAnimationIndex(uint64_t aIndex) {
+ MOZ_ASSERT(IsTiedToMarkup());
+ if (IsRelevant() && mAnimationIndex != aIndex) {
+ MutationObservers::NotifyAnimationChanged(this);
+ PostUpdate();
+ }
+ mAnimationIndex = aIndex;
+ }
+
+ // Sets the owning element which is used for determining the composite
+ // order of CSSAnimation objects generated from CSS markup.
+ //
+ // @see mOwningElement
+ void SetOwningElement(const OwningElementRef& aElement) {
+ mOwningElement = aElement;
+ }
+ // True for animations that are generated from CSS markup and continue to
+ // reflect changes to that markup.
+ bool IsTiedToMarkup() const { return mOwningElement.IsSet(); }
+
+ void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override {
+ QueueEvents(aActiveTime);
+ }
+
+ CSSAnimationProperties GetOverriddenProperties() const {
+ return mOverriddenProperties;
+ }
+ void AddOverriddenProperties(CSSAnimationProperties aProperties) {
+ mOverriddenProperties |= aProperties;
+ }
+
+ protected:
+ virtual ~CSSAnimation() {
+ MOZ_ASSERT(!mOwningElement.IsSet(),
+ "Owning element should be cleared "
+ "before a CSS animation is destroyed");
+ }
+
+ // Animation overrides
+ void UpdateTiming(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag) override;
+
+ // Returns the duration from the start of the animation's source effect's
+ // active interval to the point where the animation actually begins playback.
+ // This is zero unless the animation's source effect has a negative delay in
+ // which case it is the absolute value of that delay.
+ // This is used for setting the elapsedTime member of CSS AnimationEvents.
+ TimeDuration InitialAdvance() const {
+ return mEffect ? std::max(TimeDuration(),
+ mEffect->NormalizedTiming().Delay() * -1)
+ : TimeDuration();
+ }
+
+ RefPtr<nsAtom> mAnimationName;
+
+ // The (pseudo-)element whose computed animation-name refers to this
+ // animation (if any).
+ //
+ // This is used for determining the relative composite order of animations
+ // generated from CSS markup.
+ //
+ // Typically this will be the same as the target element of the keyframe
+ // effect associated with this animation. However, it can differ in the
+ // following circumstances:
+ //
+ // a) If script removes or replaces the effect of this animation,
+ // b) If this animation is cancelled (e.g. by updating the
+ // animation-name property or removing the owning element from the
+ // document),
+ // c) If this object is generated from script using the CSSAnimation
+ // constructor.
+ //
+ // For (b) and (c) the owning element will return !IsSet().
+ OwningElementRef mOwningElement;
+
+ // When true, indicates that when this animation next leaves the idle state,
+ // its animation index should be updated.
+ bool mNeedsNewAnimationIndexWhenRun;
+
+ // Phase and current iteration from the previous time we queued events.
+ // This is used to determine what new events to dispatch.
+ ComputedTiming::AnimationPhase mPreviousPhase;
+ uint64_t mPreviousIteration;
+
+ // Properties that would normally be defined by the cascade but which have
+ // since been explicitly set via the Web Animations API.
+ CSSAnimationProperties mOverriddenProperties = CSSAnimationProperties::None;
+};
+
+// A subclass of KeyframeEffect that reports when specific properties have been
+// overridden via the Web Animations API.
+class CSSAnimationKeyframeEffect : public KeyframeEffect {
+ public:
+ CSSAnimationKeyframeEffect(Document* aDocument,
+ OwningAnimationTarget&& aTarget,
+ TimingParams&& aTiming,
+ const KeyframeEffectParams& aOptions)
+ : KeyframeEffect(aDocument, std::move(aTarget), std::move(aTiming),
+ aOptions) {}
+
+ void GetTiming(EffectTiming& aRetVal) const override;
+ void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const override;
+ void UpdateTiming(const OptionalEffectTiming& aTiming,
+ ErrorResult& aRv) override;
+ void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
+ ErrorResult& aRv) override;
+ void SetComposite(const CompositeOperation& aComposite) override;
+
+ private:
+ CSSAnimation* GetOwningCSSAnimation() {
+ return mAnimation ? mAnimation->AsCSSAnimation() : nullptr;
+ }
+ const CSSAnimation* GetOwningCSSAnimation() const {
+ return mAnimation ? mAnimation->AsCSSAnimation() : nullptr;
+ }
+
+ // Flushes styles if our owning animation is a CSSAnimation
+ void MaybeFlushUnanimatedStyle() const;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif // mozilla_dom_CSSAnimation_h
diff --git a/dom/animation/CSSPseudoElement.cpp b/dom/animation/CSSPseudoElement.cpp
new file mode 100644
index 0000000000..9dfdd9d4f6
--- /dev/null
+++ b/dom/animation/CSSPseudoElement.cpp
@@ -0,0 +1,90 @@
+/* -*- 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/CSSPseudoElement.h"
+#include "mozilla/dom/CSSPseudoElementBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/AnimationComparator.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CSSPseudoElement, mOriginatingElement)
+
+CSSPseudoElement::CSSPseudoElement(dom::Element* aElement,
+ PseudoStyleType aType)
+ : mOriginatingElement(aElement), mPseudoType(aType) {
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(AnimationUtils::IsSupportedPseudoForAnimations(aType),
+ "Unexpected Pseudo Type");
+}
+
+CSSPseudoElement::~CSSPseudoElement() {
+ // Element might have been unlinked already, so we have to do null check.
+ if (mOriginatingElement) {
+ mOriginatingElement->RemoveProperty(
+ GetCSSPseudoElementPropertyAtom(mPseudoType));
+ }
+}
+
+ParentObject CSSPseudoElement::GetParentObject() const {
+ return mOriginatingElement->GetParentObject();
+}
+
+JSObject* CSSPseudoElement::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return CSSPseudoElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<CSSPseudoElement> CSSPseudoElement::GetCSSPseudoElement(
+ dom::Element* aElement, PseudoStyleType aType) {
+ if (!aElement) {
+ return nullptr;
+ }
+
+ nsAtom* propName = CSSPseudoElement::GetCSSPseudoElementPropertyAtom(aType);
+ RefPtr<CSSPseudoElement> pseudo =
+ static_cast<CSSPseudoElement*>(aElement->GetProperty(propName));
+ if (pseudo) {
+ return pseudo.forget();
+ }
+
+ // CSSPseudoElement is a purely external interface created on-demand, and
+ // when all references from script to the pseudo are dropped, we can drop the
+ // CSSPseudoElement object, so use a non-owning reference from Element to
+ // CSSPseudoElement.
+ pseudo = new CSSPseudoElement(aElement, aType);
+ nsresult rv = aElement->SetProperty(propName, pseudo, nullptr, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SetProperty failed");
+ return nullptr;
+ }
+ return pseudo.forget();
+}
+
+/* static */
+nsAtom* CSSPseudoElement::GetCSSPseudoElementPropertyAtom(
+ PseudoStyleType aType) {
+ switch (aType) {
+ case PseudoStyleType::before:
+ return nsGkAtoms::cssPseudoElementBeforeProperty;
+
+ case PseudoStyleType::after:
+ return nsGkAtoms::cssPseudoElementAfterProperty;
+
+ case PseudoStyleType::marker:
+ return nsGkAtoms::cssPseudoElementMarkerProperty;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Should not try to get CSSPseudoElement "
+ "other than ::before, ::after or ::marker");
+ return nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/CSSPseudoElement.h b/dom/animation/CSSPseudoElement.h
new file mode 100644
index 0000000000..4b5fbc917c
--- /dev/null
+++ b/dom/animation/CSSPseudoElement.h
@@ -0,0 +1,73 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_CSSPseudoElement_h
+#define mozilla_dom_CSSPseudoElement_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/RefPtr.h"
+#include "nsCSSPseudoElements.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+class Animation;
+class Element;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+
+class CSSPseudoElement final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CSSPseudoElement)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(CSSPseudoElement)
+
+ protected:
+ virtual ~CSSPseudoElement();
+
+ public:
+ ParentObject GetParentObject() const;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ PseudoStyleType GetType() const { return mPseudoType; }
+ void GetType(nsString& aRetVal) const {
+ MOZ_ASSERT(nsCSSPseudoElements::GetPseudoAtom(mPseudoType),
+ "All pseudo-types allowed by this class should have a"
+ " corresponding atom");
+ // Our atoms use one colon and we would like to return two colons syntax
+ // for the returned pseudo type string, so serialize this to the
+ // non-deprecated two colon syntax.
+ aRetVal.Assign(char16_t(':'));
+ aRetVal.Append(
+ nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(mPseudoType)));
+ }
+ dom::Element* Element() const { return mOriginatingElement.get(); }
+
+ // Given an element:pseudoType pair, returns the CSSPseudoElement stored as a
+ // property on |aElement|. If there is no CSSPseudoElement for the specified
+ // pseudo-type on element, a new CSSPseudoElement will be created and stored
+ // on the element.
+ static already_AddRefed<CSSPseudoElement> GetCSSPseudoElement(
+ dom::Element* aElement, PseudoStyleType aType);
+
+ private:
+ // Only ::before, ::after and ::marker are supported.
+ CSSPseudoElement(dom::Element* aElement, PseudoStyleType aType);
+
+ static nsAtom* GetCSSPseudoElementPropertyAtom(PseudoStyleType aType);
+
+ // mOriginatingElement needs to be an owning reference since if script is
+ // holding on to the pseudo-element, it needs to continue to be able to refer
+ // to the originating element.
+ RefPtr<dom::Element> mOriginatingElement;
+ PseudoStyleType mPseudoType;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_CSSPseudoElement_h
diff --git a/dom/animation/CSSTransition.cpp b/dom/animation/CSSTransition.cpp
new file mode 100644
index 0000000000..38f9c9386a
--- /dev/null
+++ b/dom/animation/CSSTransition.cpp
@@ -0,0 +1,333 @@
+/* -*- 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 "CSSTransition.h"
+
+#include "mozilla/AnimationEventDispatcher.h"
+#include "mozilla/dom/CSSTransitionBinding.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/TimeStamp.h"
+#include "nsPresContext.h"
+
+namespace mozilla::dom {
+
+JSObject* CSSTransition::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void CSSTransition::GetTransitionProperty(nsString& aRetVal) const {
+ MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty,
+ "Transition Property should be initialized");
+ aRetVal =
+ NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty));
+}
+
+AnimationPlayState CSSTransition::PlayStateFromJS() const {
+ FlushUnanimatedStyle();
+ return Animation::PlayStateFromJS();
+}
+
+bool CSSTransition::PendingFromJS() const {
+ // Transitions don't become pending again after they start running but, if
+ // while the transition is still pending, style is updated in such a way
+ // that the transition will be canceled, we need to report false here.
+ // Hence we need to flush, but only when we're pending.
+ if (Pending()) {
+ FlushUnanimatedStyle();
+ }
+ return Animation::PendingFromJS();
+}
+
+void CSSTransition::PlayFromJS(ErrorResult& aRv) {
+ FlushUnanimatedStyle();
+ Animation::PlayFromJS(aRv);
+}
+
+void CSSTransition::UpdateTiming(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag) {
+ if (mNeedsNewAnimationIndexWhenRun &&
+ PlayState() != AnimationPlayState::Idle) {
+ mAnimationIndex = sNextAnimationIndex++;
+ mNeedsNewAnimationIndexWhenRun = false;
+ }
+
+ Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
+}
+
+void CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) {
+ if (!mOwningElement.IsSet()) {
+ return;
+ }
+
+ nsPresContext* presContext = mOwningElement.GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
+
+ TransitionPhase currentPhase;
+ StickyTimeDuration intervalStartTime;
+ StickyTimeDuration intervalEndTime;
+
+ if (!mEffect) {
+ currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this);
+ } else {
+ ComputedTiming computedTiming = mEffect->GetComputedTiming();
+
+ currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase);
+ intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
+ intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
+ }
+
+ if (mPendingState != PendingState::NotPending &&
+ (mPreviousTransitionPhase == TransitionPhase::Idle ||
+ mPreviousTransitionPhase == TransitionPhase::Pending)) {
+ currentPhase = TransitionPhase::Pending;
+ }
+
+ if (currentPhase == mPreviousTransitionPhase) {
+ return;
+ }
+
+ // TimeStamps to use for ordering the events when they are dispatched. We
+ // use a TimeStamp so we can compare events produced by different elements,
+ // perhaps even with different timelines.
+ // The zero timestamp is for transitionrun events where we ignore the delay
+ // for the purpose of ordering events.
+ TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration);
+ TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
+ TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
+
+ AutoTArray<AnimationEventInfo, 3> events;
+
+ auto appendTransitionEvent = [&](EventMessage aMessage,
+ const StickyTimeDuration& aElapsedTime,
+ const TimeStamp& aScheduledEventTimeStamp) {
+ double elapsedTime = aElapsedTime.ToSeconds();
+ if (aMessage == eTransitionCancel) {
+ // 0 is an inappropriate value for this callsite. What we need to do is
+ // use a single random value for all increasing times reportable.
+ // That is to say, whenever elapsedTime goes negative (because an
+ // animation restarts, something rewinds the animation, or otherwise)
+ // a new random value for the mix-in must be generated.
+ elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
+ elapsedTime, 0, mRTPCallerType);
+ }
+ events.AppendElement(AnimationEventInfo(
+ TransitionProperty(), mOwningElement.Target(), aMessage, elapsedTime,
+ aScheduledEventTimeStamp, this));
+ };
+
+ // Handle cancel events first
+ if ((mPreviousTransitionPhase != TransitionPhase::Idle &&
+ mPreviousTransitionPhase != TransitionPhase::After) &&
+ currentPhase == TransitionPhase::Idle) {
+ appendTransitionEvent(eTransitionCancel, aActiveTime,
+ GetTimelineCurrentTimeAsTimeStamp());
+ }
+
+ // All other events
+ switch (mPreviousTransitionPhase) {
+ case TransitionPhase::Idle:
+ if (currentPhase == TransitionPhase::Pending ||
+ currentPhase == TransitionPhase::Before) {
+ appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp);
+ } else if (currentPhase == TransitionPhase::Active) {
+ appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp);
+ appendTransitionEvent(eTransitionStart, intervalStartTime,
+ startTimeStamp);
+ } else if (currentPhase == TransitionPhase::After) {
+ appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp);
+ appendTransitionEvent(eTransitionStart, intervalStartTime,
+ startTimeStamp);
+ appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp);
+ }
+ break;
+
+ case TransitionPhase::Pending:
+ case TransitionPhase::Before:
+ if (currentPhase == TransitionPhase::Active) {
+ appendTransitionEvent(eTransitionStart, intervalStartTime,
+ startTimeStamp);
+ } else if (currentPhase == TransitionPhase::After) {
+ appendTransitionEvent(eTransitionStart, intervalStartTime,
+ startTimeStamp);
+ appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp);
+ }
+ break;
+
+ case TransitionPhase::Active:
+ if (currentPhase == TransitionPhase::After) {
+ appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp);
+ } else if (currentPhase == TransitionPhase::Before) {
+ appendTransitionEvent(eTransitionEnd, intervalStartTime,
+ startTimeStamp);
+ }
+ break;
+
+ case TransitionPhase::After:
+ if (currentPhase == TransitionPhase::Active) {
+ appendTransitionEvent(eTransitionStart, intervalEndTime,
+ startTimeStamp);
+ } else if (currentPhase == TransitionPhase::Before) {
+ appendTransitionEvent(eTransitionStart, intervalEndTime,
+ startTimeStamp);
+ appendTransitionEvent(eTransitionEnd, intervalStartTime, endTimeStamp);
+ }
+ break;
+ }
+ mPreviousTransitionPhase = currentPhase;
+
+ if (!events.IsEmpty()) {
+ presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
+ }
+}
+
+void CSSTransition::Tick() {
+ Animation::Tick();
+ QueueEvents();
+}
+
+nsCSSPropertyID CSSTransition::TransitionProperty() const {
+ MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty,
+ "Transition property should be initialized");
+ return mTransitionProperty;
+}
+
+AnimationValue CSSTransition::ToValue() const {
+ MOZ_ASSERT(!mTransitionToValue.IsNull(),
+ "Transition ToValue should be initialized");
+ return mTransitionToValue;
+}
+
+bool CSSTransition::HasLowerCompositeOrderThan(
+ const CSSTransition& aOther) const {
+ MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
+ "Should only be called for CSS transitions that are sorted "
+ "as CSS transitions (i.e. tied to CSS markup)");
+
+ // 0. Object-equality case
+ if (&aOther == this) {
+ return false;
+ }
+
+ // 1. Sort by document order
+ if (!mOwningElement.Equals(aOther.mOwningElement)) {
+ return mOwningElement.LessThan(
+ const_cast<CSSTransition*>(this)->CachedChildIndexRef(),
+ aOther.mOwningElement,
+ const_cast<CSSTransition*>(&aOther)->CachedChildIndexRef());
+ }
+
+ // 2. (Same element and pseudo): Sort by transition generation
+ if (mAnimationIndex != aOther.mAnimationIndex) {
+ return mAnimationIndex < aOther.mAnimationIndex;
+ }
+
+ // 3. (Same transition generation): Sort by transition property
+ return nsCSSProps::GetStringValue(TransitionProperty()) <
+ nsCSSProps::GetStringValue(aOther.TransitionProperty());
+}
+
+/* static */
+Nullable<TimeDuration> CSSTransition::GetCurrentTimeAt(
+ const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime,
+ const TimeDuration& aStartTime, double aPlaybackRate) {
+ Nullable<TimeDuration> result;
+
+ Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime);
+ if (!timelineTime.IsNull()) {
+ result.SetValue(
+ (timelineTime.Value() - aStartTime).MultDouble(aPlaybackRate));
+ }
+
+ return result;
+}
+
+double CSSTransition::CurrentValuePortion() const {
+ if (!GetEffect()) {
+ return 0.0;
+ }
+
+ // Transitions use a fill mode of 'backwards' so GetComputedTiming will
+ // never return a null time progress due to being *before* the animation
+ // interval. However, it might be possible that we're behind on flushing
+ // causing us to get called *after* the animation interval. So, just in
+ // case, we override the fill mode to 'both' to ensure the progress
+ // is never null.
+ TimingParams timingToUse = GetEffect()->SpecifiedTiming();
+ timingToUse.SetFill(dom::FillMode::Both);
+ ComputedTiming computedTiming = GetEffect()->GetComputedTiming(&timingToUse);
+
+ if (computedTiming.mProgress.IsNull()) {
+ return 0.0;
+ }
+
+ // 'transition-timing-function' corresponds to the effect timing while
+ // the transition keyframes have a linear timing function so we can ignore
+ // them for the purposes of calculating the value portion.
+ return computedTiming.mProgress.Value();
+}
+
+void CSSTransition::UpdateStartValueFromReplacedTransition() {
+ MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() &&
+ mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet(
+ nsCSSPropertyIDSet::CompositorAnimatables()),
+ "Should be called for compositor-runnable transitions");
+
+ if (!mReplacedTransition) {
+ return;
+ }
+
+ // We don't set |mReplacedTransition| if the timeline of this transition is
+ // different from the document timeline. The timeline of Animation may be
+ // null via script, so if it's null, it must be different from the document
+ // timeline (because document timeline is readonly so we cannot change it by
+ // script). Therefore, we check this assertion if mReplacedTransition is
+ // valid.
+ MOZ_ASSERT(mTimeline,
+ "Should have a timeline if we are replacing transition start "
+ "values");
+
+ ComputedTiming computedTiming = AnimationEffect::GetComputedTimingAt(
+ CSSTransition::GetCurrentTimeAt(*mTimeline, TimeStamp::Now(),
+ mReplacedTransition->mStartTime,
+ mReplacedTransition->mPlaybackRate),
+ mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate,
+ Animation::ProgressTimelinePosition::NotBoundary);
+
+ if (!computedTiming.mProgress.IsNull()) {
+ double valuePosition = StyleComputedTimingFunction::GetPortion(
+ mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(),
+ computedTiming.mBeforeFlag);
+
+ const AnimationValue& replacedFrom = mReplacedTransition->mFromValue;
+ const AnimationValue& replacedTo = mReplacedTransition->mToValue;
+ AnimationValue startValue;
+ startValue.mServo =
+ Servo_AnimationValues_Interpolate(replacedFrom.mServo,
+ replacedTo.mServo, valuePosition)
+ .Consume();
+
+ mEffect->AsKeyframeEffect()->ReplaceTransitionStartValue(
+ std::move(startValue));
+ }
+
+ mReplacedTransition.reset();
+}
+
+void CSSTransition::SetEffectFromStyle(KeyframeEffect* aEffect) {
+ MOZ_ASSERT(aEffect->IsValidTransition());
+
+ Animation::SetEffectNoUpdate(aEffect);
+ mTransitionProperty = aEffect->Properties()[0].mProperty;
+ mTransitionToValue = aEffect->Properties()[0].mSegments[0].mToValue;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/CSSTransition.h b/dom/animation/CSSTransition.h
new file mode 100644
index 0000000000..ec246f8c26
--- /dev/null
+++ b/dom/animation/CSSTransition.h
@@ -0,0 +1,230 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_CSSTransition_h
+#define mozilla_dom_CSSTransition_h
+
+#include "mozilla/ComputedTiming.h"
+#include "mozilla/dom/Animation.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "AnimationCommon.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+namespace dom {
+
+class CSSTransition final : public Animation {
+ public:
+ explicit CSSTransition(nsIGlobalObject* aGlobal)
+ : Animation(aGlobal),
+ mPreviousTransitionPhase(TransitionPhase::Idle),
+ mNeedsNewAnimationIndexWhenRun(false),
+ mTransitionProperty(eCSSProperty_UNKNOWN) {}
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ CSSTransition* AsCSSTransition() override { return this; }
+ const CSSTransition* AsCSSTransition() const override { return this; }
+
+ // CSSTransition interface
+ void GetTransitionProperty(nsString& aRetVal) const;
+
+ // Animation interface overrides
+ AnimationPlayState PlayStateFromJS() const override;
+ bool PendingFromJS() const override;
+ void PlayFromJS(ErrorResult& aRv) override;
+
+ // A variant of Play() that avoids posting style updates since this method
+ // is expected to be called whilst already updating style.
+ void PlayFromStyle() {
+ ErrorResult rv;
+ PlayNoUpdate(rv, Animation::LimitBehavior::Continue);
+ // play() should not throw when LimitBehavior is Continue
+ MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing transition");
+ }
+
+ void CancelFromStyle(PostRestyleMode aPostRestyle) {
+ // The animation index to use for compositing will be established when
+ // this transition next transitions out of the idle state but we still
+ // update it now so that the sort order of this transition remains
+ // defined until that moment.
+ //
+ // See longer explanation in CSSAnimation::CancelFromStyle.
+ mAnimationIndex = sNextAnimationIndex++;
+ mNeedsNewAnimationIndexWhenRun = true;
+
+ Animation::Cancel(aPostRestyle);
+
+ // It is important we do this *after* calling Cancel().
+ // This is because Cancel() will end up posting a restyle and
+ // that restyle should target the *transitions* level of the cascade.
+ // However, once we clear the owning element, CascadeLevel() will begin
+ // returning CascadeLevel::Animations.
+ mOwningElement = OwningElementRef();
+ }
+
+ void SetEffectFromStyle(KeyframeEffect*);
+
+ void Tick() override;
+
+ nsCSSPropertyID TransitionProperty() const;
+ AnimationValue ToValue() const;
+
+ bool HasLowerCompositeOrderThan(const CSSTransition& aOther) const;
+ EffectCompositor::CascadeLevel CascadeLevel() const override {
+ return IsTiedToMarkup() ? EffectCompositor::CascadeLevel::Transitions
+ : EffectCompositor::CascadeLevel::Animations;
+ }
+
+ void SetCreationSequence(uint64_t aIndex) {
+ MOZ_ASSERT(IsTiedToMarkup());
+ mAnimationIndex = aIndex;
+ }
+
+ // Sets the owning element which is used for determining the composite
+ // oder of CSSTransition objects generated from CSS markup.
+ //
+ // @see mOwningElement
+ void SetOwningElement(const OwningElementRef& aElement) {
+ mOwningElement = aElement;
+ }
+ // True for transitions that are generated from CSS markup and continue to
+ // reflect changes to that markup.
+ bool IsTiedToMarkup() const { return mOwningElement.IsSet(); }
+
+ // Return the animation current time based on a given TimeStamp, a given
+ // start time and a given playbackRate on a given timeline. This is useful
+ // when we estimate the current animated value running on the compositor
+ // because the animation on the compositor may be running ahead while
+ // main-thread is busy.
+ static Nullable<TimeDuration> GetCurrentTimeAt(
+ const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime,
+ const TimeDuration& aStartTime, double aPlaybackRate);
+
+ void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override {
+ QueueEvents(aActiveTime);
+ }
+
+ // Compute the portion of the *value* space that we should be through
+ // at the current time. (The input to the transition timing function
+ // has time units, the output has value units.)
+ double CurrentValuePortion() const;
+
+ const AnimationValue& StartForReversingTest() const {
+ return mStartForReversingTest;
+ }
+ double ReversePortion() const { return mReversePortion; }
+
+ void SetReverseParameters(AnimationValue&& aStartForReversingTest,
+ double aReversePortion) {
+ mStartForReversingTest = std::move(aStartForReversingTest);
+ mReversePortion = aReversePortion;
+ }
+
+ struct ReplacedTransitionProperties {
+ TimeDuration mStartTime;
+ double mPlaybackRate;
+ TimingParams mTiming;
+ Maybe<StyleComputedTimingFunction> mTimingFunction;
+ AnimationValue mFromValue, mToValue;
+ };
+ void SetReplacedTransition(
+ ReplacedTransitionProperties&& aReplacedTransition) {
+ mReplacedTransition.emplace(std::move(aReplacedTransition));
+ }
+
+ // For a new transition interrupting an existing transition on the
+ // compositor, update the start value to match the value of the replaced
+ // transitions at the current time.
+ void UpdateStartValueFromReplacedTransition();
+
+ protected:
+ virtual ~CSSTransition() {
+ MOZ_ASSERT(!mOwningElement.IsSet(),
+ "Owning element should be cleared "
+ "before a CSS transition is destroyed");
+ }
+
+ // Animation overrides
+ void UpdateTiming(SeekFlag aSeekFlag,
+ SyncNotifyFlag aSyncNotifyFlag) override;
+
+ void QueueEvents(const StickyTimeDuration& activeTime = StickyTimeDuration());
+
+ enum class TransitionPhase;
+
+ // The (pseudo-)element whose computed transition-property refers to this
+ // transition (if any).
+ //
+ // This is used for determining the relative composite order of transitions
+ // generated from CSS markup.
+ //
+ // Typically this will be the same as the target element of the keyframe
+ // effect associated with this transition. However, it can differ in the
+ // following circumstances:
+ //
+ // a) If script removes or replaces the effect of this transition,
+ // b) If this transition is cancelled (e.g. by updating the
+ // transition-property or removing the owning element from the document),
+ // c) If this object is generated from script using the CSSTransition
+ // constructor.
+ //
+ // For (b) and (c) the owning element will return !IsSet().
+ OwningElementRef mOwningElement;
+
+ // The 'transition phase' used to determine which transition events need
+ // to be queued on this tick.
+ // See: https://drafts.csswg.org/css-transitions-2/#transition-phase
+ enum class TransitionPhase {
+ Idle = static_cast<int>(ComputedTiming::AnimationPhase::Idle),
+ Before = static_cast<int>(ComputedTiming::AnimationPhase::Before),
+ Active = static_cast<int>(ComputedTiming::AnimationPhase::Active),
+ After = static_cast<int>(ComputedTiming::AnimationPhase::After),
+ Pending
+ };
+ TransitionPhase mPreviousTransitionPhase;
+
+ // When true, indicates that when this transition next leaves the idle state,
+ // its animation index should be updated.
+ bool mNeedsNewAnimationIndexWhenRun;
+
+ // Store the transition property and to-value here since we need that
+ // information in order to determine if there is an existing transition
+ // for a given style change. We can't store that information on the
+ // effect however since it can be replaced using the Web Animations API.
+ nsCSSPropertyID mTransitionProperty;
+ AnimationValue mTransitionToValue;
+
+ // This is the start value to be used for a check for whether a
+ // transition is being reversed. Normally the same as
+ // mEffect->mProperties[0].mSegments[0].mFromValue, except when this
+ // transition started as the reversal of another in-progress transition
+ // or when the effect has been mutated using the Web Animations API.
+ //
+ // Needed so we can handle two reverses in a row.
+ AnimationValue mStartForReversingTest;
+
+ // Likewise, the portion (in value space) of the "full" reversed
+ // transition that we're actually covering. For example, if a :hover
+ // effect has a transition that moves the element 10px to the right
+ // (by changing 'left' from 0px to 10px), and the mouse moves in to
+ // the element (starting the transition) but then moves out after the
+ // transition has advanced 4px, the second transition (from 10px/4px
+ // to 0px) will have mReversePortion of 0.4. (If the mouse then moves
+ // in again when the transition is back to 2px, the mReversePortion
+ // for the third transition (from 0px/2px to 10px) will be 0.8.
+ double mReversePortion = 1.0;
+
+ Maybe<ReplacedTransitionProperties> mReplacedTransition;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif // mozilla_dom_CSSTransition_h
diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h
new file mode 100644
index 0000000000..fe98277bb3
--- /dev/null
+++ b/dom/animation/ComputedTiming.h
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ComputedTiming_h
+#define mozilla_ComputedTiming_h
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/StickyTimeDuration.h"
+
+#include "mozilla/dom/AnimationEffectBinding.h" // FillMode
+
+namespace mozilla {
+
+/**
+ * Stores the results of calculating the timing properties of an animation
+ * at a given sample time.
+ */
+struct ComputedTiming {
+ // The total duration of the animation including all iterations.
+ // Will equal StickyTimeDuration::Forever() if the animation repeats
+ // indefinitely.
+ StickyTimeDuration mActiveDuration;
+ // The time within the active interval.
+ StickyTimeDuration mActiveTime;
+ // The effect end time in local time (i.e. an offset from the effect's
+ // start time). Will equal StickyTimeDuration::Forever() if the animation
+ // plays indefinitely.
+ StickyTimeDuration mEndTime;
+ // Progress towards the end of the current iteration. If the effect is
+ // being sampled backwards, this will go from 1.0 to 0.0.
+ // Will be null if the animation is neither animating nor
+ // filling at the sampled time.
+ dom::Nullable<double> mProgress;
+ // Zero-based iteration index (meaningless if mProgress is null).
+ uint64_t mCurrentIteration = 0;
+ // Unlike TimingParams::mIterations, this value is
+ // guaranteed to be in the range [0, Infinity].
+ double mIterations = 1.0;
+ double mIterationStart = 0.0;
+ StickyTimeDuration mDuration;
+
+ // This is the computed fill mode so it is never auto
+ dom::FillMode mFill = dom::FillMode::None;
+ bool FillsForwards() const {
+ MOZ_ASSERT(mFill != dom::FillMode::Auto,
+ "mFill should not be Auto in ComputedTiming.");
+ return mFill == dom::FillMode::Both || mFill == dom::FillMode::Forwards;
+ }
+ bool FillsBackwards() const {
+ MOZ_ASSERT(mFill != dom::FillMode::Auto,
+ "mFill should not be Auto in ComputedTiming.");
+ return mFill == dom::FillMode::Both || mFill == dom::FillMode::Backwards;
+ }
+
+ enum class AnimationPhase {
+ Idle, // Not sampled (null sample time)
+ Before, // Sampled prior to the start of the active interval
+ Active, // Sampled within the active interval
+ After // Sampled after (or at) the end of the active interval
+ };
+ AnimationPhase mPhase = AnimationPhase::Idle;
+
+ bool mBeforeFlag = false;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ComputedTiming_h
diff --git a/dom/animation/DocumentTimeline.cpp b/dom/animation/DocumentTimeline.cpp
new file mode 100644
index 0000000000..1ccd65365a
--- /dev/null
+++ b/dom/animation/DocumentTimeline.cpp
@@ -0,0 +1,311 @@
+/* -*- 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 "DocumentTimeline.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/DocumentTimelineBinding.h"
+#include "AnimationUtils.h"
+#include "nsContentUtils.h"
+#include "nsDOMMutationObserver.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsPresContext.h"
+#include "nsRefreshDriver.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+ tmp->UnregisterFromRefreshDriver();
+ if (tmp->isInList()) {
+ tmp->remove();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentTimeline)
+NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
+
+NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline)
+NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline)
+
+DocumentTimeline::DocumentTimeline(Document* aDocument,
+ const TimeDuration& aOriginTime)
+ : AnimationTimeline(aDocument->GetParentObject(),
+ aDocument->GetScopeObject()->GetRTPCallerType()),
+ mDocument(aDocument),
+ mIsObservingRefreshDriver(false),
+ mOriginTime(aOriginTime) {
+ if (mDocument) {
+ mDocument->Timelines().insertBack(this);
+ }
+ // Ensure mLastRefreshDriverTime is valid.
+ UpdateLastRefreshDriverTime();
+}
+
+DocumentTimeline::~DocumentTimeline() {
+ MOZ_ASSERT(!mIsObservingRefreshDriver,
+ "Timeline should have disassociated"
+ " from the refresh driver before being destroyed");
+ if (isInList()) {
+ remove();
+ }
+}
+
+JSObject* DocumentTimeline::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return DocumentTimeline_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<DocumentTimeline> DocumentTimeline::Constructor(
+ const GlobalObject& aGlobal, const DocumentTimelineOptions& aOptions,
+ ErrorResult& aRv) {
+ Document* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ TimeDuration originTime =
+ TimeDuration::FromMilliseconds(aOptions.mOriginTime);
+
+ if (originTime == TimeDuration::Forever() ||
+ originTime == -TimeDuration::Forever()) {
+ aRv.ThrowTypeError<dom::MSG_TIME_VALUE_OUT_OF_RANGE>("Origin time");
+ return nullptr;
+ }
+ RefPtr<DocumentTimeline> timeline = new DocumentTimeline(doc, originTime);
+
+ return timeline.forget();
+}
+
+Nullable<TimeDuration> DocumentTimeline::GetCurrentTimeAsDuration() const {
+ return ToTimelineTime(GetCurrentTimeStamp());
+}
+
+bool DocumentTimeline::TracksWallclockTime() const {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ return !refreshDriver || !refreshDriver->IsTestControllingRefreshesEnabled();
+}
+
+TimeStamp DocumentTimeline::GetCurrentTimeStamp() const {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ return refreshDriver ? refreshDriver->MostRecentRefresh()
+ : mLastRefreshDriverTime;
+}
+
+void DocumentTimeline::UpdateLastRefreshDriverTime() {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ TimeStamp refreshTime =
+ refreshDriver ? refreshDriver->MostRecentRefresh() : TimeStamp();
+
+ // Always return the same object to benefit from return-value optimization.
+ TimeStamp result =
+ !refreshTime.IsNull() ? refreshTime : mLastRefreshDriverTime;
+
+ nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
+ // If we don't have a refresh driver and we've never had one use the
+ // timeline's zero time.
+ // In addition, it's possible that our refresh driver's timestamp is behind
+ // from the navigation start time because the refresh driver timestamp is
+ // sent through an IPC call whereas the navigation time is set by calling
+ // TimeStamp::Now() directly. In such cases we also use the timeline's zero
+ // time.
+ if (timing &&
+ (result.IsNull() || result < timing->GetNavigationStartTimeStamp())) {
+ result = timing->GetNavigationStartTimeStamp();
+ // Also, let this time represent the current refresh time. This way
+ // we'll save it as the last refresh time and skip looking up
+ // navigation start time each time.
+ refreshTime = result;
+ }
+
+ if (!refreshTime.IsNull()) {
+ mLastRefreshDriverTime = refreshTime;
+ }
+}
+
+Nullable<TimeDuration> DocumentTimeline::ToTimelineTime(
+ const TimeStamp& aTimeStamp) const {
+ Nullable<TimeDuration> result; // Initializes to null
+ if (aTimeStamp.IsNull()) {
+ return result;
+ }
+
+ nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
+ if (MOZ_UNLIKELY(!timing)) {
+ return result;
+ }
+
+ result.SetValue(aTimeStamp - timing->GetNavigationStartTimeStamp() -
+ mOriginTime);
+ return result;
+}
+
+void DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation) {
+ AnimationTimeline::NotifyAnimationUpdated(aAnimation);
+
+ if (!mIsObservingRefreshDriver && !mAnimationOrder.isEmpty()) {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (refreshDriver) {
+ MOZ_ASSERT(isInList(),
+ "We should not register with the refresh driver if we are not"
+ " in the document's list of timelines");
+
+ ObserveRefreshDriver(refreshDriver);
+ }
+ }
+}
+
+void DocumentTimeline::MostRecentRefreshTimeUpdated() {
+ MOZ_ASSERT(mIsObservingRefreshDriver);
+ MOZ_ASSERT(GetRefreshDriver(),
+ "Should be able to reach refresh driver from within WillRefresh");
+
+ nsAutoAnimationMutationBatch mb(mDocument);
+
+ bool ticked = Tick();
+ if (!ticked) {
+ // We already assert that GetRefreshDriver() is non-null at the beginning
+ // of this function but we check it again here to be sure that ticking
+ // animations does not have any side effects that cause us to lose the
+ // connection with the refresh driver, such as triggering the destruction
+ // of mDocument's PresShell.
+ MOZ_ASSERT(GetRefreshDriver(),
+ "Refresh driver should still be valid at end of WillRefresh");
+ UnregisterFromRefreshDriver();
+ }
+}
+
+void DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime) {
+ UpdateLastRefreshDriverTime();
+ MostRecentRefreshTimeUpdated();
+}
+
+void DocumentTimeline::NotifyTimerAdjusted(TimeStamp aTime) {
+ MostRecentRefreshTimeUpdated();
+}
+
+void DocumentTimeline::ObserveRefreshDriver(nsRefreshDriver* aDriver) {
+ MOZ_ASSERT(!mIsObservingRefreshDriver);
+ // Set the mIsObservingRefreshDriver flag before calling AddRefreshObserver
+ // since it might end up calling NotifyTimerAdjusted which calls
+ // MostRecentRefreshTimeUpdated which has an assertion for
+ // mIsObserveingRefreshDriver check.
+ mIsObservingRefreshDriver = true;
+ aDriver->AddRefreshObserver(this, FlushType::Style,
+ "DocumentTimeline animations");
+ aDriver->AddTimerAdjustmentObserver(this);
+}
+
+void DocumentTimeline::NotifyRefreshDriverCreated(nsRefreshDriver* aDriver) {
+ MOZ_ASSERT(!mIsObservingRefreshDriver,
+ "Timeline should not be observing the refresh driver before"
+ " it is created");
+
+ if (!mAnimationOrder.isEmpty()) {
+ MOZ_ASSERT(isInList(),
+ "We should not register with the refresh driver if we are not"
+ " in the document's list of timelines");
+ ObserveRefreshDriver(aDriver);
+ // Although we have started observing the refresh driver, it's possible we
+ // could perform a paint before the first refresh driver tick happens. To
+ // ensure we're in a consistent state in that case we run the first tick
+ // manually.
+ MostRecentRefreshTimeUpdated();
+ }
+}
+
+void DocumentTimeline::DisconnectRefreshDriver(nsRefreshDriver* aDriver) {
+ MOZ_ASSERT(mIsObservingRefreshDriver);
+
+ aDriver->RemoveRefreshObserver(this, FlushType::Style);
+ aDriver->RemoveTimerAdjustmentObserver(this);
+ mIsObservingRefreshDriver = false;
+}
+
+void DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver) {
+ if (!mIsObservingRefreshDriver) {
+ return;
+ }
+
+ DisconnectRefreshDriver(aDriver);
+}
+
+void DocumentTimeline::RemoveAnimation(Animation* aAnimation) {
+ AnimationTimeline::RemoveAnimation(aAnimation);
+
+ if (!mIsObservingRefreshDriver || !mAnimationOrder.isEmpty()) {
+ return;
+ }
+
+ UnregisterFromRefreshDriver();
+}
+
+void DocumentTimeline::NotifyAnimationContentVisibilityChanged(
+ Animation* aAnimation, bool aIsVisible) {
+ AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation,
+ aIsVisible);
+
+ if (mIsObservingRefreshDriver && mAnimationOrder.isEmpty()) {
+ UnregisterFromRefreshDriver();
+ }
+
+ if (!mIsObservingRefreshDriver && !mAnimationOrder.isEmpty()) {
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (refreshDriver) {
+ MOZ_ASSERT(isInList(),
+ "We should not register with the refresh driver if we are not"
+ " in the document's list of timelines");
+
+ ObserveRefreshDriver(refreshDriver);
+ }
+ }
+}
+
+TimeStamp DocumentTimeline::ToTimeStamp(
+ const TimeDuration& aTimeDuration) const {
+ TimeStamp result;
+ nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
+ if (MOZ_UNLIKELY(!timing)) {
+ return result;
+ }
+
+ result =
+ timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime);
+ return result;
+}
+
+nsRefreshDriver* DocumentTimeline::GetRefreshDriver() const {
+ nsPresContext* presContext = mDocument->GetPresContext();
+ if (MOZ_UNLIKELY(!presContext)) {
+ return nullptr;
+ }
+
+ return presContext->RefreshDriver();
+}
+
+void DocumentTimeline::UnregisterFromRefreshDriver() {
+ if (!mIsObservingRefreshDriver) {
+ return;
+ }
+
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (!refreshDriver) {
+ return;
+ }
+ DisconnectRefreshDriver(refreshDriver);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/DocumentTimeline.h b/dom/animation/DocumentTimeline.h
new file mode 100644
index 0000000000..b0c8c86546
--- /dev/null
+++ b/dom/animation/DocumentTimeline.h
@@ -0,0 +1,97 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_DocumentTimeline_h
+#define mozilla_dom_DocumentTimeline_h
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentTimelineBinding.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/TimeStamp.h"
+#include "AnimationTimeline.h"
+#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
+#include "nsRefreshDriver.h"
+#include "nsRefreshObservers.h"
+
+struct JSContext;
+
+namespace mozilla::dom {
+
+class DocumentTimeline final : public AnimationTimeline,
+ public nsARefreshObserver,
+ public nsATimerAdjustmentObserver,
+ public LinkedListElement<DocumentTimeline> {
+ public:
+ DocumentTimeline(Document* aDocument, const TimeDuration& aOriginTime);
+
+ protected:
+ virtual ~DocumentTimeline();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DocumentTimeline,
+ AnimationTimeline)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<DocumentTimeline> Constructor(
+ const GlobalObject& aGlobal, const DocumentTimelineOptions& aOptions,
+ ErrorResult& aRv);
+
+ // AnimationTimeline methods
+
+ // This is deliberately _not_ called GetCurrentTime since that would clash
+ // with a macro defined in winbase.h
+ virtual Nullable<TimeDuration> GetCurrentTimeAsDuration() const override;
+
+ bool TracksWallclockTime() const override;
+ Nullable<TimeDuration> ToTimelineTime(
+ const TimeStamp& aTimeStamp) const override;
+ TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override;
+
+ void NotifyAnimationUpdated(Animation& aAnimation) override;
+
+ void RemoveAnimation(Animation* aAnimation) override;
+ void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
+ bool aIsVisible) override;
+
+ // nsARefreshObserver methods
+ void WillRefresh(TimeStamp aTime) override;
+ // nsATimerAdjustmentObserver methods
+ void NotifyTimerAdjusted(TimeStamp aTime) override;
+
+ void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver);
+ void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver);
+
+ Document* GetDocument() const override { return mDocument; }
+
+ void UpdateLastRefreshDriverTime();
+
+ bool IsMonotonicallyIncreasing() const override { return true; }
+
+ protected:
+ TimeStamp GetCurrentTimeStamp() const;
+ nsRefreshDriver* GetRefreshDriver() const;
+ void UnregisterFromRefreshDriver();
+ void MostRecentRefreshTimeUpdated();
+ void ObserveRefreshDriver(nsRefreshDriver* aDriver);
+ void DisconnectRefreshDriver(nsRefreshDriver* aDriver);
+
+ RefPtr<Document> mDocument;
+
+ // The most recently used refresh driver time. This is used in cases where
+ // we don't have a refresh driver (e.g. because we are in a display:none
+ // iframe).
+ TimeStamp mLastRefreshDriverTime;
+ bool mIsObservingRefreshDriver;
+
+ TimeDuration mOriginTime;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_DocumentTimeline_h
diff --git a/dom/animation/EffectCompositor.cpp b/dom/animation/EffectCompositor.cpp
new file mode 100644
index 0000000000..095ec63baf
--- /dev/null
+++ b/dom/animation/EffectCompositor.cpp
@@ -0,0 +1,969 @@
+/* -*- 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 "EffectCompositor.h"
+
+#include <bitset>
+#include <initializer_list>
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/AnimationComparator.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/LayerAnimationInfo.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/RestyleManager.h"
+#include "mozilla/ServoBindings.h" // Servo_GetProperties_Overriding_Animation
+#include "mozilla/ServoStyleSet.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "nsContentUtils.h"
+#include "nsCSSPseudoElements.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h"
+#include "nsDisplayItemTypes.h"
+#include "nsAtom.h"
+#include "nsLayoutUtils.h"
+#include "nsTArray.h"
+#include "PendingAnimationTracker.h"
+
+using mozilla::dom::Animation;
+using mozilla::dom::Element;
+using mozilla::dom::KeyframeEffect;
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor)
+ for (auto& elementSet : tmp->mElementsToRestyle) {
+ elementSet.Clear();
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EffectCompositor)
+ for (const auto& elementSet : tmp->mElementsToRestyle) {
+ for (const auto& key : elementSet.Keys()) {
+ CycleCollectionNoteChild(cb, key.mElement,
+ "EffectCompositor::mElementsToRestyle[]",
+ cb.Flags());
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+/* static */
+bool EffectCompositor::AllowCompositorAnimationsOnFrame(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aWarning /* out */) {
+ if (aFrame->RefusedAsyncAnimation()) {
+ return false;
+ }
+
+ if (!nsLayoutUtils::AreAsyncAnimationsEnabled()) {
+ if (StaticPrefs::layers_offmainthreadcomposition_log_animations()) {
+ nsCString message;
+ message.AppendLiteral(
+ "Performance warning: Async animations are "
+ "disabled");
+ AnimationUtils::LogAsyncAnimationFailure(message);
+ }
+ return false;
+ }
+
+ // Disable async animations if we have a rendering observer that
+ // depends on our content (svg masking, -moz-element etc) so that
+ // it gets updated correctly.
+ nsIContent* content = aFrame->GetContent();
+ while (content) {
+ if (content->HasRenderingObservers()) {
+ aWarning = AnimationPerformanceWarning::Type::HasRenderingObserver;
+ return false;
+ }
+ content = content->GetParent();
+ }
+
+ return true;
+}
+
+// Helper function to factor out the common logic from
+// GetAnimationsForCompositor and HasAnimationsForCompositor.
+//
+// Takes an optional array to fill with eligible animations.
+//
+// Returns true if there are eligible animations, false otherwise.
+bool FindAnimationsForCompositor(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
+ nsTArray<RefPtr<dom::Animation>>* aMatches /*out*/) {
+ // Do not process any animations on the compositor when in print or print
+ // preview.
+ if (aFrame->PresContext()->IsPrintingOrPrintPreview()) {
+ return false;
+ }
+
+ MOZ_ASSERT(
+ aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_TRANSFORM)) ||
+ aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_OPACITY)) ||
+ aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_BACKGROUND_COLOR)),
+ "Should be the subset of transform-like properties, or opacity, "
+ "or background color");
+
+ MOZ_ASSERT(!aMatches || aMatches->IsEmpty(),
+ "Matches array, if provided, should be empty");
+
+ EffectSet* effects = EffectSet::GetForFrame(aFrame, aPropertySet);
+ if (!effects || effects->IsEmpty()) {
+ return false;
+ }
+
+ // First check for newly-started transform animations that should be
+ // synchronized with geometric animations. We need to do this before any
+ // other early returns (the one above is ok) since we can only check this
+ // state when the animation is newly-started.
+ if (aPropertySet.Intersects(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_TRANSFORM))) {
+ PendingAnimationTracker* tracker =
+ aFrame->PresContext()->Document()->GetPendingAnimationTracker();
+ if (tracker) {
+ tracker->MarkAnimationsThatMightNeedSynchronization();
+ }
+ }
+
+ AnimationPerformanceWarning::Type warning =
+ AnimationPerformanceWarning::Type::None;
+ if (!EffectCompositor::AllowCompositorAnimationsOnFrame(aFrame, warning)) {
+ if (warning != AnimationPerformanceWarning::Type::None) {
+ EffectCompositor::SetPerformanceWarning(
+ aFrame, aPropertySet, AnimationPerformanceWarning(warning));
+ }
+ return false;
+ }
+
+ // The animation cascade will almost always be up-to-date by this point
+ // but there are some cases such as when we are restoring the refresh driver
+ // from test control after seeking where it might not be the case.
+ //
+ // Those cases are probably not important but just to be safe, let's make
+ // sure the cascade is up to date since if it *is* up to date, this is
+ // basically a no-op.
+ Maybe<NonOwningAnimationTarget> pseudoElement =
+ EffectCompositor::GetAnimationElementAndPseudoForFrame(
+ nsLayoutUtils::GetStyleFrame(aFrame));
+ MOZ_ASSERT(pseudoElement,
+ "We have a valid element for the frame, if we don't we should "
+ "have bailed out at above the call to EffectSet::Get");
+ EffectCompositor::MaybeUpdateCascadeResults(pseudoElement->mElement,
+ pseudoElement->mPseudoType);
+
+ bool foundRunningAnimations = false;
+ for (KeyframeEffect* effect : *effects) {
+ AnimationPerformanceWarning::Type effectWarning =
+ AnimationPerformanceWarning::Type::None;
+ KeyframeEffect::MatchForCompositor matchResult =
+ effect->IsMatchForCompositor(aPropertySet, aFrame, *effects,
+ effectWarning);
+ if (effectWarning != AnimationPerformanceWarning::Type::None) {
+ EffectCompositor::SetPerformanceWarning(
+ aFrame, aPropertySet, AnimationPerformanceWarning(effectWarning));
+ }
+
+ if (matchResult ==
+ KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty) {
+ // For a given |aFrame|, we don't want some animations of |aPropertySet|
+ // 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.
+ if (aMatches) {
+ aMatches->Clear();
+ }
+ return false;
+ }
+
+ if (matchResult == KeyframeEffect::MatchForCompositor::No) {
+ continue;
+ }
+
+ if (aMatches) {
+ aMatches->AppendElement(effect->GetAnimation());
+ }
+
+ if (matchResult == KeyframeEffect::MatchForCompositor::Yes) {
+ foundRunningAnimations = true;
+ }
+ }
+
+ // If all animations we added were not currently playing animations, don't
+ // send them to the compositor.
+ if (aMatches && !foundRunningAnimations) {
+ aMatches->Clear();
+ }
+
+ MOZ_ASSERT(!foundRunningAnimations || !aMatches || !aMatches->IsEmpty(),
+ "If return value is true, matches array should be non-empty");
+
+ if (aMatches && foundRunningAnimations) {
+ aMatches->Sort(AnimationPtrComparator<RefPtr<dom::Animation>>());
+ }
+
+ return foundRunningAnimations;
+}
+
+void EffectCompositor::RequestRestyle(dom::Element* aElement,
+ PseudoStyleType aPseudoType,
+ RestyleType aRestyleType,
+ CascadeLevel aCascadeLevel) {
+ if (!mPresContext) {
+ // Pres context will be null after the effect compositor is disconnected.
+ return;
+ }
+
+ // Ignore animations on orphaned elements and elements in documents without
+ // a pres shell (e.g. XMLHttpRequest responseXML documents).
+ if (!nsContentUtils::GetPresShellForContent(aElement)) {
+ return;
+ }
+
+ auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+ PseudoElementHashEntry::KeyType key = {aElement, aPseudoType};
+
+ bool& restyleEntry = elementsToRestyle.LookupOrInsert(key, false);
+ if (aRestyleType == RestyleType::Throttled) {
+ mPresContext->PresShell()->SetNeedThrottledAnimationFlush();
+ } else {
+ // Update hashtable first in case PostRestyleForAnimation mutates it
+ // and invalidates the restyleEntry reference.
+ // (It shouldn't, but just to be sure.)
+ bool skipRestyle = std::exchange(restyleEntry, true);
+ if (!skipRestyle) {
+ PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel);
+ }
+ }
+
+ if (aRestyleType == RestyleType::Layer) {
+ mPresContext->RestyleManager()->IncrementAnimationGeneration();
+ if (auto* effectSet = EffectSet::Get(aElement, aPseudoType)) {
+ effectSet->UpdateAnimationGeneration(mPresContext);
+ }
+ }
+}
+
+void EffectCompositor::PostRestyleForAnimation(dom::Element* aElement,
+ PseudoStyleType aPseudoType,
+ CascadeLevel aCascadeLevel) {
+ if (!mPresContext) {
+ return;
+ }
+
+ // FIXME: Bug 1615083 KeyframeEffect::SetTarget() and
+ // KeyframeEffect::SetPseudoElement() may set a non-existing pseudo element,
+ // and we still have to update its style, based on the wpt. However, we don't
+ // have the generated element here, so we failed the wpt.
+ //
+ // See wpt for more info: web-animations/interfaces/KeyframeEffect/target.html
+ Element* element =
+ AnimationUtils::GetElementForRestyle(aElement, aPseudoType);
+ if (!element) {
+ return;
+ }
+
+ RestyleHint hint = aCascadeLevel == CascadeLevel::Transitions
+ ? RestyleHint::RESTYLE_CSS_TRANSITIONS
+ : RestyleHint::RESTYLE_CSS_ANIMATIONS;
+
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Restyle request during restyling should be requested only on "
+ "the main-thread. e.g. after the parallel traversal");
+ if (ServoStyleSet::IsInServoTraversal() || mIsInPreTraverse) {
+ MOZ_ASSERT(hint == RestyleHint::RESTYLE_CSS_ANIMATIONS ||
+ hint == RestyleHint::RESTYLE_CSS_TRANSITIONS);
+
+ // We can't call Servo_NoteExplicitHints here since AtomicRefCell does not
+ // allow us mutate ElementData of the |aElement| in SequentialTask.
+ // Instead we call Servo_NoteExplicitHints for the element in PreTraverse()
+ // which will be called right before the second traversal that we do for
+ // updating CSS animations.
+ // In that case PreTraverse() will return true so that we know to do the
+ // second traversal so we don't need to post any restyle requests to the
+ // PresShell.
+ return;
+ }
+
+ MOZ_ASSERT(!mPresContext->RestyleManager()->IsInStyleRefresh());
+
+ mPresContext->PresShell()->RestyleForAnimation(element, hint);
+}
+
+void EffectCompositor::PostRestyleForThrottledAnimations() {
+ for (size_t i = 0; i < kCascadeLevelCount; i++) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ auto& elementSet = mElementsToRestyle[cascadeLevel];
+
+ for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+ bool& postedRestyle = iter.Data();
+ if (postedRestyle) {
+ continue;
+ }
+
+ PostRestyleForAnimation(iter.Key().mElement, iter.Key().mPseudoType,
+ cascadeLevel);
+ postedRestyle = true;
+ }
+ }
+}
+
+void EffectCompositor::ClearRestyleRequestsFor(Element* aElement) {
+ MOZ_ASSERT(aElement);
+
+ auto& elementsToRestyle = mElementsToRestyle[CascadeLevel::Animations];
+
+ PseudoStyleType pseudoType = aElement->GetPseudoElementType();
+ if (pseudoType == PseudoStyleType::NotPseudo) {
+ PseudoElementHashEntry::KeyType notPseudoKey = {aElement,
+ PseudoStyleType::NotPseudo};
+ PseudoElementHashEntry::KeyType beforePseudoKey = {aElement,
+ PseudoStyleType::before};
+ PseudoElementHashEntry::KeyType afterPseudoKey = {aElement,
+ PseudoStyleType::after};
+ PseudoElementHashEntry::KeyType markerPseudoKey = {aElement,
+ PseudoStyleType::marker};
+
+ elementsToRestyle.Remove(notPseudoKey);
+ elementsToRestyle.Remove(beforePseudoKey);
+ elementsToRestyle.Remove(afterPseudoKey);
+ elementsToRestyle.Remove(markerPseudoKey);
+ } else if (AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) {
+ Element* parentElement = aElement->GetParentElement();
+ MOZ_ASSERT(parentElement);
+ PseudoElementHashEntry::KeyType key = {parentElement, pseudoType};
+ elementsToRestyle.Remove(key);
+ }
+}
+
+void EffectCompositor::UpdateEffectProperties(const ComputedStyle* aStyle,
+ Element* aElement,
+ PseudoStyleType aPseudoType) {
+ EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType);
+ if (!effectSet) {
+ return;
+ }
+
+ // Style context (Gecko) or computed values (Stylo) change might cause CSS
+ // cascade level, e.g removing !important, so we should update the cascading
+ // result.
+ effectSet->MarkCascadeNeedsUpdate();
+
+ for (KeyframeEffect* effect : *effectSet) {
+ effect->UpdateProperties(aStyle);
+ }
+}
+
+namespace {
+class EffectCompositeOrderComparator {
+ public:
+ bool Equals(const KeyframeEffect* a, const KeyframeEffect* b) const {
+ return a == b;
+ }
+
+ bool LessThan(const KeyframeEffect* a, const KeyframeEffect* b) const {
+ MOZ_ASSERT(a->GetAnimation() && b->GetAnimation());
+ MOZ_ASSERT(
+ Equals(a, b) ||
+ a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) !=
+ b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation()));
+ return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation());
+ }
+};
+} // namespace
+
+static void ComposeSortedEffects(
+ const nsTArray<KeyframeEffect*>& aSortedEffects,
+ const EffectSet* aEffectSet, EffectCompositor::CascadeLevel aCascadeLevel,
+ StyleAnimationValueMap* aAnimationValues) {
+ const bool isTransition =
+ aCascadeLevel == EffectCompositor::CascadeLevel::Transitions;
+ nsCSSPropertyIDSet propertiesToSkip;
+ // Transitions should be overridden by running animations of the same
+ // property per https://drafts.csswg.org/css-transitions/#application:
+ //
+ // > Implementations must add this value to the cascade if and only if that
+ // > property is not currently undergoing a CSS Animation on the same element.
+ //
+ // FIXME(emilio, bug 1606176): This should assert that
+ // aEffectSet->PropertiesForAnimationsLevel() is up-to-date, and it may not
+ // follow the spec in those cases. There are various places where we get style
+ // without flushing that would trigger the below assertion.
+ //
+ // MOZ_ASSERT_IF(aEffectSet, !aEffectSet->CascadeNeedsUpdate());
+ if (aEffectSet) {
+ propertiesToSkip =
+ isTransition ? aEffectSet->PropertiesForAnimationsLevel()
+ : aEffectSet->PropertiesForAnimationsLevel().Inverse();
+ }
+
+ for (KeyframeEffect* effect : aSortedEffects) {
+ auto* animation = effect->GetAnimation();
+ MOZ_ASSERT(!isTransition || animation->CascadeLevel() == aCascadeLevel);
+ animation->ComposeStyle(*aAnimationValues, propertiesToSkip);
+ }
+}
+
+bool EffectCompositor::GetServoAnimationRule(
+ const dom::Element* aElement, PseudoStyleType aPseudoType,
+ CascadeLevel aCascadeLevel, StyleAnimationValueMap* aAnimationValues) {
+ MOZ_ASSERT(aAnimationValues);
+ // Gecko_GetAnimationRule should have already checked this
+ MOZ_ASSERT(nsContentUtils::GetPresShellForContent(aElement),
+ "Should not be trying to run animations on elements in documents"
+ " without a pres shell (e.g. XMLHttpRequest documents)");
+
+ EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType);
+ if (!effectSet) {
+ return false;
+ }
+
+ const bool isTransition = aCascadeLevel == CascadeLevel::Transitions;
+
+ // Get a list of effects sorted by composite order.
+ nsTArray<KeyframeEffect*> sortedEffectList(effectSet->Count());
+ for (KeyframeEffect* effect : *effectSet) {
+ if (isTransition &&
+ effect->GetAnimation()->CascadeLevel() != aCascadeLevel) {
+ // We may need to use transition rules for the animations level for the
+ // case of missing keyframes in animations, but we don't ever need to look
+ // at non-transition levels to build a transition rule. When the effect
+ // set information is out of date (see above), this avoids creating bogus
+ // transition rules, see bug 1605610.
+ continue;
+ }
+ sortedEffectList.AppendElement(effect);
+ }
+
+ if (sortedEffectList.IsEmpty()) {
+ return false;
+ }
+
+ sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+ ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel,
+ aAnimationValues);
+
+ MOZ_ASSERT(effectSet == EffectSet::Get(aElement, aPseudoType),
+ "EffectSet should not change while composing style");
+
+ return true;
+}
+
+bool EffectCompositor::ComposeServoAnimationRuleForEffect(
+ KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
+ StyleAnimationValueMap* aAnimationValues) {
+ MOZ_ASSERT(aAnimationValues);
+ MOZ_ASSERT(mPresContext && mPresContext->IsDynamic(),
+ "Should not be in print preview");
+
+ NonOwningAnimationTarget target = aEffect.GetAnimationTarget();
+ if (!target) {
+ return false;
+ }
+
+ // Don't try to compose animations for elements in documents without a pres
+ // shell (e.g. XMLHttpRequest documents).
+ if (!nsContentUtils::GetPresShellForContent(target.mElement)) {
+ return false;
+ }
+
+ // GetServoAnimationRule is called as part of the regular style resolution
+ // where the cascade results are updated in the pre-traversal as needed.
+ // This function, however, is only called when committing styles so we
+ // need to ensure the cascade results are up-to-date manually.
+ MaybeUpdateCascadeResults(target.mElement, target.mPseudoType);
+
+ EffectSet* effectSet = EffectSet::Get(target.mElement, target.mPseudoType);
+
+ // Get a list of effects sorted by composite order up to and including
+ // |aEffect|, even if it is not in the EffectSet.
+ auto comparator = EffectCompositeOrderComparator();
+ nsTArray<KeyframeEffect*> sortedEffectList(effectSet ? effectSet->Count() + 1
+ : 1);
+ if (effectSet) {
+ for (KeyframeEffect* effect : *effectSet) {
+ if (comparator.LessThan(effect, &aEffect)) {
+ sortedEffectList.AppendElement(effect);
+ }
+ }
+ sortedEffectList.Sort(comparator);
+ }
+ sortedEffectList.AppendElement(&aEffect);
+
+ ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel,
+ aAnimationValues);
+
+ MOZ_ASSERT(effectSet == EffectSet::Get(target.mElement, target.mPseudoType),
+ "EffectSet should not change while composing style");
+
+ return true;
+}
+
+bool EffectCompositor::HasPendingStyleUpdates() const {
+ for (auto& elementSet : mElementsToRestyle) {
+ if (elementSet.Count()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* static */
+bool EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame,
+ DisplayItemType aType) {
+ return FindAnimationsForCompositor(
+ aFrame, LayerAnimationInfo::GetCSSPropertiesFor(aType), nullptr);
+}
+
+/* static */
+nsTArray<RefPtr<dom::Animation>> EffectCompositor::GetAnimationsForCompositor(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet) {
+ nsTArray<RefPtr<dom::Animation>> result;
+
+#ifdef DEBUG
+ bool foundSome =
+#endif
+ FindAnimationsForCompositor(aFrame, aPropertySet, &result);
+ MOZ_ASSERT(!foundSome || !result.IsEmpty(),
+ "If return value is true, matches array should be non-empty");
+
+ return result;
+}
+
+/* static */
+void EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame* aFrame,
+ DisplayItemType aType) {
+ EffectSet* effects = EffectSet::GetForFrame(aFrame, aType);
+ if (!effects) {
+ return;
+ }
+
+ const nsCSSPropertyIDSet& propertySet =
+ LayerAnimationInfo::GetCSSPropertiesFor(aType);
+ for (KeyframeEffect* effect : *effects) {
+ effect->SetIsRunningOnCompositor(propertySet, false);
+ }
+}
+
+/* static */
+void EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
+ PseudoStyleType aPseudoType) {
+ EffectSet* effects = EffectSet::Get(aElement, aPseudoType);
+ if (!effects || !effects->CascadeNeedsUpdate()) {
+ return;
+ }
+
+ UpdateCascadeResults(*effects, aElement, aPseudoType);
+
+ MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state");
+}
+
+/* static */
+Maybe<NonOwningAnimationTarget>
+EffectCompositor::GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame) {
+ // Always return the same object to benefit from return-value optimization.
+ Maybe<NonOwningAnimationTarget> result;
+
+ PseudoStyleType pseudoType = aFrame->Style()->GetPseudoType();
+
+ if (pseudoType != PseudoStyleType::NotPseudo &&
+ !AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) {
+ return result;
+ }
+
+ nsIContent* content = aFrame->GetContent();
+ if (!content) {
+ return result;
+ }
+
+ if (AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) {
+ content = content->GetParent();
+ if (!content) {
+ return result;
+ }
+ }
+
+ if (!content->IsElement()) {
+ return result;
+ }
+
+ result.emplace(content->AsElement(), pseudoType);
+
+ return result;
+}
+
+/* static */
+nsCSSPropertyIDSet EffectCompositor::GetOverriddenProperties(
+ EffectSet& aEffectSet, Element* aElement, PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(aElement, "Should have an element to get style data from");
+
+ nsCSSPropertyIDSet result;
+
+ Element* elementForRestyle =
+ AnimationUtils::GetElementForRestyle(aElement, aPseudoType);
+ if (!elementForRestyle) {
+ return result;
+ }
+
+ static constexpr size_t compositorAnimatableCount =
+ nsCSSPropertyIDSet::CompositorAnimatableCount();
+ AutoTArray<nsCSSPropertyID, compositorAnimatableCount> propertiesToTrack;
+ {
+ nsCSSPropertyIDSet propertiesToTrackAsSet;
+ for (KeyframeEffect* effect : aEffectSet) {
+ for (const AnimationProperty& property : effect->Properties()) {
+ if (nsCSSProps::PropHasFlags(property.mProperty,
+ CSSPropFlags::CanAnimateOnCompositor) &&
+ !propertiesToTrackAsSet.HasProperty(property.mProperty)) {
+ propertiesToTrackAsSet.AddProperty(property.mProperty);
+ propertiesToTrack.AppendElement(property.mProperty);
+ }
+ }
+ // Skip iterating over the rest of the effects if we've already
+ // found all the compositor-animatable properties.
+ if (propertiesToTrack.Length() == compositorAnimatableCount) {
+ break;
+ }
+ }
+ }
+
+ if (propertiesToTrack.IsEmpty()) {
+ return result;
+ }
+
+ Servo_GetProperties_Overriding_Animation(elementForRestyle,
+ &propertiesToTrack, &result);
+ return result;
+}
+
+/* static */
+void EffectCompositor::UpdateCascadeResults(EffectSet& aEffectSet,
+ Element* aElement,
+ PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(EffectSet::Get(aElement, aPseudoType) == &aEffectSet,
+ "Effect set should correspond to the specified (pseudo-)element");
+ if (aEffectSet.IsEmpty()) {
+ aEffectSet.MarkCascadeUpdated();
+ return;
+ }
+
+ // Get a list of effects sorted by composite order.
+ nsTArray<KeyframeEffect*> sortedEffectList(aEffectSet.Count());
+ for (KeyframeEffect* effect : aEffectSet) {
+ sortedEffectList.AppendElement(effect);
+ }
+ sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+ // Get properties that override the *animations* level of the cascade.
+ //
+ // We only do this for properties that we can animate on the compositor
+ // since we will apply other properties on the main thread where the usual
+ // cascade applies.
+ nsCSSPropertyIDSet overriddenProperties =
+ GetOverriddenProperties(aEffectSet, aElement, aPseudoType);
+
+ nsCSSPropertyIDSet& propertiesWithImportantRules =
+ aEffectSet.PropertiesWithImportantRules();
+ nsCSSPropertyIDSet& propertiesForAnimationsLevel =
+ aEffectSet.PropertiesForAnimationsLevel();
+
+ static constexpr nsCSSPropertyIDSet compositorAnimatables =
+ nsCSSPropertyIDSet::CompositorAnimatables();
+ // Record which compositor-animatable properties were originally set so we can
+ // compare for changes later.
+ nsCSSPropertyIDSet prevCompositorPropertiesWithImportantRules =
+ propertiesWithImportantRules.Intersect(compositorAnimatables);
+
+ nsCSSPropertyIDSet prevPropertiesForAnimationsLevel =
+ propertiesForAnimationsLevel;
+
+ propertiesWithImportantRules.Empty();
+ propertiesForAnimationsLevel.Empty();
+
+ nsCSSPropertyIDSet propertiesForTransitionsLevel;
+
+ for (const KeyframeEffect* effect : sortedEffectList) {
+ MOZ_ASSERT(effect->GetAnimation(),
+ "Effects on a target element should have an Animation");
+ CascadeLevel cascadeLevel = effect->GetAnimation()->CascadeLevel();
+
+ for (const AnimationProperty& prop : effect->Properties()) {
+ if (overriddenProperties.HasProperty(prop.mProperty)) {
+ propertiesWithImportantRules.AddProperty(prop.mProperty);
+ }
+
+ switch (cascadeLevel) {
+ case EffectCompositor::CascadeLevel::Animations:
+ propertiesForAnimationsLevel.AddProperty(prop.mProperty);
+ break;
+ case EffectCompositor::CascadeLevel::Transitions:
+ propertiesForTransitionsLevel.AddProperty(prop.mProperty);
+ break;
+ }
+ }
+ }
+
+ aEffectSet.MarkCascadeUpdated();
+
+ nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
+ if (!presContext) {
+ return;
+ }
+
+ // If properties for compositor are newly overridden by !important rules, or
+ // released from being overridden by !important rules, we need to update
+ // layers for animations level because it's a trigger to send animations to
+ // the compositor or pull animations back from the compositor.
+ if (!prevCompositorPropertiesWithImportantRules.Equals(
+ propertiesWithImportantRules.Intersect(compositorAnimatables))) {
+ presContext->EffectCompositor()->RequestRestyle(
+ aElement, aPseudoType, EffectCompositor::RestyleType::Layer,
+ EffectCompositor::CascadeLevel::Animations);
+ }
+
+ // If we have transition properties and if the same propery for animations
+ // level is newly added or removed, we need to update the transition level
+ // rule since the it will be added/removed from the rule tree.
+ nsCSSPropertyIDSet changedPropertiesForAnimationLevel =
+ prevPropertiesForAnimationsLevel.Xor(propertiesForAnimationsLevel);
+ nsCSSPropertyIDSet commonProperties = propertiesForTransitionsLevel.Intersect(
+ changedPropertiesForAnimationLevel);
+ if (!commonProperties.IsEmpty()) {
+ EffectCompositor::RestyleType restyleType =
+ changedPropertiesForAnimationLevel.Intersects(compositorAnimatables)
+ ? EffectCompositor::RestyleType::Standard
+ : EffectCompositor::RestyleType::Layer;
+ presContext->EffectCompositor()->RequestRestyle(
+ aElement, aPseudoType, restyleType,
+ EffectCompositor::CascadeLevel::Transitions);
+ }
+}
+
+/* static */
+void EffectCompositor::SetPerformanceWarning(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
+ const AnimationPerformanceWarning& aWarning) {
+ EffectSet* effects = EffectSet::GetForFrame(aFrame, aPropertySet);
+ if (!effects) {
+ return;
+ }
+
+ for (KeyframeEffect* effect : *effects) {
+ effect->SetPerformanceWarning(aPropertySet, aWarning);
+ }
+}
+
+bool EffectCompositor::PreTraverse(ServoTraversalFlags aFlags) {
+ return PreTraverseInSubtree(aFlags, nullptr);
+}
+
+bool EffectCompositor::PreTraverseInSubtree(ServoTraversalFlags aFlags,
+ Element* aRoot) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aRoot || nsContentUtils::GetPresShellForContent(aRoot),
+ "Traversal root, if provided, should be bound to a display "
+ "document");
+
+ // Convert the root element to the parent element if the root element is
+ // pseudo since we check each element in mElementsToRestyle is in the subtree
+ // of the root element later in this function, but for pseudo elements the
+ // element in mElementsToRestyle is the parent of the pseudo.
+ if (aRoot && (aRoot->IsGeneratedContentContainerForBefore() ||
+ aRoot->IsGeneratedContentContainerForAfter() ||
+ aRoot->IsGeneratedContentContainerForMarker())) {
+ aRoot = aRoot->GetParentElement();
+ }
+
+ AutoRestore<bool> guard(mIsInPreTraverse);
+ mIsInPreTraverse = true;
+
+ // We need to force flush all throttled animations if we also have
+ // non-animation restyles (since we'll want the up-to-date animation style
+ // when we go to process them so we can trigger transitions correctly), and
+ // if we are currently flushing all throttled animation restyles.
+ bool flushThrottledRestyles =
+ (aRoot && aRoot->HasDirtyDescendantsForServo()) ||
+ (aFlags & ServoTraversalFlags::FlushThrottledAnimations);
+
+ using ElementsToRestyleIterType =
+ nsTHashMap<PseudoElementHashEntry, bool>::ConstIterator;
+ auto getNeededRestyleTarget =
+ [&](const ElementsToRestyleIterType& aIter) -> NonOwningAnimationTarget {
+ NonOwningAnimationTarget returnTarget;
+
+ // If aIter.Data() is false, the element only requested a throttled
+ // (skippable) restyle, so we can skip it if flushThrottledRestyles is not
+ // true.
+ if (!flushThrottledRestyles && !aIter.Data()) {
+ return returnTarget;
+ }
+
+ const NonOwningAnimationTarget& target = aIter.Key();
+
+ // Skip elements in documents without a pres shell. Normally we filter out
+ // such elements in RequestRestyle but it can happen that, after adding
+ // them to mElementsToRestyle, they are transferred to a different document.
+ //
+ // We will drop them from mElementsToRestyle at the end of the next full
+ // document restyle (at the end of this function) but for consistency with
+ // how we treat such elements in RequestRestyle, we just ignore them here.
+ if (!nsContentUtils::GetPresShellForContent(target.mElement)) {
+ return returnTarget;
+ }
+
+ // Ignore restyles that aren't in the flattened tree subtree rooted at
+ // aRoot.
+ if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOfForStyle(
+ target.mElement, aRoot)) {
+ return returnTarget;
+ }
+
+ returnTarget = target;
+ return returnTarget;
+ };
+
+ bool foundElementsNeedingRestyle = false;
+
+ nsTArray<NonOwningAnimationTarget> elementsWithCascadeUpdates;
+ for (size_t i = 0; i < kCascadeLevelCount; ++i) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ auto& elementSet = mElementsToRestyle[cascadeLevel];
+ for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) {
+ const NonOwningAnimationTarget& target = getNeededRestyleTarget(iter);
+ if (!target.mElement) {
+ continue;
+ }
+
+ EffectSet* effects = EffectSet::Get(target.mElement, target.mPseudoType);
+ if (!effects || !effects->CascadeNeedsUpdate()) {
+ continue;
+ }
+
+ elementsWithCascadeUpdates.AppendElement(target);
+ }
+ }
+
+ for (const NonOwningAnimationTarget& target : elementsWithCascadeUpdates) {
+ MaybeUpdateCascadeResults(target.mElement, target.mPseudoType);
+ }
+ elementsWithCascadeUpdates.Clear();
+
+ for (size_t i = 0; i < kCascadeLevelCount; ++i) {
+ CascadeLevel cascadeLevel = CascadeLevel(i);
+ auto& elementSet = mElementsToRestyle[cascadeLevel];
+ for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+ const NonOwningAnimationTarget& target = getNeededRestyleTarget(iter);
+ if (!target.mElement) {
+ continue;
+ }
+
+ if (target.mElement->GetComposedDoc() != mPresContext->Document()) {
+ iter.Remove();
+ continue;
+ }
+
+ // We need to post restyle hints even if the target is not in EffectSet to
+ // ensure the final restyling for removed animations.
+ // We can't call PostRestyleEvent directly here since we are still in the
+ // middle of the servo traversal.
+ mPresContext->RestyleManager()->PostRestyleEventForAnimations(
+ target.mElement, target.mPseudoType,
+ cascadeLevel == CascadeLevel::Transitions
+ ? RestyleHint::RESTYLE_CSS_TRANSITIONS
+ : RestyleHint::RESTYLE_CSS_ANIMATIONS);
+
+ foundElementsNeedingRestyle = true;
+
+ auto* effects = EffectSet::Get(target.mElement, target.mPseudoType);
+ if (!effects) {
+ // Drop EffectSets that have been destroyed.
+ iter.Remove();
+ continue;
+ }
+
+ for (KeyframeEffect* effect : *effects) {
+ effect->GetAnimation()->WillComposeStyle();
+ }
+
+ // Remove the element from the list of elements to restyle since we are
+ // about to restyle it.
+ iter.Remove();
+ }
+
+ // If this is a full document restyle, then unconditionally clear
+ // elementSet in case there are any elements that didn't match above
+ // because they were moved to a document without a pres shell after
+ // posting an animation restyle.
+ if (!aRoot && flushThrottledRestyles) {
+ elementSet.Clear();
+ }
+ }
+
+ return foundElementsNeedingRestyle;
+}
+
+void EffectCompositor::NoteElementForReducing(
+ const NonOwningAnimationTarget& aTarget) {
+ if (!StaticPrefs::dom_animations_api_autoremove_enabled()) {
+ return;
+ }
+
+ Unused << mElementsToReduce.put(
+ OwningAnimationTarget{aTarget.mElement, aTarget.mPseudoType});
+}
+
+static void ReduceEffectSet(EffectSet& aEffectSet) {
+ // Get a list of effects sorted by composite order.
+ nsTArray<KeyframeEffect*> sortedEffectList(aEffectSet.Count());
+ for (KeyframeEffect* effect : aEffectSet) {
+ sortedEffectList.AppendElement(effect);
+ }
+ sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+ nsCSSPropertyIDSet setProperties;
+
+ // Iterate in reverse
+ for (auto iter = sortedEffectList.rbegin(); iter != sortedEffectList.rend();
+ ++iter) {
+ MOZ_ASSERT(*iter && (*iter)->GetAnimation(),
+ "Effect in an EffectSet should have an animation");
+ KeyframeEffect& effect = **iter;
+ Animation& animation = *effect.GetAnimation();
+ if (animation.IsRemovable() &&
+ effect.GetPropertySet().IsSubsetOf(setProperties)) {
+ animation.Remove();
+ } else if (animation.IsReplaceable()) {
+ setProperties |= effect.GetPropertySet();
+ }
+ }
+}
+
+void EffectCompositor::ReduceAnimations() {
+ for (auto iter = mElementsToReduce.iter(); !iter.done(); iter.next()) {
+ const OwningAnimationTarget& target = iter.get();
+ auto* effectSet = EffectSet::Get(target.mElement, target.mPseudoType);
+ if (effectSet) {
+ ReduceEffectSet(*effectSet);
+ }
+ }
+
+ mElementsToReduce.clear();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/EffectCompositor.h b/dom/animation/EffectCompositor.h
new file mode 100644
index 0000000000..34c2688486
--- /dev/null
+++ b/dom/animation/EffectCompositor.h
@@ -0,0 +1,258 @@
+/* -*- 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/. */
+
+#ifndef mozilla_EffectCompositor_h
+#define mozilla_EffectCompositor_h
+
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/PseudoElementHashEntry.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ServoTypes.h"
+#include "nsCSSPropertyID.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashMap.h"
+#include "nsTArray.h"
+
+class nsCSSPropertyIDSet;
+class nsAtom;
+class nsIFrame;
+class nsPresContext;
+enum class DisplayItemType : uint8_t;
+
+namespace mozilla {
+
+class ComputedStyle;
+class EffectSet;
+class RestyleTracker;
+struct StyleAnimationValue;
+struct StyleAnimationValueMap;
+struct AnimationProperty;
+struct NonOwningAnimationTarget;
+
+namespace dom {
+class Animation;
+class Element;
+class KeyframeEffect;
+} // namespace dom
+
+class EffectCompositor {
+ public:
+ explicit EffectCompositor(nsPresContext* aPresContext)
+ : mPresContext(aPresContext) {}
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EffectCompositor)
+
+ void Disconnect() { mPresContext = nullptr; }
+
+ // Animations can be applied at two different levels in the CSS cascade:
+ enum class CascadeLevel : uint32_t {
+ // The animations sheet (CSS animations, script-generated animations,
+ // and CSS transitions that are no longer tied to CSS markup)
+ Animations = 0,
+ // The transitions sheet (CSS transitions that are tied to CSS markup)
+ Transitions = 1
+ };
+ // We don't define this as part of CascadeLevel as then we'd have to add
+ // explicit checks for the Count enum value everywhere CascadeLevel is used.
+ static const size_t kCascadeLevelCount =
+ static_cast<size_t>(CascadeLevel::Transitions) + 1;
+
+ // NOTE: This can return null after Disconnect().
+ nsPresContext* PresContext() const { return mPresContext; }
+
+ enum class RestyleType {
+ // Animation style has changed but the compositor is applying the same
+ // change so we might be able to defer updating the main thread until it
+ // becomes necessary.
+ Throttled,
+ // Animation style has changed and needs to be updated on the main thread.
+ Standard,
+ // Animation style has changed and needs to be updated on the main thread
+ // as well as forcing animations on layers to be updated.
+ // This is needed in cases such as when an animation becomes paused or has
+ // its playback rate changed. In such cases, although the computed style
+ // and refresh driver time might not change, we still need to ensure the
+ // corresponding animations on layers are updated to reflect the new
+ // configuration of the animation.
+ Layer
+ };
+
+ // Notifies the compositor that the animation rule for the specified
+ // (pseudo-)element at the specified cascade level needs to be updated.
+ // The specified steps taken to update the animation rule depend on
+ // |aRestyleType| whose values are described above.
+ void RequestRestyle(dom::Element* aElement, PseudoStyleType aPseudoType,
+ RestyleType aRestyleType, CascadeLevel aCascadeLevel);
+
+ // Schedule an animation restyle. This is called automatically by
+ // RequestRestyle when necessary. However, it is exposed here since we also
+ // need to perform this step when triggering transitions *without* also
+ // invalidating the animation style rule (which RequestRestyle would do).
+ void PostRestyleForAnimation(dom::Element* aElement,
+ PseudoStyleType aPseudoType,
+ CascadeLevel aCascadeLevel);
+
+ // Posts an animation restyle for any elements whose animation style rule
+ // is out of date but for which an animation restyle has not yet been
+ // posted because updates on the main thread are throttled.
+ void PostRestyleForThrottledAnimations();
+
+ // Clear all pending restyle requests for the given (pseudo-) element (and its
+ // ::before, ::after and ::marker elements if the given element is not
+ // pseudo).
+ void ClearRestyleRequestsFor(dom::Element* aElement);
+
+ // Called when computed style on the specified (pseudo-) element might
+ // have changed so that any context-sensitive values stored within
+ // animation effects (e.g. em-based endpoints used in keyframe effects)
+ // can be re-resolved to computed values.
+ void UpdateEffectProperties(const ComputedStyle* aStyle,
+ dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ // Get the animation rule for the appropriate level of the cascade for
+ // a (pseudo-)element. Called from the Servo side.
+ //
+ // The animation rule is stored in |StyleAnimationValueMap|.
+ // We need to be careful while doing any modification because it may cause
+ // some thread-safe issues.
+ bool GetServoAnimationRule(const dom::Element* aElement,
+ PseudoStyleType aPseudoType,
+ CascadeLevel aCascadeLevel,
+ StyleAnimationValueMap* aAnimationValues);
+
+ // A variant on GetServoAnimationRule that composes all the effects for an
+ // element up to and including |aEffect|.
+ //
+ // Note that |aEffect| might not be in the EffectSet since we can use this for
+ // committing the computed style of a removed Animation.
+ bool ComposeServoAnimationRuleForEffect(
+ dom::KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
+ StyleAnimationValueMap* aAnimationValues);
+
+ bool HasPendingStyleUpdates() const;
+
+ static bool HasAnimationsForCompositor(const nsIFrame* aFrame,
+ DisplayItemType aType);
+
+ static nsTArray<RefPtr<dom::Animation>> GetAnimationsForCompositor(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet);
+
+ static void ClearIsRunningOnCompositor(const nsIFrame* aFrame,
+ DisplayItemType aType);
+
+ // Update animation cascade results for the specified (pseudo-)element
+ // but only if we have marked the cascade as needing an update due a
+ // the change in the set of effects or a change in one of the effects'
+ // "in effect" state.
+ //
+ // This method does NOT detect if other styles that apply above the
+ // animation level of the cascade have changed.
+ static void MaybeUpdateCascadeResults(dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ // Update the mPropertiesWithImportantRules and
+ // mPropertiesForAnimationsLevel members of the given EffectSet, and also
+ // request any restyles required by changes to the cascade result.
+ //
+ // NOTE: This can be expensive so we should only call it if styles that apply
+ // above the animation level of the cascade might have changed. For all
+ // other cases we should call MaybeUpdateCascadeResults.
+ //
+ // This is typically reserved for internal callers but is public here since
+ // when we detect changes to the cascade on the Servo side we can't call
+ // MarkCascadeNeedsUpdate during the traversal so instead we call this as part
+ // of a follow-up sequential task.
+ static void UpdateCascadeResults(EffectSet& aEffectSet,
+ dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ // Helper to fetch the corresponding element and pseudo-type from a frame.
+ //
+ // For frames corresponding to pseudo-elements, the returned element is the
+ // element on which we store the animations (i.e. the EffectSet and/or
+ // AnimationCollection), *not* the generated content.
+ //
+ // For display:table content, which maintains a distinction between primary
+ // frame (table wrapper frame) and style frame (inner table frame), animations
+ // are stored on the content associated with the _style_ frame even though
+ // some (particularly transform-like animations) may be applied to the
+ // _primary_ frame. As a result, callers will typically want to pass the style
+ // frame to this function.
+ //
+ // Returns an empty result when a suitable element cannot be found including
+ // when the frame represents a pseudo-element on which we do not support
+ // animations.
+ static Maybe<NonOwningAnimationTarget> GetAnimationElementAndPseudoForFrame(
+ const nsIFrame* aFrame);
+
+ // Associates a performance warning with effects on |aFrame| that animate
+ // properties in |aPropertySet|.
+ static void SetPerformanceWarning(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
+ const AnimationPerformanceWarning& aWarning);
+
+ // Do a bunch of stuff that we should avoid doing during the parallel
+ // traversal (e.g. changing member variables) for all elements that we expect
+ // to restyle on the next traversal.
+ //
+ // Returns true if there are elements needing a restyle for animation.
+ bool PreTraverse(ServoTraversalFlags aFlags);
+
+ // Similar to the above but for all elements in the subtree rooted
+ // at aElement.
+ bool PreTraverseInSubtree(ServoTraversalFlags aFlags, dom::Element* aRoot);
+
+ // Record a (pseudo-)element that may have animations that can be removed.
+ void NoteElementForReducing(const NonOwningAnimationTarget& aTarget);
+
+ bool NeedsReducing() const { return !mElementsToReduce.empty(); }
+ void ReduceAnimations();
+
+ // Returns true if any type of compositor animations on |aFrame| allow
+ // runnning on the compositor.
+ // Sets the reason in |aWarning| if the result is false.
+ static bool AllowCompositorAnimationsOnFrame(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aWarning /* out */);
+
+ private:
+ ~EffectCompositor() = default;
+
+ // Get the properties in |aEffectSet| that we are able to animate on the
+ // compositor but which are also specified at a higher level in the cascade
+ // than the animations level.
+ static nsCSSPropertyIDSet GetOverriddenProperties(
+ EffectSet& aEffectSet, dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ static nsPresContext* GetPresContext(dom::Element* aElement);
+
+ nsPresContext* mPresContext;
+
+ // Elements with a pending animation restyle. The associated bool value is
+ // true if a pending animation restyle has also been dispatched. For
+ // animations that can be throttled, we will add an entry to the hashtable to
+ // indicate that the style rule on the element is out of date but without
+ // posting a restyle to update it.
+ EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
+ nsTHashMap<PseudoElementHashEntry, bool>>
+ mElementsToRestyle;
+
+ bool mIsInPreTraverse = false;
+
+ HashSet<OwningAnimationTarget> mElementsToReduce;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EffectCompositor_h
diff --git a/dom/animation/EffectSet.cpp b/dom/animation/EffectSet.cpp
new file mode 100644
index 0000000000..160b1322e8
--- /dev/null
+++ b/dom/animation/EffectSet.cpp
@@ -0,0 +1,132 @@
+/* -*- 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 "EffectSet.h"
+#include "mozilla/dom/Element.h" // For Element
+#include "mozilla/RestyleManager.h"
+#include "mozilla/LayerAnimationInfo.h"
+#include "nsCSSPseudoElements.h" // For PseudoStyleType
+#include "nsCycleCollectionNoteChild.h" // For CycleCollectionNoteChild
+#include "nsPresContext.h"
+#include "nsLayoutUtils.h"
+#include "ElementAnimationData.h"
+
+namespace mozilla {
+
+void EffectSet::Traverse(nsCycleCollectionTraversalCallback& aCallback) {
+ for (const auto& key : mEffects) {
+ CycleCollectionNoteChild(aCallback, key, "EffectSet::mEffects[]",
+ aCallback.Flags());
+ }
+}
+
+/* static */
+EffectSet* EffectSet::Get(const dom::Element* aElement,
+ PseudoStyleType aPseudoType) {
+ if (auto* data = aElement->GetAnimationData()) {
+ return data->GetEffectSetFor(aPseudoType);
+ }
+ return nullptr;
+}
+
+/* static */
+EffectSet* EffectSet::GetOrCreate(dom::Element* aElement,
+ PseudoStyleType aPseudoType) {
+ return &aElement->EnsureAnimationData().EnsureEffectSetFor(aPseudoType);
+}
+
+/* static */
+EffectSet* EffectSet::GetForFrame(const nsIFrame* aFrame,
+ const nsCSSPropertyIDSet& aProperties) {
+ MOZ_ASSERT(aFrame);
+
+ // Transform animations are run on the primary frame (but stored on the
+ // content associated with the style frame).
+ const nsIFrame* frameToQuery = nullptr;
+ if (aProperties.IsSubsetOf(nsCSSPropertyIDSet::TransformLikeProperties())) {
+ // Make sure to return nullptr if we're looking for transform animations on
+ // the inner table frame.
+ if (!aFrame->IsFrameOfType(nsIFrame::eSupportsCSSTransforms)) {
+ return nullptr;
+ }
+ frameToQuery = nsLayoutUtils::GetStyleFrame(aFrame);
+ } else {
+ MOZ_ASSERT(
+ !aProperties.Intersects(nsCSSPropertyIDSet::TransformLikeProperties()),
+ "We should have only transform properties or no transform properties");
+ // We don't need to explicitly return nullptr when |aFrame| is NOT the style
+ // frame since there will be no effect set in that case.
+ frameToQuery = aFrame;
+ }
+
+ Maybe<NonOwningAnimationTarget> target =
+ EffectCompositor::GetAnimationElementAndPseudoForFrame(frameToQuery);
+ if (!target) {
+ return nullptr;
+ }
+
+ return Get(target->mElement, target->mPseudoType);
+}
+
+/* static */
+EffectSet* EffectSet::GetForFrame(const nsIFrame* aFrame,
+ DisplayItemType aDisplayItemType) {
+ return EffectSet::GetForFrame(
+ aFrame, LayerAnimationInfo::GetCSSPropertiesFor(aDisplayItemType));
+}
+
+/* static */
+EffectSet* EffectSet::GetForStyleFrame(const nsIFrame* aStyleFrame) {
+ Maybe<NonOwningAnimationTarget> target =
+ EffectCompositor::GetAnimationElementAndPseudoForFrame(aStyleFrame);
+
+ if (!target) {
+ return nullptr;
+ }
+
+ return Get(target->mElement, target->mPseudoType);
+}
+
+/* static */
+EffectSet* EffectSet::GetForEffect(const dom::KeyframeEffect* aEffect) {
+ NonOwningAnimationTarget target = aEffect->GetAnimationTarget();
+ if (!target) {
+ return nullptr;
+ }
+
+ return EffectSet::Get(target.mElement, target.mPseudoType);
+}
+
+/* static */
+void EffectSet::DestroyEffectSet(dom::Element* aElement,
+ PseudoStyleType aPseudoType) {
+ if (auto* data = aElement->GetAnimationData()) {
+ data->ClearEffectSetFor(aPseudoType);
+ }
+}
+
+void EffectSet::UpdateAnimationGeneration(nsPresContext* aPresContext) {
+ mAnimationGeneration =
+ aPresContext->RestyleManager()->GetAnimationGeneration();
+}
+
+void EffectSet::AddEffect(dom::KeyframeEffect& aEffect) {
+ if (!mEffects.EnsureInserted(&aEffect)) {
+ return;
+ }
+
+ MarkCascadeNeedsUpdate();
+}
+
+void EffectSet::RemoveEffect(dom::KeyframeEffect& aEffect) {
+ if (!mEffects.EnsureRemoved(&aEffect)) {
+ return;
+ }
+
+ MarkCascadeNeedsUpdate();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/EffectSet.h b/dom/animation/EffectSet.h
new file mode 100644
index 0000000000..c743676794
--- /dev/null
+++ b/dom/animation/EffectSet.h
@@ -0,0 +1,246 @@
+/* -*- 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/. */
+
+#ifndef mozilla_EffectSet_h
+#define mozilla_EffectSet_h
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "nsHashKeys.h" // For nsPtrHashKey
+#include "nsTHashSet.h"
+
+class nsPresContext;
+enum class DisplayItemType : uint8_t;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+enum class PseudoStyleType : uint8_t;
+
+// A wrapper around a hashset of AnimationEffect objects to handle
+// storing the set as a property of an element.
+class EffectSet {
+ public:
+ EffectSet()
+ : mCascadeNeedsUpdate(false),
+ mMayHaveOpacityAnim(false),
+ mMayHaveTransformAnim(false) {
+ MOZ_COUNT_CTOR(EffectSet);
+ }
+
+ ~EffectSet() {
+ MOZ_ASSERT(!IsBeingEnumerated(),
+ "Effect set should not be destroyed while it is being "
+ "enumerated");
+ MOZ_COUNT_DTOR(EffectSet);
+ }
+
+ // Methods for supporting cycle-collection
+ void Traverse(nsCycleCollectionTraversalCallback& aCallback);
+
+ static EffectSet* Get(const dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+ static EffectSet* GetOrCreate(dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ static EffectSet* GetForFrame(const nsIFrame* aFrame,
+ const nsCSSPropertyIDSet& aProperties);
+ static EffectSet* GetForFrame(const nsIFrame* aFrame,
+ DisplayItemType aDisplayItemType);
+ // Gets the EffectSet associated with the specified frame's content.
+ //
+ // Typically the specified frame should be a "style frame".
+ //
+ // That is because display:table content:
+ //
+ // - makes a distinction between the primary frame and style frame,
+ // - associates the EffectSet with the style frame's content,
+ // - applies transform animations to the primary frame.
+ //
+ // In such a situation, passing in the primary frame here will return nullptr
+ // despite the fact that it has a transform animation applied to it.
+ //
+ // GetForFrame, above, handles this by automatically looking up the
+ // EffectSet on the corresponding style frame when querying transform
+ // properties. Unless you are sure you know what you are doing, you should
+ // try using GetForFrame first.
+ //
+ // If you decide to use this, consider documenting why you are sure it is ok
+ // to use this.
+ static EffectSet* GetForStyleFrame(const nsIFrame* aStyleFrame);
+
+ static EffectSet* GetForEffect(const dom::KeyframeEffect* aEffect);
+
+ static void DestroyEffectSet(dom::Element* aElement,
+ PseudoStyleType aPseudoType);
+
+ void AddEffect(dom::KeyframeEffect& aEffect);
+ void RemoveEffect(dom::KeyframeEffect& aEffect);
+
+ void SetMayHaveOpacityAnimation() { mMayHaveOpacityAnim = true; }
+ bool MayHaveOpacityAnimation() const { return mMayHaveOpacityAnim; }
+ void SetMayHaveTransformAnimation() { mMayHaveTransformAnim = true; }
+ bool MayHaveTransformAnimation() const { return mMayHaveTransformAnim; }
+
+ private:
+ using OwningEffectSet = nsTHashSet<nsRefPtrHashKey<dom::KeyframeEffect>>;
+
+ public:
+ // A simple iterator to support iterating over the effects in this object in
+ // range-based for loops.
+ //
+ // This allows us to avoid exposing mEffects directly and saves the
+ // caller from having to dereference hashtable iterators using
+ // the rather complicated: iter.Get()->GetKey().
+ //
+ // XXX Except for the active iterator checks, this could be replaced by the
+ // STL-style iterators of nsTHashSet directly now.
+ class Iterator {
+ public:
+ explicit Iterator(EffectSet& aEffectSet)
+ : Iterator(aEffectSet, aEffectSet.mEffects.begin()) {}
+
+ Iterator() = delete;
+ Iterator(const Iterator&) = delete;
+ Iterator(Iterator&&) = delete;
+ Iterator& operator=(const Iterator&) = delete;
+ Iterator& operator=(Iterator&&) = delete;
+
+ static Iterator EndIterator(EffectSet& aEffectSet) {
+ return {aEffectSet, aEffectSet.mEffects.end()};
+ }
+
+#ifdef DEBUG
+ ~Iterator() {
+ MOZ_ASSERT(mEffectSet.mActiveIterators > 0);
+ mEffectSet.mActiveIterators--;
+ }
+#endif
+
+ bool operator!=(const Iterator& aOther) const {
+ return mHashIterator != aOther.mHashIterator;
+ }
+
+ Iterator& operator++() {
+ ++mHashIterator;
+ return *this;
+ }
+
+ dom::KeyframeEffect* operator*() { return *mHashIterator; }
+
+ private:
+ Iterator(EffectSet& aEffectSet,
+ OwningEffectSet::const_iterator aHashIterator)
+ :
+#ifdef DEBUG
+ mEffectSet(aEffectSet),
+#endif
+ mHashIterator(std::move(aHashIterator)) {
+#ifdef DEBUG
+ mEffectSet.mActiveIterators++;
+#endif
+ }
+
+#ifdef DEBUG
+ EffectSet& mEffectSet;
+#endif
+ OwningEffectSet::const_iterator mHashIterator;
+ };
+
+ friend class Iterator;
+
+ Iterator begin() { return Iterator(*this); }
+ Iterator end() { return Iterator::EndIterator(*this); }
+#ifdef DEBUG
+ bool IsBeingEnumerated() const { return mActiveIterators != 0; }
+#endif
+
+ bool IsEmpty() const { return mEffects.IsEmpty(); }
+
+ size_t Count() const { return mEffects.Count(); }
+
+ const TimeStamp& LastOverflowAnimationSyncTime() const {
+ return mLastOverflowAnimationSyncTime;
+ }
+ void UpdateLastOverflowAnimationSyncTime(const TimeStamp& aRefreshTime) {
+ mLastOverflowAnimationSyncTime = aRefreshTime;
+ }
+
+ bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; }
+ void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; }
+ void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; }
+
+ void UpdateAnimationGeneration(nsPresContext* aPresContext);
+ uint64_t GetAnimationGeneration() const { return mAnimationGeneration; }
+
+ const nsCSSPropertyIDSet& PropertiesWithImportantRules() const {
+ return mPropertiesWithImportantRules;
+ }
+ nsCSSPropertyIDSet& PropertiesWithImportantRules() {
+ return mPropertiesWithImportantRules;
+ }
+ nsCSSPropertyIDSet PropertiesForAnimationsLevel() const {
+ return mPropertiesForAnimationsLevel;
+ }
+ nsCSSPropertyIDSet& PropertiesForAnimationsLevel() {
+ return mPropertiesForAnimationsLevel;
+ }
+
+ private:
+ OwningEffectSet mEffects;
+
+ // Refresh driver timestamp from the moment when the animations which produce
+ // overflow change hints in this effect set were last updated.
+
+ // This is used for animations whose main-thread restyling is throttled either
+ // because they are running on the compositor or because they are not visible.
+ // We still need to update them on the main thread periodically, however (e.g.
+ // so scrollbars can be updated), so this tracks the last time we did that.
+ TimeStamp mLastOverflowAnimationSyncTime;
+
+ // RestyleManager keeps track of the number of animation restyles.
+ // 'mini-flushes' (see nsTransitionManager::UpdateAllThrottledStyles()).
+ // mAnimationGeneration is the sequence number of the last flush where a
+ // transition/animation changed. We keep a similar count on the
+ // corresponding layer so we can check that the layer is up to date with
+ // the animation manager.
+ uint64_t mAnimationGeneration = 0;
+
+ // Specifies the compositor-animatable properties that are overridden by
+ // !important rules.
+ nsCSSPropertyIDSet mPropertiesWithImportantRules;
+ // Specifies the properties for which the result will be added to the
+ // animations level of the cascade and hence should be skipped when we are
+ // composing the animation style for the transitions level of the cascede.
+ nsCSSPropertyIDSet mPropertiesForAnimationsLevel;
+
+#ifdef DEBUG
+ // Track how many iterators are referencing this effect set when we are
+ // destroyed, we can assert that nothing is still pointing to us.
+ uint64_t mActiveIterators = 0;
+#endif
+
+ // Dirty flag to represent when the mPropertiesWithImportantRules and
+ // mPropertiesForAnimationsLevel on effects in this set might need to be
+ // updated.
+ //
+ // Set to true any time the set of effects is changed or when
+ // one the effects goes in or out of the "in effect" state.
+ bool mCascadeNeedsUpdate = false;
+
+ bool mMayHaveOpacityAnim = false;
+ bool mMayHaveTransformAnim = false;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EffectSet_h
diff --git a/dom/animation/ElementAnimationData.cpp b/dom/animation/ElementAnimationData.cpp
new file mode 100644
index 0000000000..d87caf8a58
--- /dev/null
+++ b/dom/animation/ElementAnimationData.cpp
@@ -0,0 +1,126 @@
+/* -*- 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 "ElementAnimationData.h"
+#include "mozilla/AnimationCollection.h"
+#include "mozilla/TimelineCollection.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/dom/CSSTransition.h"
+#include "mozilla/dom/CSSAnimation.h"
+#include "mozilla/dom/ScrollTimeline.h"
+#include "mozilla/dom/ViewTimeline.h"
+
+namespace mozilla {
+
+void ElementAnimationData::Traverse(nsCycleCollectionTraversalCallback& cb) {
+ mElementData.Traverse(cb);
+ mBeforeData.Traverse(cb);
+ mAfterData.Traverse(cb);
+ mMarkerData.Traverse(cb);
+}
+
+void ElementAnimationData::ClearAllAnimationCollections() {
+ for (auto* data : {&mElementData, &mBeforeData, &mAfterData, &mMarkerData}) {
+ data->mAnimations = nullptr;
+ data->mTransitions = nullptr;
+ data->mScrollTimelines = nullptr;
+ data->mViewTimelines = nullptr;
+ data->mProgressTimelineScheduler = nullptr;
+ }
+}
+
+ElementAnimationData::PerElementOrPseudoData::PerElementOrPseudoData() =
+ default;
+ElementAnimationData::PerElementOrPseudoData::~PerElementOrPseudoData() =
+ default;
+
+void ElementAnimationData::PerElementOrPseudoData::Traverse(
+ nsCycleCollectionTraversalCallback& cb) {
+ // We only care about mEffectSet. The animation collections are managed by the
+ // pres context and go away when presentation of the document goes away.
+ if (mEffectSet) {
+ mEffectSet->Traverse(cb);
+ }
+}
+
+EffectSet& ElementAnimationData::PerElementOrPseudoData::DoEnsureEffectSet() {
+ MOZ_ASSERT(!mEffectSet);
+ mEffectSet = MakeUnique<EffectSet>();
+ return *mEffectSet;
+}
+
+CSSTransitionCollection&
+ElementAnimationData::PerElementOrPseudoData::DoEnsureTransitions(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ MOZ_ASSERT(!mTransitions);
+ mTransitions = MakeUnique<CSSTransitionCollection>(aOwner, aType);
+ return *mTransitions;
+}
+
+CSSAnimationCollection&
+ElementAnimationData::PerElementOrPseudoData::DoEnsureAnimations(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ MOZ_ASSERT(!mAnimations);
+ mAnimations = MakeUnique<CSSAnimationCollection>(aOwner, aType);
+ return *mAnimations;
+}
+
+ScrollTimelineCollection&
+ElementAnimationData::PerElementOrPseudoData::DoEnsureScrollTimelines(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ MOZ_ASSERT(!mScrollTimelines);
+ mScrollTimelines = MakeUnique<ScrollTimelineCollection>(aOwner, aType);
+ return *mScrollTimelines;
+}
+
+ViewTimelineCollection&
+ElementAnimationData::PerElementOrPseudoData::DoEnsureViewTimelines(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ MOZ_ASSERT(!mViewTimelines);
+ mViewTimelines = MakeUnique<ViewTimelineCollection>(aOwner, aType);
+ return *mViewTimelines;
+}
+
+dom::ProgressTimelineScheduler&
+ElementAnimationData::PerElementOrPseudoData::DoEnsureProgressTimelineScheduler(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ MOZ_ASSERT(!mProgressTimelineScheduler);
+ mProgressTimelineScheduler = MakeUnique<dom::ProgressTimelineScheduler>();
+ return *mProgressTimelineScheduler;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::DoClearEffectSet() {
+ MOZ_ASSERT(mEffectSet);
+ mEffectSet = nullptr;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::DoClearTransitions() {
+ MOZ_ASSERT(mTransitions);
+ mTransitions = nullptr;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::DoClearAnimations() {
+ MOZ_ASSERT(mAnimations);
+ mAnimations = nullptr;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::DoClearScrollTimelines() {
+ MOZ_ASSERT(mScrollTimelines);
+ mScrollTimelines = nullptr;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::DoClearViewTimelines() {
+ MOZ_ASSERT(mViewTimelines);
+ mViewTimelines = nullptr;
+}
+
+void ElementAnimationData::PerElementOrPseudoData::
+ DoClearProgressTimelineScheduler() {
+ MOZ_ASSERT(mProgressTimelineScheduler);
+ mProgressTimelineScheduler = nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/animation/ElementAnimationData.h b/dom/animation/ElementAnimationData.h
new file mode 100644
index 0000000000..9183f0f008
--- /dev/null
+++ b/dom/animation/ElementAnimationData.h
@@ -0,0 +1,262 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ElementAnimationData_h
+#define mozilla_ElementAnimationData_h
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/PseudoStyleType.h"
+
+class nsCycleCollectionTraversalCallback;
+
+namespace mozilla {
+enum class PseudoStyleType : uint8_t;
+class EffectSet;
+template <typename Animation>
+class AnimationCollection;
+template <typename TimelineType>
+class TimelineCollection;
+namespace dom {
+class Element;
+class CSSAnimation;
+class CSSTransition;
+class ProgressTimelineScheduler;
+class ScrollTimeline;
+class ViewTimeline;
+} // namespace dom
+using CSSAnimationCollection = AnimationCollection<dom::CSSAnimation>;
+using CSSTransitionCollection = AnimationCollection<dom::CSSTransition>;
+using ScrollTimelineCollection = TimelineCollection<dom::ScrollTimeline>;
+using ViewTimelineCollection = TimelineCollection<dom::ViewTimeline>;
+
+// The animation data for a given element (and its pseudo-elements).
+class ElementAnimationData {
+ struct PerElementOrPseudoData {
+ UniquePtr<EffectSet> mEffectSet;
+ UniquePtr<CSSAnimationCollection> mAnimations;
+ UniquePtr<CSSTransitionCollection> mTransitions;
+
+ // Note: scroll-timeline-name is applied to elements which could be
+ // scroll containers, or replaced elements. view-timeline-name is applied to
+ // all elements. However, the named timeline is referenceable in
+ // animation-timeline by the tree order scope.
+ // Spec: https://drafts.csswg.org/scroll-animations-1/#timeline-scope.
+ //
+ // So it should be fine to create timeline objects only on the elements and
+ // pseudo elements which support animations.
+ UniquePtr<ScrollTimelineCollection> mScrollTimelines;
+ UniquePtr<ViewTimelineCollection> mViewTimelines;
+
+ // This is different from |mScrollTimelines|. We use this to schedule all
+ // scroll-driven animations (which use anonymous/named scroll timelines or
+ // anonymous/name view timelines) for a specific scroll source (which is the
+ // element with nsIScrollableFrame).
+ //
+ // TimelineCollection owns and manages the named progress timeline generated
+ // by specifying scroll-timeline-name property and view-timeline-name
+ // property on this element. However, the anonymous progress timelines (e.g.
+ // animation-timeline:scroll()) are owned by Animation objects only.
+ //
+ // Note:
+ // 1. For named scroll timelines. The element which specifies
+ // scroll-timeline-name is the scroll source. However, for named view
+ // timelines, the element which specifies view-timeline-name may not be
+ // the scroll source because we use its nearest scroll container as the
+ // scroll source.
+ // 2. For anonymous progress timelines, we don't keep their timeline obejcts
+ // in TimelineCollection.
+ // So, per 1) and 2), we use |mProgressTimelineScheduler| for the scroll
+ // source element to schedule scroll-driven animations.
+ UniquePtr<dom::ProgressTimelineScheduler> mProgressTimelineScheduler;
+
+ PerElementOrPseudoData();
+ ~PerElementOrPseudoData();
+
+ EffectSet& DoEnsureEffectSet();
+ CSSTransitionCollection& DoEnsureTransitions(dom::Element&,
+ PseudoStyleType);
+ CSSAnimationCollection& DoEnsureAnimations(dom::Element&, PseudoStyleType);
+ ScrollTimelineCollection& DoEnsureScrollTimelines(dom::Element&,
+ PseudoStyleType);
+ ViewTimelineCollection& DoEnsureViewTimelines(dom::Element&,
+ PseudoStyleType);
+ dom::ProgressTimelineScheduler& DoEnsureProgressTimelineScheduler(
+ dom::Element&, PseudoStyleType);
+
+ void DoClearEffectSet();
+ void DoClearTransitions();
+ void DoClearAnimations();
+ void DoClearScrollTimelines();
+ void DoClearViewTimelines();
+ void DoClearProgressTimelineScheduler();
+
+ void Traverse(nsCycleCollectionTraversalCallback&);
+ };
+
+ PerElementOrPseudoData mElementData;
+
+ // TODO(emilio): Maybe this should be a hash map eventually, once we allow
+ // animating all pseudo-elements.
+ PerElementOrPseudoData mBeforeData;
+ PerElementOrPseudoData mAfterData;
+ PerElementOrPseudoData mMarkerData;
+
+ const PerElementOrPseudoData& DataFor(PseudoStyleType aType) const {
+ switch (aType) {
+ case PseudoStyleType::NotPseudo:
+ break;
+ case PseudoStyleType::before:
+ return mBeforeData;
+ case PseudoStyleType::after:
+ return mAfterData;
+ case PseudoStyleType::marker:
+ return mMarkerData;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Should not try to get animation effects for "
+ "a pseudo other that :before, :after or ::marker");
+ break;
+ }
+ return mElementData;
+ }
+
+ PerElementOrPseudoData& DataFor(PseudoStyleType aType) {
+ const auto& data =
+ const_cast<const ElementAnimationData*>(this)->DataFor(aType);
+ return const_cast<PerElementOrPseudoData&>(data);
+ }
+
+ public:
+ void Traverse(nsCycleCollectionTraversalCallback&);
+
+ void ClearAllAnimationCollections();
+
+ EffectSet* GetEffectSetFor(PseudoStyleType aType) const {
+ return DataFor(aType).mEffectSet.get();
+ }
+
+ void ClearEffectSetFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mEffectSet) {
+ data.DoClearEffectSet();
+ }
+ }
+
+ EffectSet& EnsureEffectSetFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* set = data.mEffectSet.get()) {
+ return *set;
+ }
+ return data.DoEnsureEffectSet();
+ }
+
+ CSSTransitionCollection* GetTransitionCollection(PseudoStyleType aType) {
+ return DataFor(aType).mTransitions.get();
+ }
+
+ void ClearTransitionCollectionFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mTransitions) {
+ data.DoClearTransitions();
+ }
+ }
+
+ CSSTransitionCollection& EnsureTransitionCollection(dom::Element& aOwner,
+ PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* collection = data.mTransitions.get()) {
+ return *collection;
+ }
+ return data.DoEnsureTransitions(aOwner, aType);
+ }
+
+ CSSAnimationCollection* GetAnimationCollection(PseudoStyleType aType) {
+ return DataFor(aType).mAnimations.get();
+ }
+
+ void ClearAnimationCollectionFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mAnimations) {
+ data.DoClearAnimations();
+ }
+ }
+
+ CSSAnimationCollection& EnsureAnimationCollection(dom::Element& aOwner,
+ PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* collection = data.mAnimations.get()) {
+ return *collection;
+ }
+ return data.DoEnsureAnimations(aOwner, aType);
+ }
+
+ ScrollTimelineCollection* GetScrollTimelineCollection(PseudoStyleType aType) {
+ return DataFor(aType).mScrollTimelines.get();
+ }
+
+ void ClearScrollTimelineCollectionFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mScrollTimelines) {
+ data.DoClearScrollTimelines();
+ }
+ }
+
+ ScrollTimelineCollection& EnsureScrollTimelineCollection(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* collection = data.mScrollTimelines.get()) {
+ return *collection;
+ }
+ return data.DoEnsureScrollTimelines(aOwner, aType);
+ }
+
+ ViewTimelineCollection* GetViewTimelineCollection(PseudoStyleType aType) {
+ return DataFor(aType).mViewTimelines.get();
+ }
+
+ void ClearViewTimelineCollectionFor(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mViewTimelines) {
+ data.DoClearViewTimelines();
+ }
+ }
+
+ ViewTimelineCollection& EnsureViewTimelineCollection(dom::Element& aOwner,
+ PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* collection = data.mViewTimelines.get()) {
+ return *collection;
+ }
+ return data.DoEnsureViewTimelines(aOwner, aType);
+ }
+
+ dom::ProgressTimelineScheduler* GetProgressTimelineScheduler(
+ PseudoStyleType aType) {
+ return DataFor(aType).mProgressTimelineScheduler.get();
+ }
+
+ void ClearProgressTimelineScheduler(PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (data.mProgressTimelineScheduler) {
+ data.DoClearProgressTimelineScheduler();
+ }
+ }
+
+ dom::ProgressTimelineScheduler& EnsureProgressTimelineScheduler(
+ dom::Element& aOwner, PseudoStyleType aType) {
+ auto& data = DataFor(aType);
+ if (auto* collection = data.mProgressTimelineScheduler.get()) {
+ return *collection;
+ }
+ return data.DoEnsureProgressTimelineScheduler(aOwner, aType);
+ }
+
+ ElementAnimationData() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/animation/Keyframe.h b/dom/animation/Keyframe.h
new file mode 100644
index 0000000000..304358d61e
--- /dev/null
+++ b/dom/animation/Keyframe.h
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_Keyframe_h
+#define mozilla_dom_Keyframe_h
+
+#include "nsCSSPropertyID.h"
+#include "nsCSSValue.h"
+#include "nsTArray.h"
+#include "mozilla/dom/BaseKeyframeTypesBinding.h" // CompositeOperationOrAuto
+#include "mozilla/ServoStyleConsts.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+struct StyleLockedDeclarationBlock;
+
+/**
+ * A property-value pair specified on a keyframe.
+ */
+struct PropertyValuePair {
+ explicit PropertyValuePair(nsCSSPropertyID aProperty)
+ : mProperty(aProperty) {}
+ PropertyValuePair(nsCSSPropertyID aProperty,
+ RefPtr<StyleLockedDeclarationBlock>&& aValue)
+ : mProperty(aProperty), mServoDeclarationBlock(std::move(aValue)) {
+ MOZ_ASSERT(mServoDeclarationBlock, "Should be valid property value");
+ }
+
+ nsCSSPropertyID mProperty;
+
+ // The specified value when using the Servo backend.
+ RefPtr<StyleLockedDeclarationBlock> mServoDeclarationBlock;
+
+#ifdef DEBUG
+ // Flag to indicate that when we call StyleAnimationValue::ComputeValues on
+ // this value we should behave as if that function had failed.
+ bool mSimulateComputeValuesFailure = false;
+#endif
+
+ bool operator==(const PropertyValuePair&) const;
+};
+
+/**
+ * A single keyframe.
+ *
+ * This is the canonical form in which keyframe effects are stored and
+ * corresponds closely to the type of objects returned via the getKeyframes()
+ * API.
+ *
+ * Before computing an output animation value, however, we flatten these frames
+ * down to a series of per-property value arrays where we also resolve any
+ * overlapping shorthands/longhands, convert specified CSS values to computed
+ * values, etc.
+ *
+ * When the target element or computed style changes, however, we rebuild these
+ * per-property arrays from the original list of keyframes objects. As a result,
+ * these objects represent the master definition of the effect's values.
+ */
+struct Keyframe {
+ Keyframe() = default;
+ Keyframe(const Keyframe& aOther) = default;
+ Keyframe(Keyframe&& aOther) = default;
+
+ Keyframe& operator=(const Keyframe& aOther) = default;
+ Keyframe& operator=(Keyframe&& aOther) = default;
+
+ Maybe<double> mOffset;
+ static constexpr double kComputedOffsetNotSet = -1.0;
+ double mComputedOffset = kComputedOffsetNotSet;
+ Maybe<StyleComputedTimingFunction> mTimingFunction; // Nothing() here means
+ // "linear"
+ dom::CompositeOperationOrAuto mComposite =
+ dom::CompositeOperationOrAuto::Auto;
+ CopyableTArray<PropertyValuePair> mPropertyValues;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_Keyframe_h
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
diff --git a/dom/animation/KeyframeEffect.h b/dom/animation/KeyframeEffect.h
new file mode 100644
index 0000000000..8f8466369e
--- /dev/null
+++ b/dom/animation/KeyframeEffect.h
@@ -0,0 +1,529 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_KeyframeEffect_h
+#define mozilla_dom_KeyframeEffect_h
+
+#include "nsChangeHint.h"
+#include "nsCSSPropertyID.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSValue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationPropertySegment.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/Keyframe.h"
+#include "mozilla/KeyframeEffectParams.h"
+#include "mozilla/PostRestyleMode.h"
+// StyleLockedDeclarationBlock and associated RefPtrTraits
+#include "mozilla/ServoBindingTypes.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/dom/AnimationEffect.h"
+#include "mozilla/dom/BindingDeclarations.h"
+
+struct JSContext;
+class JSObject;
+class nsIContent;
+class nsIFrame;
+
+namespace mozilla {
+
+class AnimValuesStyleRule;
+class ErrorResult;
+struct AnimationRule;
+struct TimingParams;
+class EffectSet;
+class ComputedStyle;
+class PresShell;
+
+namespace dom {
+class Element;
+class GlobalObject;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+enum class IterationCompositeOperation : uint8_t;
+enum class CompositeOperation : uint8_t;
+struct AnimationPropertyDetails;
+} // namespace dom
+
+struct AnimationProperty {
+ nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN;
+
+ // If true, the propery is currently being animated on the compositor.
+ //
+ // Note that when the owning Animation requests a non-throttled restyle, in
+ // between calling RequestRestyle on its EffectCompositor and when the
+ // restyle is performed, this member may temporarily become false even if
+ // the animation remains on the layer after the restyle.
+ //
+ // **NOTE**: This member is not included when comparing AnimationProperty
+ // objects for equality.
+ bool mIsRunningOnCompositor = false;
+
+ Maybe<AnimationPerformanceWarning> mPerformanceWarning;
+
+ nsTArray<AnimationPropertySegment> mSegments;
+
+ // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and
+ // mPerformanceWarning.
+ AnimationProperty() = default;
+ AnimationProperty(const AnimationProperty& aOther)
+ : mProperty(aOther.mProperty), mSegments(aOther.mSegments.Clone()) {}
+ AnimationProperty& operator=(const AnimationProperty& aOther) {
+ mProperty = aOther.mProperty;
+ mSegments = aOther.mSegments.Clone();
+ return *this;
+ }
+
+ // NOTE: This operator does *not* compare the mIsRunningOnCompositor member.
+ // This is because AnimationProperty objects are compared when recreating
+ // CSS animations to determine if mutation observer change records need to
+ // be created or not. However, at the point when these objects are compared
+ // the mIsRunningOnCompositor will not have been set on the new objects so
+ // we ignore this member to avoid generating spurious change records.
+ bool operator==(const AnimationProperty& aOther) const {
+ return mProperty == aOther.mProperty && mSegments == aOther.mSegments;
+ }
+ bool operator!=(const AnimationProperty& aOther) const {
+ return !(*this == aOther);
+ }
+
+ void SetPerformanceWarning(const AnimationPerformanceWarning& aWarning,
+ const dom::Element* aElement);
+};
+
+namespace dom {
+
+class Animation;
+class Document;
+
+class KeyframeEffect : public AnimationEffect {
+ public:
+ KeyframeEffect(Document* aDocument, OwningAnimationTarget&& aTarget,
+ TimingParams&& aTiming, const KeyframeEffectParams& aOptions);
+
+ KeyframeEffect(Document* aDocument, OwningAnimationTarget&& aTarget,
+ const KeyframeEffect& aOther);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffect,
+ AnimationEffect)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ KeyframeEffect* AsKeyframeEffect() override { return this; }
+
+ bool IsValidTransition() const {
+ return Properties().Length() == 1 &&
+ Properties()[0].mSegments.Length() == 1;
+ }
+
+ // KeyframeEffect interface
+ static already_AddRefed<KeyframeEffect> Constructor(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv);
+
+ static already_AddRefed<KeyframeEffect> Constructor(
+ const GlobalObject& aGlobal, KeyframeEffect& aSource, ErrorResult& aRv);
+
+ // Variant of Constructor that accepts a KeyframeAnimationOptions object
+ // for use with for Animatable.animate.
+ // Not exposed to content.
+ static already_AddRefed<KeyframeEffect> Constructor(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes,
+ const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv);
+
+ Element* GetTarget() const { return mTarget.mElement.get(); }
+ NonOwningAnimationTarget GetAnimationTarget() const {
+ return NonOwningAnimationTarget(mTarget.mElement, mTarget.mPseudoType);
+ }
+ void GetPseudoElement(nsAString& aRetVal) const {
+ if (mTarget.mPseudoType == PseudoStyleType::NotPseudo) {
+ SetDOMStringToNull(aRetVal);
+ return;
+ }
+ aRetVal = nsCSSPseudoElements::PseudoTypeAsString(mTarget.mPseudoType);
+ }
+
+ // These two setters call GetTargetComputedStyle which is not safe to use when
+ // we are in the middle of updating style. If we need to use this when
+ // updating style, we should pass the ComputedStyle into this method and use
+ // that to update the properties rather than calling
+ // GetComputedStyle.
+ void SetTarget(Element* aTarget) {
+ UpdateTarget(aTarget, mTarget.mPseudoType);
+ }
+ void SetPseudoElement(const nsAString& aPseudoElement, ErrorResult& aRv);
+
+ void GetKeyframes(JSContext* aCx, nsTArray<JSObject*>& aResult,
+ ErrorResult& aRv) const;
+ void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
+ ErrorResult& aRv) const;
+
+ IterationCompositeOperation IterationComposite() const;
+ void SetIterationComposite(
+ const IterationCompositeOperation& aIterationComposite);
+
+ CompositeOperation Composite() const;
+ virtual void SetComposite(const CompositeOperation& aComposite);
+ void SetCompositeFromStyle(const CompositeOperation& aComposite) {
+ KeyframeEffect::SetComposite(aComposite);
+ }
+
+ void NotifySpecifiedTimingUpdated();
+ void NotifyAnimationTimingUpdated(PostRestyleMode aPostRestyle);
+ void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
+ void SetAnimation(Animation* aAnimation) override;
+ virtual void SetKeyframes(JSContext* aContext,
+ JS::Handle<JSObject*> aKeyframes, ErrorResult& aRv);
+ void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+ const ComputedStyle* aStyle,
+ const AnimationTimeline* aTimeline);
+
+ // Replace the start value of the transition. This is used for updating
+ // transitions running on the compositor.
+ void ReplaceTransitionStartValue(AnimationValue&& aStartValue);
+
+ // Returns the set of properties affected by this effect regardless of
+ // whether any of these properties is overridden by an !important rule.
+ nsCSSPropertyIDSet GetPropertySet() const;
+
+ // Returns true if the effect includes a property in |aPropertySet| regardless
+ // of whether any property in the set is overridden by an !important rule.
+ bool HasAnimationOfPropertySet(const nsCSSPropertyIDSet& aPropertySet) const {
+ return GetPropertySet().Intersects(aPropertySet);
+ }
+
+ // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
+ // to a given CSS property if the effect includes the property and the
+ // property is not overridden by !important rules.
+ // Also EffectiveAnimationOfProperty returns true under the same condition.
+ //
+ // |aEffect| should be the EffectSet containing this KeyframeEffect since
+ // this function is typically called for all KeyframeEffects on an element
+ // so that we can avoid multiple calls of EffectSet::GetEffect().
+ //
+ // Note that does not consider the interaction between related transform
+ // properties where an !important rule on another transform property may
+ // cause all transform properties to be run on the main thread. That check is
+ // performed by GetPropertiesForCompositor.
+ bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty,
+ const EffectSet& aEffect) const {
+ return GetEffectiveAnimationOfProperty(aProperty, aEffect) != nullptr;
+ }
+ const AnimationProperty* GetEffectiveAnimationOfProperty(
+ nsCSSPropertyID aProperty, const EffectSet& aEffect) const;
+
+ // Similar to HasEffectiveAnimationOfProperty, above, but for
+ // an nsCSSPropertyIDSet. Returns true if this keyframe effect has at least
+ // one property in |aPropertySet| that is not overridden by an !important
+ // rule.
+ //
+ // Note that does not consider the interaction between related transform
+ // properties where an !important rule on another transform property may
+ // cause all transform properties to be run on the main thread. That check is
+ // performed by GetPropertiesForCompositor.
+ bool HasEffectiveAnimationOfPropertySet(
+ const nsCSSPropertyIDSet& aPropertySet,
+ const EffectSet& aEffectSet) const;
+
+ // Returns all the effective animated CSS properties that can be animated on
+ // the compositor and are not overridden by a higher cascade level.
+ //
+ // NOTE: This function is basically called for all KeyframeEffects on an
+ // element thus it takes |aEffects| to avoid multiple calls of
+ // EffectSet::GetEffect().
+ //
+ // NOTE(2): This function does NOT check that animations are permitted on
+ // |aFrame|. It is the responsibility of the caller to first call
+ // EffectCompositor::AllowCompositorAnimationsOnFrame for |aFrame|, or use
+ // nsLayoutUtils::GetAnimationPropertiesForCompositor instead.
+ nsCSSPropertyIDSet GetPropertiesForCompositor(EffectSet& aEffects,
+ const nsIFrame* aFrame) const;
+
+ const nsTArray<AnimationProperty>& Properties() const { return mProperties; }
+
+ // Update |mProperties| by recalculating from |mKeyframes| using
+ // |aComputedStyle| to resolve specified values.
+ // Note: we use |aTimeline| to check if we need to ensure the base styles.
+ // If it is nullptr, we use the timeline from |mAnimation|.
+ void UpdateProperties(const ComputedStyle* aStyle,
+ const AnimationTimeline* aTimeline = nullptr);
+
+ // Update various bits of state related to running ComposeStyle().
+ // We need to update this outside ComposeStyle() because we should avoid
+ // mutating any state in ComposeStyle() since it might be called during
+ // parallel traversal.
+ void WillComposeStyle();
+
+ // Updates |aComposeResult| with the animation values produced by this
+ // AnimationEffect for the current time except any properties contained
+ // in |aPropertiesToSkip|.
+ void ComposeStyle(StyleAnimationValueMap& aComposeResult,
+ const nsCSSPropertyIDSet& aPropertiesToSkip);
+
+ // Returns true if at least one property is being animated on compositor.
+ bool IsRunningOnCompositor() const;
+ void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
+ void SetIsRunningOnCompositor(const nsCSSPropertyIDSet& aPropertySet,
+ bool aIsRunning);
+ void ResetIsRunningOnCompositor();
+
+ void ResetPartialPrerendered();
+
+ // Returns true if this effect, applied to |aFrame|, contains properties
+ // that mean we shouldn't run transform compositor animations on this element.
+ //
+ // For example, if we have an animation of geometric properties like 'left'
+ // and 'top' on an element, we force all 'transform' animations running at
+ // the same time on the same element to run on the main thread.
+ //
+ // When returning true, |aPerformanceWarning| stores the reason why
+ // we shouldn't run the transform animations.
+ bool ShouldBlockAsyncTransformAnimations(
+ const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const;
+ bool HasGeometricProperties() const;
+ bool AffectsGeometry() const override {
+ return mTarget && HasGeometricProperties();
+ }
+
+ Document* GetRenderedDocument() const;
+ PresShell* GetPresShell() const;
+
+ // Associates a warning with the animated property set on the specified frame
+ // indicating why, for example, the property could not be animated on the
+ // compositor. |aParams| and |aParamsLength| are optional parameters which
+ // will be used to generate a localized message for devtools.
+ void SetPerformanceWarning(const nsCSSPropertyIDSet& aPropertySet,
+ const AnimationPerformanceWarning& aWarning);
+
+ // Cumulative change hint on each segment for each property.
+ // This is used for deciding the animation is paint-only.
+ void CalculateCumulativeChangeHint(const ComputedStyle* aStyle);
+
+ // Returns true if all of animation properties' change hints
+ // can ignore painting if the animation is not visible.
+ // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
+ // in detail which change hint can be ignored.
+ bool CanIgnoreIfNotVisible() const;
+
+ // Returns true if the effect is current state and has scale animation.
+ // |aFrame| is used for calculation of scale values.
+ bool ContainsAnimatedScale(const nsIFrame* aFrame) const;
+
+ AnimationValue BaseStyle(nsCSSPropertyID aProperty) const {
+ AnimationValue result;
+ bool hasProperty = false;
+ // We cannot use getters_AddRefs on StyleAnimationValue because it is
+ // an incomplete type, so Get() doesn't work. Instead, use GetWeak, and
+ // then assign the raw pointer to a RefPtr.
+ result.mServo = mBaseValues.GetWeak(aProperty, &hasProperty);
+ MOZ_ASSERT(hasProperty || result.IsNull());
+ return result;
+ }
+
+ enum class MatchForCompositor {
+ // This animation matches and should run on the compositor if possible.
+ Yes,
+ // This (not currently playing) animation matches and can be run on the
+ // compositor if there are other animations for this property that return
+ // 'Yes'.
+ IfNeeded,
+ // This animation does not match or can't be run on the compositor.
+ No,
+ // This animation does not match or can't be run on the compositor and,
+ // furthermore, its presence means we should not run any animations for this
+ // property on the compositor.
+ NoAndBlockThisProperty
+ };
+
+ MatchForCompositor IsMatchForCompositor(
+ const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
+ const EffectSet& aEffects,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const;
+
+ static bool HasComputedTimingChanged(
+ const ComputedTiming& aComputedTiming,
+ IterationCompositeOperation aIterationComposite,
+ const Nullable<double>& aProgressOnLastCompose,
+ uint64_t aCurrentIterationOnLastCompose);
+
+ bool HasOpacityChange() const {
+ return mCumulativeChangeHint & nsChangeHint_UpdateOpacityLayer;
+ }
+
+ protected:
+ ~KeyframeEffect() override = default;
+
+ template <class OptionsType>
+ static already_AddRefed<KeyframeEffect> ConstructKeyframeEffect(
+ const GlobalObject& aGlobal, Element* aTarget,
+ JS::Handle<JSObject*> aKeyframes, const OptionsType& aOptions,
+ ErrorResult& aRv);
+
+ // Build properties by recalculating from |mKeyframes| using |aComputedStyle|
+ // to resolve specified values. This function also applies paced spacing if
+ // needed.
+ nsTArray<AnimationProperty> BuildProperties(const ComputedStyle* aStyle);
+
+ // Helper for SetTarget() and SetPseudoElement().
+ void UpdateTarget(Element* aElement, PseudoStyleType aPseudoType);
+
+ // This effect is registered with its target element so long as:
+ //
+ // (a) It has a target element, and
+ // (b) It is "relevant" (i.e. yet to finish but not idle, or finished but
+ // filling forwards)
+ //
+ // As a result, we need to make sure this gets called whenever anything
+ // changes with regards to this effects's timing including changes to the
+ // owning Animation's timing.
+ void UpdateTargetRegistration();
+
+ // Remove the current effect target from its EffectSet.
+ void UnregisterTarget();
+
+ // Looks up the ComputedStyle associated with the target element, if any.
+ // We need to be careful to *not* call this when we are updating the style
+ // context. That's because calling GetComputedStyle when we are in the process
+ // of building a ComputedStyle may trigger various forms of infinite
+ // recursion.
+ enum class Flush {
+ Style,
+ None,
+ };
+ already_AddRefed<const ComputedStyle> GetTargetComputedStyle(Flush) const;
+
+ // A wrapper for marking cascade update according to the current
+ // target and its effectSet.
+ void MarkCascadeNeedsUpdate();
+
+ void EnsureBaseStyles(const ComputedStyle* aComputedValues,
+ const nsTArray<AnimationProperty>& aProperties,
+ const AnimationTimeline* aTimeline,
+ bool* aBaseStylesChanged);
+ void EnsureBaseStyle(const AnimationProperty& aProperty,
+ nsPresContext* aPresContext,
+ const ComputedStyle* aComputedValues,
+ const AnimationTimeline* aTimeline,
+ RefPtr<const ComputedStyle>& aBaseComputedValues);
+
+ OwningAnimationTarget mTarget;
+
+ KeyframeEffectParams mEffectOptions;
+
+ // The specified keyframes.
+ nsTArray<Keyframe> mKeyframes;
+
+ // A set of per-property value arrays, derived from |mKeyframes|.
+ nsTArray<AnimationProperty> mProperties;
+
+ // The computed progress last time we composed the style rule. This is
+ // used to detect when the progress is not changing (e.g. due to a step
+ // timing function) so we can avoid unnecessary style updates.
+ Nullable<double> mProgressOnLastCompose;
+
+ // The purpose of this value is the same as mProgressOnLastCompose but
+ // this is used to detect when the current iteration is not changing
+ // in the case when iterationComposite is accumulate.
+ uint64_t mCurrentIterationOnLastCompose = 0;
+
+ // We need to track when we go to or from being "in effect" since
+ // we need to re-evaluate the cascade of animations when that changes.
+ bool mInEffectOnLastAnimationTimingUpdate = false;
+
+ // True if this effect is in the EffectSet for its target element. This is
+ // used as an optimization to avoid unnecessary hashmap lookups on the
+ // EffectSet.
+ bool mInEffectSet = false;
+
+ // True if the last time we tried to update the cumulative change hint we
+ // didn't have style data to do so. We set this flag so that the next time
+ // our style context changes we know to update the cumulative change hint even
+ // if our properties haven't changed.
+ bool mNeedsStyleData = false;
+
+ // True if there is any current-color for background color in this keyframes.
+ bool mHasCurrentColor = false;
+
+ // The non-animated values for properties in this effect that contain at
+ // least one animation value that is composited with the underlying value
+ // (i.e. it uses the additive or accumulate composite mode).
+ using BaseValuesHashmap =
+ nsRefPtrHashtable<nsUint32HashKey, StyleAnimationValue>;
+ BaseValuesHashmap mBaseValues;
+
+ private:
+ nsChangeHint mCumulativeChangeHint = nsChangeHint{0};
+
+ void ComposeStyleRule(StyleAnimationValueMap& aAnimationValues,
+ const AnimationProperty& aProperty,
+ const AnimationPropertySegment& aSegment,
+ const ComputedTiming& aComputedTiming);
+
+ already_AddRefed<const ComputedStyle> CreateComputedStyleForAnimationValue(
+ nsCSSPropertyID aProperty, const AnimationValue& aValue,
+ nsPresContext* aPresContext, const ComputedStyle* aBaseComputedStyle);
+
+ // Return the primary frame for the target (pseudo-)element.
+ nsIFrame* GetPrimaryFrame() const;
+ // Returns the frame which is used for styling.
+ nsIFrame* GetStyleFrame() const;
+
+ bool CanThrottle() const;
+ bool CanThrottleOverflowChanges(const nsIFrame& aFrame) const;
+ bool CanThrottleOverflowChangesInScrollable(nsIFrame& aFrame) const;
+ bool CanThrottleIfNotVisible(nsIFrame& aFrame) const;
+
+ // Returns true if the computedTiming has changed since the last
+ // composition.
+ bool HasComputedTimingChanged() const;
+
+ // Returns true unless Gecko limitations prevent performing transform
+ // animations for |aFrame|. When returning true, the reason for the
+ // limitation is stored in |aOutPerformanceWarning|.
+ static bool CanAnimateTransformOnCompositor(
+ const nsIFrame* aFrame,
+ AnimationPerformanceWarning::Type& aPerformanceWarning /* out */);
+ static bool IsGeometricProperty(const nsCSSPropertyID aProperty);
+
+ static const TimeDuration OverflowRegionRefreshInterval();
+
+ void UpdateEffectSet(mozilla::EffectSet* aEffectSet = nullptr) const;
+
+ // Returns true if this effect has properties that might affect the overflow
+ // region.
+ // This function is used for updating scroll bars or notifying intersection
+ // observers reflected by the transform.
+ bool HasPropertiesThatMightAffectOverflow() const {
+ return mCumulativeChangeHint &
+ (nsChangeHint_AddOrRemoveTransform | nsChangeHint_UpdateOverflow |
+ nsChangeHint_UpdatePostTransformOverflow |
+ nsChangeHint_UpdateTransformLayer);
+ }
+
+ // Returns true if this effect causes visibility change.
+ // (i.e. 'visibility: hidden' -> 'visibility: visible' and vice versa.)
+ bool HasVisibilityChange() const {
+ return mCumulativeChangeHint & nsChangeHint_VisibilityChange;
+ }
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_KeyframeEffect_h
diff --git a/dom/animation/KeyframeEffectParams.h b/dom/animation/KeyframeEffectParams.h
new file mode 100644
index 0000000000..08bb22e08e
--- /dev/null
+++ b/dom/animation/KeyframeEffectParams.h
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+#ifndef mozilla_KeyframeEffectParams_h
+#define mozilla_KeyframeEffectParams_h
+
+#include "mozilla/dom/KeyframeEffectBinding.h" // IterationCompositeOperation
+#include "mozilla/PseudoStyleType.h" // PseudoStyleType
+
+namespace mozilla {
+
+struct KeyframeEffectParams {
+ KeyframeEffectParams() = default;
+ KeyframeEffectParams(dom::IterationCompositeOperation aIterationComposite,
+ dom::CompositeOperation aComposite,
+ PseudoStyleType aPseudoType)
+ : mIterationComposite(aIterationComposite),
+ mComposite(aComposite),
+ mPseudoType(aPseudoType) {}
+ explicit KeyframeEffectParams(dom::CompositeOperation aComposite)
+ : mComposite(aComposite) {}
+
+ dom::IterationCompositeOperation mIterationComposite =
+ dom::IterationCompositeOperation::Replace;
+ dom::CompositeOperation mComposite = dom::CompositeOperation::Replace;
+ PseudoStyleType mPseudoType = PseudoStyleType::NotPseudo;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_KeyframeEffectParams_h
diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp
new file mode 100644
index 0000000000..35234e0a9d
--- /dev/null
+++ b/dom/animation/KeyframeUtils.cpp
@@ -0,0 +1,1255 @@
+/* -*- 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/KeyframeUtils.h"
+
+#include <algorithm> // For std::stable_sort, std::min
+#include <utility>
+
+#include "jsapi.h" // For most JSAPI
+#include "js/ForOfIterator.h" // For JS::ForOfIterator
+#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetProperty, JS_GetPropertyById
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/RangedArray.h"
+#include "mozilla/ServoBindingTypes.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/ServoCSSParser.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/TimingParams.h"
+#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
+#include "mozilla/dom/BindingCallContext.h"
+#include "mozilla/dom/Document.h" // For Document::AreWebAnimationsImplicitKeyframesEnabled
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffect.h" // For PropertyValuesPair etc.
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h"
+#include "nsCSSPseudoElements.h" // For PseudoStyleType
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h" // For GetContextForContent
+#include "nsIScriptError.h"
+#include "nsPresContextInlines.h"
+#include "nsTArray.h"
+
+using mozilla::dom::Nullable;
+
+namespace mozilla {
+
+// ------------------------------------------------------------------
+//
+// Internal data types
+//
+// ------------------------------------------------------------------
+
+// For the aAllowList parameter of AppendStringOrStringSequence and
+// GetPropertyValuesPairs.
+enum class ListAllowance { eDisallow, eAllow };
+
+/**
+ * A property-values pair obtained from the open-ended properties
+ * discovered on a regular keyframe or property-indexed keyframe object.
+ *
+ * Single values (as required by a regular keyframe, and as also supported
+ * on property-indexed keyframes) are stored as the only element in
+ * mValues.
+ */
+struct PropertyValuesPair {
+ nsCSSPropertyID mProperty;
+ nsTArray<nsCString> mValues;
+};
+
+/**
+ * An additional property (for a property-values pair) found on a
+ * BaseKeyframe or BasePropertyIndexedKeyframe object.
+ */
+struct AdditionalProperty {
+ nsCSSPropertyID mProperty;
+ size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs.
+
+ struct PropertyComparator {
+ bool Equals(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const {
+ return aLhs.mProperty == aRhs.mProperty;
+ }
+ bool LessThan(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const {
+ return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ }
+ };
+};
+
+/**
+ * Data for a segment in a keyframe animation of a given property
+ * whose value is a StyleAnimationValue.
+ *
+ * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
+ * to gather data for each individual segment.
+ */
+struct KeyframeValueEntry {
+ nsCSSPropertyID mProperty;
+ AnimationValue mValue;
+
+ float mOffset;
+ Maybe<StyleComputedTimingFunction> mTimingFunction;
+ dom::CompositeOperation mComposite;
+
+ struct PropertyOffsetComparator {
+ static bool Equals(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs) {
+ return aLhs.mProperty == aRhs.mProperty && aLhs.mOffset == aRhs.mOffset;
+ }
+ static bool LessThan(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs) {
+ // First, sort by property IDL name.
+ int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ if (order != 0) {
+ return order < 0;
+ }
+
+ // Then, by offset.
+ return aLhs.mOffset < aRhs.mOffset;
+ }
+ };
+};
+
+class ComputedOffsetComparator {
+ public:
+ static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) {
+ return aLhs.mComputedOffset == aRhs.mComputedOffset;
+ }
+
+ static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
+ return aLhs.mComputedOffset < aRhs.mComputedOffset;
+ }
+};
+
+// ------------------------------------------------------------------
+//
+// Internal helper method declarations
+//
+// ------------------------------------------------------------------
+
+static void GetKeyframeListFromKeyframeSequence(
+ JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv);
+
+static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
+ JS::ForOfIterator& aIterator,
+ const char* aContext,
+ nsTArray<Keyframe>& aResult);
+
+static bool GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult);
+
+static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsCString>& aValues);
+
+static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
+ JS::Handle<JS::Value> aValue);
+
+static Maybe<PropertyValuePair> MakePropertyValuePair(
+ nsCSSPropertyID aProperty, const nsACString& aStringValue,
+ dom::Document* aDocument);
+
+static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
+
+#ifdef DEBUG
+static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
+
+#endif
+
+static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
+ const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
+ PseudoStyleType aPseudoType, const ComputedStyle* aComputedValues);
+
+static void BuildSegmentsFromValueEntries(
+ nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult);
+
+static void GetKeyframeListFromPropertyIndexedKeyframe(
+ JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult, ErrorResult& aRv);
+
+static bool HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+ dom::Document* aDocument);
+
+static void DistributeRange(const Range<Keyframe>& aRange);
+
+// ------------------------------------------------------------------
+//
+// Public API
+//
+// ------------------------------------------------------------------
+
+/* static */
+nsTArray<Keyframe> KeyframeUtils::GetKeyframesFromObject(
+ JSContext* aCx, dom::Document* aDocument, JS::Handle<JSObject*> aFrames,
+ const char* aContext, ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed());
+
+ nsTArray<Keyframe> keyframes;
+
+ if (!aFrames) {
+ // The argument was explicitly null meaning no keyframes.
+ return keyframes;
+ }
+
+ // At this point we know we have an object. We try to convert it to a
+ // sequence of keyframes first, and if that fails due to not being iterable,
+ // we try to convert it to a property-indexed keyframe.
+ JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return keyframes;
+ }
+
+ if (iter.valueIsIterable()) {
+ GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes,
+ aContext, aRv);
+ } else {
+ GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
+ keyframes, aRv);
+ }
+
+ if (aRv.Failed()) {
+ MOZ_ASSERT(keyframes.IsEmpty(),
+ "Should not set any keyframes when there is an error");
+ return keyframes;
+ }
+
+ if (!dom::Document::AreWebAnimationsImplicitKeyframesEnabled(aCx, nullptr) &&
+ HasImplicitKeyframeValues(keyframes, aDocument)) {
+ keyframes.Clear();
+ aRv.ThrowNotSupportedError(
+ "Animation to or from an underlying value is not yet supported");
+ }
+
+ return keyframes;
+}
+
+/* static */
+void KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes) {
+ if (aKeyframes.IsEmpty()) {
+ return;
+ }
+
+ // If the first keyframe has an unspecified offset, fill it in with 0%.
+ // If there is only a single keyframe, then it gets 100%.
+ if (aKeyframes.Length() > 1) {
+ Keyframe& firstElement = aKeyframes[0];
+ firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
+ // We will fill in the last keyframe's offset below
+ } else {
+ Keyframe& lastElement = aKeyframes.LastElement();
+ lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
+ }
+
+ // Fill in remaining missing offsets.
+ const Keyframe* const last = &aKeyframes.LastElement();
+ const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
+ RangedPtr<Keyframe> keyframeA = begin;
+ while (keyframeA != last) {
+ // Find keyframe A and keyframe B *between* which we will apply spacing.
+ RangedPtr<Keyframe> keyframeB = keyframeA + 1;
+ while (keyframeB->mOffset.isNothing() && keyframeB != last) {
+ ++keyframeB;
+ }
+ keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
+
+ // Fill computed offsets in (keyframe A, keyframe B).
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
+ keyframeA = keyframeB;
+ }
+}
+
+/* static */
+nsTArray<AnimationProperty> KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+ const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
+ PseudoStyleType aPseudoType, const ComputedStyle* aStyle,
+ dom::CompositeOperation aEffectComposite) {
+ nsTArray<AnimationProperty> result;
+
+ const nsTArray<ComputedKeyframeValues> computedValues =
+ GetComputedKeyframeValues(aKeyframes, aElement, aPseudoType, aStyle);
+ if (computedValues.IsEmpty()) {
+ // In rare cases GetComputedKeyframeValues might fail and return an empty
+ // array, in which case we likewise return an empty array from here.
+ return result;
+ }
+
+ MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
+ "Array length mismatch");
+
+ nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
+
+ const size_t len = aKeyframes.Length();
+ for (size_t i = 0; i < len; ++i) {
+ const Keyframe& frame = aKeyframes[i];
+ for (auto& value : computedValues[i]) {
+ MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Invalid computed offset");
+ KeyframeValueEntry* entry = entries.AppendElement();
+ entry->mOffset = frame.mComputedOffset;
+ entry->mProperty = value.mProperty;
+ entry->mValue = value.mValue;
+ entry->mTimingFunction = frame.mTimingFunction;
+ // The following assumes that CompositeOperation is a strict subset of
+ // CompositeOperationOrAuto.
+ entry->mComposite =
+ frame.mComposite == dom::CompositeOperationOrAuto::Auto
+ ? aEffectComposite
+ : static_cast<dom::CompositeOperation>(frame.mComposite);
+ }
+ }
+
+ BuildSegmentsFromValueEntries(entries, result);
+ return result;
+}
+
+/* static */
+bool KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty) {
+ // Regardless of the backend type, treat the 'display' property as not
+ // animatable. (Servo will report it as being animatable, since it is
+ // in fact animatable by SMIL.)
+ if (aProperty == eCSSProperty_display) {
+ return false;
+ }
+ return Servo_Property_IsAnimatable(aProperty);
+}
+
+// ------------------------------------------------------------------
+//
+// Internal helpers
+//
+// ------------------------------------------------------------------
+
+/**
+ * Converts a JS object to an IDL sequence<Keyframe>.
+ *
+ * @param aCx The JSContext corresponding to |aIterator|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aIterator An already-initialized ForOfIterator for the JS
+ * object to iterate over as a sequence.
+ * @param aResult The array into which the resulting Keyframe objects will be
+ * appended.
+ * @param aContext The context string to prepend to thrown exceptions.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void GetKeyframeListFromKeyframeSequence(
+ JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed());
+ MOZ_ASSERT(aResult.IsEmpty());
+
+ // Convert the object in aIterator to a sequence of keyframes producing
+ // an array of Keyframe objects.
+ if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aContext, aResult)) {
+ aResult.Clear();
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ // If the sequence<> had zero elements, we won't generate any
+ // keyframes.
+ if (aResult.IsEmpty()) {
+ return;
+ }
+
+ // Check that the keyframes are loosely sorted and with values all
+ // between 0% and 100%.
+ if (!HasValidOffsets(aResult)) {
+ aResult.Clear();
+ aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
+ return;
+ }
+}
+
+/**
+ * Converts a JS object wrapped by the given JS::ForIfIterator to an
+ * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
+ * aResult.
+ */
+static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
+ JS::ForOfIterator& aIterator,
+ const char* aContext,
+ nsTArray<Keyframe>& aResult) {
+ JS::Rooted<JS::Value> value(aCx);
+ // Parsing errors should only be reported after we have finished iterating
+ // through all values. If we have any early returns while iterating, we should
+ // ignore parsing errors.
+ IgnoredErrorResult parseEasingResult;
+
+ for (;;) {
+ bool done;
+ if (!aIterator.next(&value, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ // Each value found when iterating the object must be an object
+ // or null/undefined (which gets treated as a default {} dictionary
+ // value).
+ if (!value.isObject() && !value.isNullOrUndefined()) {
+ dom::ThrowErrorMessage<dom::MSG_NOT_OBJECT>(
+ aCx, aContext, "Element of sequence<Keyframe> argument");
+ return false;
+ }
+
+ // Convert the JS value into a BaseKeyframe dictionary value.
+ dom::binding_detail::FastBaseKeyframe keyframeDict;
+ dom::BindingCallContext callCx(aCx, aContext);
+ if (!keyframeDict.Init(callCx, value,
+ "Element of sequence<Keyframe> argument")) {
+ // This may happen if the value type of the member of BaseKeyframe is
+ // invalid. e.g. `offset` only accept a double value, so if we provide a
+ // string, we enter this branch.
+ // Besides, keyframeDict.Init() should throw a Type Error message already,
+ // so we don't have to do it again.
+ return false;
+ }
+
+ Keyframe* keyframe = aResult.AppendElement(fallible);
+ if (!keyframe) {
+ return false;
+ }
+
+ if (!keyframeDict.mOffset.IsNull()) {
+ keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
+ }
+
+ if (StaticPrefs::dom_animations_api_compositing_enabled()) {
+ keyframe->mComposite = keyframeDict.mComposite;
+ }
+
+ // Look for additional property-values pairs on the object.
+ nsTArray<PropertyValuesPair> propertyValuePairs;
+ if (value.isObject()) {
+ JS::Rooted<JSObject*> object(aCx, &value.toObject());
+ if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eDisallow,
+ propertyValuePairs)) {
+ return false;
+ }
+ }
+
+ if (!parseEasingResult.Failed()) {
+ keyframe->mTimingFunction =
+ TimingParams::ParseEasing(keyframeDict.mEasing, parseEasingResult);
+ // Even if the above fails, we still need to continue reading off all the
+ // properties since checking the validity of easing should be treated as
+ // a separate step that happens *after* all the other processing in this
+ // loop since (since it is never likely to be handled by WebIDL unlike the
+ // rest of this loop).
+ }
+
+ for (PropertyValuesPair& pair : propertyValuePairs) {
+ MOZ_ASSERT(pair.mValues.Length() == 1);
+
+ Maybe<PropertyValuePair> valuePair =
+ MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument);
+ if (!valuePair) {
+ continue;
+ }
+ keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
+
+#ifdef DEBUG
+ // When we go to convert keyframes into arrays of property values we
+ // call StyleAnimation::ComputeValues. This should normally return true
+ // but in order to test the case where it does not, BaseKeyframeDict
+ // includes a chrome-only member that can be set to indicate that
+ // ComputeValues should fail for shorthand property values on that
+ // keyframe.
+ if (nsCSSProps::IsShorthand(pair.mProperty) &&
+ keyframeDict.mSimulateComputeValuesFailure) {
+ MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
+ }
+#endif
+ }
+ }
+
+ // Throw any errors we encountered while parsing 'easing' properties.
+ if (parseEasingResult.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Reads the property-values pairs from the specified JS object.
+ *
+ * @param aObject The JS object to look at.
+ * @param aAllowLists If eAllow, values will be converted to
+ * (DOMString or sequence<DOMString); if eDisallow, values
+ * will be converted to DOMString.
+ * @param aResult The array into which the enumerated property-values
+ * pairs will be stored.
+ * @return false on failure or JS exception thrown while interacting
+ * with aObject; true otherwise.
+ */
+static bool GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult) {
+ nsTArray<AdditionalProperty> properties;
+
+ // Iterate over all the properties on aObject and append an
+ // entry to properties for them.
+ //
+ // We don't compare the jsids that we encounter with those for
+ // the explicit dictionary members, since we know that none
+ // of the CSS property IDL names clash with them.
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, aObject, &ids)) {
+ return false;
+ }
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSCString propName;
+ if (!propName.init(aCx, ids[i])) {
+ return false;
+ }
+
+ // Basically, we have to handle "cssOffset" and "cssFloat" specially here:
+ // "cssOffset" => eCSSProperty_offset
+ // "cssFloat" => eCSSProperty_float
+ // This means if the attribute is the string "cssOffset"/"cssFloat", we use
+ // CSS "offset"/"float" property.
+ // https://drafts.csswg.org/web-animations/#property-name-conversion
+ nsCSSPropertyID property = nsCSSPropertyID::eCSSProperty_UNKNOWN;
+ if (propName.EqualsLiteral("cssOffset")) {
+ property = nsCSSPropertyID::eCSSProperty_offset;
+ } else if (propName.EqualsLiteral("cssFloat")) {
+ property = nsCSSPropertyID::eCSSProperty_float;
+ } else if (!propName.EqualsLiteral("offset") &&
+ !propName.EqualsLiteral("float")) {
+ property = nsCSSProps::LookupPropertyByIDLName(
+ propName, CSSEnabledState::ForAllContent);
+ }
+
+ if (KeyframeUtils::IsAnimatableProperty(property)) {
+ AdditionalProperty* p = properties.AppendElement();
+ p->mProperty = property;
+ p->mJsidIndex = i;
+ }
+ }
+
+ // Sort the entries by IDL name and then get each value and
+ // convert it either to a DOMString or to a
+ // (DOMString or sequence<DOMString>), depending on aAllowLists,
+ // and build up aResult.
+ properties.Sort(AdditionalProperty::PropertyComparator());
+
+ for (AdditionalProperty& p : properties) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
+ return false;
+ }
+ PropertyValuesPair* pair = aResult.AppendElement();
+ pair->mProperty = p.mProperty;
+ if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
+ pair->mValues)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString, if aAllowLists is eDisallow, or
+ * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
+ * The resulting strings are appended to aValues.
+ */
+static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsCString>& aValues) {
+ if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
+ // The value is an object, and we want to allow lists; convert
+ // aValue to (DOMString or sequence<DOMString>).
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
+ return false;
+ }
+ if (iter.valueIsIterable()) {
+ // If the object is iterable, convert it to sequence<DOMString>.
+ JS::Rooted<JS::Value> element(aCx);
+ for (;;) {
+ bool done;
+ if (!iter.next(&element, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ if (!AppendValueAsString(aCx, aValues, element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ // Either the object is not iterable, or aAllowLists doesn't want
+ // a list; convert it to DOMString.
+ if (!AppendValueAsString(aCx, aValues, aValue)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString and appends it to aValues.
+ */
+static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
+ JS::Handle<JS::Value> aValue) {
+ return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
+ *aValues.AppendElement());
+}
+
+static void ReportInvalidPropertyValueToConsole(
+ nsCSSPropertyID aProperty, const nsACString& aInvalidPropertyValue,
+ dom::Document* aDoc) {
+ AutoTArray<nsString, 2> params;
+ params.AppendElement(NS_ConvertUTF8toUTF16(aInvalidPropertyValue));
+ CopyASCIItoUTF16(nsCSSProps::GetStringValue(aProperty),
+ *params.AppendElement());
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Animation"_ns,
+ aDoc, nsContentUtils::eDOM_PROPERTIES,
+ "InvalidKeyframePropertyValue", params);
+}
+
+/**
+ * Construct a PropertyValuePair parsing the given string into a suitable
+ * nsCSSValue object.
+ *
+ * @param aProperty The CSS property.
+ * @param aStringValue The property value to parse.
+ * @param aDocument The document to use when parsing.
+ * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
+ * an invalid property value.
+ */
+static Maybe<PropertyValuePair> MakePropertyValuePair(
+ nsCSSPropertyID aProperty, const nsACString& aStringValue,
+ dom::Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+ Maybe<PropertyValuePair> result;
+
+ ServoCSSParser::ParsingEnvironment env =
+ ServoCSSParser::GetParsingEnvironment(aDocument);
+ RefPtr<StyleLockedDeclarationBlock> servoDeclarationBlock =
+ ServoCSSParser::ParseProperty(aProperty, aStringValue, env);
+
+ if (servoDeclarationBlock) {
+ result.emplace(aProperty, std::move(servoDeclarationBlock));
+ } else {
+ ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
+ }
+ return result;
+}
+
+/**
+ * Checks that the given keyframes are loosely ordered (each keyframe's
+ * offset that is not null is greater than or equal to the previous
+ * non-null offset) and that all values are within the range [0.0, 1.0].
+ *
+ * @return true if the keyframes' offsets are correctly ordered and
+ * within range; false otherwise.
+ */
+static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes) {
+ double offset = 0.0;
+ for (const Keyframe& keyframe : aKeyframes) {
+ if (keyframe.mOffset) {
+ double thisOffset = keyframe.mOffset.value();
+ if (thisOffset < offset || thisOffset > 1.0f) {
+ return false;
+ }
+ offset = thisOffset;
+ }
+ }
+ return true;
+}
+
+#ifdef DEBUG
+/**
+ * Takes a property-value pair for a shorthand property and modifies the
+ * value to indicate that when we call StyleAnimationValue::ComputeValues on
+ * that value we should behave as if that function had failed.
+ *
+ * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
+ * a shorthand property.
+ */
+static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair) {
+ MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
+ "Only shorthand property values can be marked as failure values");
+
+ aPair.mSimulateComputeValuesFailure = true;
+}
+
+#endif
+
+/**
+ * The variation of the above function. This is for Servo backend.
+ */
+static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
+ const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
+ PseudoStyleType aPseudoType, const ComputedStyle* aComputedStyle) {
+ MOZ_ASSERT(aElement);
+
+ nsTArray<ComputedKeyframeValues> result;
+
+ nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
+ if (!presContext) {
+ // This has been reported to happen with some combinations of content
+ // (particularly involving resize events and layout flushes? See bug 1407898
+ // and bug 1408420) but no reproducible steps have been found.
+ // For now we just return an empty array.
+ return result;
+ }
+
+ result = presContext->StyleSet()->GetComputedKeyframeValuesFor(
+ aKeyframes, aElement, aPseudoType, aComputedStyle);
+ return result;
+}
+
+static void AppendInitialSegment(AnimationProperty* aAnimationProperty,
+ const KeyframeValueEntry& aFirstEntry) {
+ AnimationPropertySegment* segment =
+ aAnimationProperty->mSegments.AppendElement();
+ segment->mFromKey = 0.0f;
+ segment->mToKey = aFirstEntry.mOffset;
+ segment->mToValue = aFirstEntry.mValue;
+ segment->mToComposite = aFirstEntry.mComposite;
+}
+
+static void AppendFinalSegment(AnimationProperty* aAnimationProperty,
+ const KeyframeValueEntry& aLastEntry) {
+ AnimationPropertySegment* segment =
+ aAnimationProperty->mSegments.AppendElement();
+ segment->mFromKey = aLastEntry.mOffset;
+ segment->mFromValue = aLastEntry.mValue;
+ segment->mFromComposite = aLastEntry.mComposite;
+ segment->mToKey = 1.0f;
+ segment->mTimingFunction = aLastEntry.mTimingFunction;
+}
+
+// Returns a newly created AnimationProperty if one was created to fill-in the
+// missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
+// becase we don't support implicit keyframes).
+static AnimationProperty* HandleMissingInitialKeyframe(
+ nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry) {
+ MOZ_ASSERT(aEntry.mOffset != 0.0f,
+ "The offset of the entry should not be 0.0");
+
+ // If the preference for implicit keyframes is not enabled, don't fill in the
+ // missing keyframe.
+ if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
+ return nullptr;
+ }
+
+ AnimationProperty* result = aResult.AppendElement();
+ result->mProperty = aEntry.mProperty;
+
+ AppendInitialSegment(result, aEntry);
+
+ return result;
+}
+
+static void HandleMissingFinalKeyframe(
+ nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry,
+ AnimationProperty* aCurrentAnimationProperty) {
+ MOZ_ASSERT(aEntry.mOffset != 1.0f,
+ "The offset of the entry should not be 1.0");
+
+ // If the preference for implicit keyframes is not enabled, don't fill
+ // in the missing keyframe.
+ if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
+ // If we have already appended a new entry for the property so we have to
+ // remove it.
+ if (aCurrentAnimationProperty) {
+ aResult.RemoveLastElement();
+ }
+ return;
+ }
+
+ // If |aCurrentAnimationProperty| is nullptr, that means this is the first
+ // entry for the property, we have to append a new AnimationProperty for this
+ // property.
+ if (!aCurrentAnimationProperty) {
+ aCurrentAnimationProperty = aResult.AppendElement();
+ aCurrentAnimationProperty->mProperty = aEntry.mProperty;
+
+ // If we have only one entry whose offset is neither 1 nor 0 for this
+ // property, we need to append the initial segment as well.
+ if (aEntry.mOffset != 0.0f) {
+ AppendInitialSegment(aCurrentAnimationProperty, aEntry);
+ }
+ }
+ AppendFinalSegment(aCurrentAnimationProperty, aEntry);
+}
+
+/**
+ * Builds an array of AnimationProperty objects to represent the keyframe
+ * animation segments in aEntries.
+ */
+static void BuildSegmentsFromValueEntries(
+ nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult) {
+ if (aEntries.IsEmpty()) {
+ return;
+ }
+
+ // Sort the KeyframeValueEntry objects so that all entries for a given
+ // property are together, and the entries are sorted by offset otherwise.
+ std::stable_sort(aEntries.begin(), aEntries.end(),
+ &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
+
+ // For a given index i, we want to generate a segment from aEntries[i]
+ // to aEntries[j], if:
+ //
+ // * j > i,
+ // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
+ // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
+ //
+ // That will eliminate runs of same offset/property values where there's no
+ // point generating zero length segments in the middle of the animation.
+ //
+ // Additionally we need to generate a zero length segment at offset 0 and at
+ // offset 1, if we have multiple values for a given property at that offset,
+ // since we need to retain the very first and very last value so they can
+ // be used for reverse and forward filling.
+ //
+ // Typically, for each property in |aEntries|, we expect there to be at least
+ // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
+ // However, since it is possible that when building |aEntries|, the call to
+ // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
+ // Furthermore, if additive animation is disabled, the following loop takes
+ // care to identify properties that lack a value at offset 0.0/1.0 and drops
+ // those properties from |aResult|.
+
+ nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
+ AnimationProperty* animationProperty = nullptr;
+
+ size_t i = 0, n = aEntries.Length();
+
+ while (i < n) {
+ // If we've reached the end of the array of entries, synthesize a final (and
+ // initial) segment if necessary.
+ if (i + 1 == n) {
+ if (aEntries[i].mOffset != 1.0f) {
+ HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
+ } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
+ // If the last entry with offset 1 and no animation property, that means
+ // it is the only entry for this property so append a single segment
+ // from 0 offset to |aEntry[i].offset|.
+ Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
+ }
+ animationProperty = nullptr;
+ break;
+ }
+
+ MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
+ aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
+ "Each entry should specify a valid property");
+
+ // No keyframe for this property at offset 0.
+ if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) {
+ // If we don't support additive animation we can't fill in the missing
+ // keyframes and we should just skip this property altogether. Since the
+ // entries are sorted by offset for a given property, and since we don't
+ // update |lastProperty|, we will keep hitting this condition until we
+ // change property.
+ animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
+ if (animationProperty) {
+ lastProperty = aEntries[i].mProperty;
+ } else {
+ // Skip this entry if we did not handle the missing entry.
+ ++i;
+ continue;
+ }
+ }
+
+ // Skip this entry if the next entry has the same offset except for initial
+ // and final ones. We will handle missing keyframe in the next loop
+ // if the property is changed on the next entry.
+ if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
+ aEntries[i].mOffset == aEntries[i + 1].mOffset &&
+ aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
+ ++i;
+ continue;
+ }
+
+ // No keyframe for this property at offset 1.
+ if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
+ aEntries[i].mOffset != 1.0f) {
+ HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
+ // Move on to new property.
+ animationProperty = nullptr;
+ ++i;
+ continue;
+ }
+
+ // Starting from i + 1, determine the next [i, j] interval from which to
+ // generate a segment. Basically, j is i + 1, but there are some special
+ // cases for offset 0 and 1, so we need to handle them specifically.
+ // Note: From this moment, we make sure [i + 1] is valid and
+ // there must be an initial entry (i.e. mOffset = 0.0) and
+ // a final entry (i.e. mOffset = 1.0). Besides, all the entries
+ // with the same offsets except for initial/final ones are filtered
+ // out already.
+ size_t j = i + 1;
+ if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
+ // We need to generate an initial zero-length segment.
+ MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
+ while (j + 1 < n && aEntries[j + 1].mOffset == 0.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else if (aEntries[i].mOffset == 1.0f) {
+ if (aEntries[i + 1].mOffset == 1.0f &&
+ aEntries[i + 1].mProperty == aEntries[i].mProperty) {
+ // We need to generate a final zero-length segment.
+ while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else {
+ // New property.
+ MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
+ animationProperty = nullptr;
+ ++i;
+ continue;
+ }
+ }
+
+ // If we've moved on to a new property, create a new AnimationProperty
+ // to insert segments into.
+ if (aEntries[i].mProperty != lastProperty) {
+ MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
+ MOZ_ASSERT(!animationProperty);
+ animationProperty = aResult.AppendElement();
+ animationProperty->mProperty = aEntries[i].mProperty;
+ lastProperty = aEntries[i].mProperty;
+ }
+
+ MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
+
+ // Now generate the segment.
+ AnimationPropertySegment* segment =
+ animationProperty->mSegments.AppendElement();
+ segment->mFromKey = aEntries[i].mOffset;
+ segment->mToKey = aEntries[j].mOffset;
+ segment->mFromValue = aEntries[i].mValue;
+ segment->mToValue = aEntries[j].mValue;
+ segment->mTimingFunction = aEntries[i].mTimingFunction;
+ segment->mFromComposite = aEntries[i].mComposite;
+ segment->mToComposite = aEntries[j].mComposite;
+
+ i = j;
+ }
+}
+
+/**
+ * Converts a JS object representing a property-indexed keyframe into
+ * an array of Keyframe objects.
+ *
+ * @param aCx The JSContext for |aValue|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aValue The JS object.
+ * @param aResult The array into which the resulting AnimationProperty
+ * objects will be appended.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void GetKeyframeListFromPropertyIndexedKeyframe(
+ JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult, ErrorResult& aRv) {
+ MOZ_ASSERT(aValue.isObject());
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ // Convert the object to a property-indexed keyframe dictionary to
+ // get its explicit dictionary members.
+ dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
+ // XXXbz Pass in the method name from callers and set up a BindingCallContext?
+ if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument")) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Get all the property--value-list pairs off the object.
+ JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
+ nsTArray<PropertyValuesPair> propertyValuesPairs;
+ if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
+ propertyValuesPairs)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Create a set of keyframes for each property.
+ nsTHashMap<nsFloatHashKey, Keyframe> processedKeyframes;
+ for (const PropertyValuesPair& pair : propertyValuesPairs) {
+ size_t count = pair.mValues.Length();
+ if (count == 0) {
+ // No animation values for this property.
+ continue;
+ }
+
+ // If we only have one value, we should animate from the underlying value
+ // but not if the pref for supporting implicit keyframes is disabled.
+ if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled() &&
+ count == 1) {
+ aRv.ThrowNotSupportedError(
+ "Animation to or from an underlying value is not yet supported");
+ return;
+ }
+
+ size_t n = pair.mValues.Length() - 1;
+ size_t i = 0;
+
+ for (const nsCString& stringValue : pair.mValues) {
+ // For single-valued lists, the single value should be added to a
+ // keyframe with offset 1.
+ double offset = n ? i++ / double(n) : 1;
+ Keyframe& keyframe = processedKeyframes.LookupOrInsert(offset);
+ if (keyframe.mPropertyValues.IsEmpty()) {
+ keyframe.mComputedOffset = offset;
+ }
+
+ Maybe<PropertyValuePair> valuePair =
+ MakePropertyValuePair(pair.mProperty, stringValue, aDocument);
+ if (!valuePair) {
+ continue;
+ }
+ keyframe.mPropertyValues.AppendElement(std::move(valuePair.ref()));
+ }
+ }
+
+ aResult.SetCapacity(processedKeyframes.Count());
+ std::transform(processedKeyframes.begin(), processedKeyframes.end(),
+ MakeBackInserter(aResult), [](auto& entry) {
+ return std::move(*entry.GetModifiableData());
+ });
+
+ aResult.Sort(ComputedOffsetComparator());
+
+ // Fill in any specified offsets
+ //
+ // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of
+ // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
+ const FallibleTArray<Nullable<double>>* offsets = nullptr;
+ AutoTArray<Nullable<double>, 1> singleOffset;
+ auto& offset = keyframeDict.mOffset;
+ if (offset.IsDouble()) {
+ singleOffset.AppendElement(offset.GetAsDouble());
+ // dom::Sequence is a fallible but AutoTArray is infallible and we need to
+ // point to one or the other. Fortunately, fallible and infallible array
+ // types can be implicitly converted provided they are const.
+ const FallibleTArray<Nullable<double>>& asFallibleArray = singleOffset;
+ offsets = &asFallibleArray;
+ } else if (offset.IsDoubleOrNullSequence()) {
+ offsets = &offset.GetAsDoubleOrNullSequence();
+ }
+ // If offset.IsNull() is true, then we want to leave the mOffset member of
+ // each keyframe with its initialized value of null. By leaving |offsets|
+ // as nullptr here, we skip updating mOffset below.
+
+ size_t offsetsToFill =
+ offsets ? std::min(offsets->Length(), aResult.Length()) : 0;
+ for (size_t i = 0; i < offsetsToFill; i++) {
+ if (!offsets->ElementAt(i).IsNull()) {
+ aResult[i].mOffset.emplace(offsets->ElementAt(i).Value());
+ }
+ }
+
+ // Check that the keyframes are loosely sorted and that any specified offsets
+ // are between 0.0 and 1.0 inclusive.
+ //
+ // This corresponds to steps 6-7 of
+ // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
+ //
+ // In the spec, TypeErrors arising from invalid offsets and easings are thrown
+ // at the end of the procedure since it assumes we initially store easing
+ // values as strings and then later parse them.
+ //
+ // However, we will parse easing members immediately when we process them
+ // below. In order to maintain the relative order in which TypeErrors are
+ // thrown according to the spec, namely exceptions arising from invalid
+ // offsets are thrown before exceptions arising from invalid easings, we check
+ // the offsets here.
+ if (!HasValidOffsets(aResult)) {
+ aResult.Clear();
+ aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
+ return;
+ }
+
+ // Fill in any easings.
+ //
+ // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of
+ // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
+ FallibleTArray<Maybe<StyleComputedTimingFunction>> easings;
+ auto parseAndAppendEasing = [&](const nsACString& easingString,
+ ErrorResult& aRv) {
+ auto easing = TimingParams::ParseEasing(easingString, aRv);
+ if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ }
+ };
+
+ auto& easing = keyframeDict.mEasing;
+ if (easing.IsUTF8String()) {
+ parseAndAppendEasing(easing.GetAsUTF8String(), aRv);
+ if (aRv.Failed()) {
+ aResult.Clear();
+ return;
+ }
+ } else {
+ for (const auto& easingString : easing.GetAsUTF8StringSequence()) {
+ parseAndAppendEasing(easingString, aRv);
+ if (aRv.Failed()) {
+ aResult.Clear();
+ return;
+ }
+ }
+ }
+
+ // If |easings| is empty, then we are supposed to fill it in with the value
+ // "linear" and then repeat the list as necessary.
+ //
+ // However, for Keyframe.mTimingFunction we represent "linear" as a None
+ // value. Since we have not assigned 'mTimingFunction' for any of the
+ // keyframes in |aResult| they will already have their initial None value
+ // (i.e. linear). As a result, if |easings| is empty, we don't need to do
+ // anything.
+ if (!easings.IsEmpty()) {
+ for (size_t i = 0; i < aResult.Length(); i++) {
+ aResult[i].mTimingFunction = easings[i % easings.Length()];
+ }
+ }
+
+ // Fill in any composite operations.
+ //
+ // This corresponds to step 5, "Otherwise," branch, substep 12 of
+ // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
+ if (StaticPrefs::dom_animations_api_compositing_enabled()) {
+ const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
+ AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
+ auto& composite = keyframeDict.mComposite;
+ if (composite.IsCompositeOperationOrAuto()) {
+ singleCompositeOp.AppendElement(
+ composite.GetAsCompositeOperationOrAuto());
+ const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
+ singleCompositeOp;
+ compositeOps = &asFallibleArray;
+ } else if (composite.IsCompositeOperationOrAutoSequence()) {
+ compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
+ }
+
+ // Fill in and repeat as needed.
+ if (compositeOps && !compositeOps->IsEmpty()) {
+ size_t length = compositeOps->Length();
+ for (size_t i = 0; i < aResult.Length(); i++) {
+ aResult[i].mComposite = compositeOps->ElementAt(i % length);
+ }
+ }
+ }
+}
+
+/**
+ * Returns true if the supplied set of keyframes has keyframe values for
+ * any property for which it does not also supply a value for the 0% and 100%
+ * offsets. The check is not entirely accurate but should detect most common
+ * cases.
+ *
+ * @param aKeyframes The set of keyframes to analyze.
+ * @param aDocument The document to use when parsing keyframes so we can
+ * try to detect where we have an invalid value at 0%/100%.
+ */
+static bool HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+ dom::Document* aDocument) {
+ // We are looking to see if that every property referenced in |aKeyframes|
+ // has a valid property at offset 0.0 and 1.0. The check as to whether a
+ // property is valid or not, however, is not precise. We only check if the
+ // property can be parsed, NOT whether it can also be converted to a
+ // StyleAnimationValue since doing that requires a target element bound to
+ // a document which we might not always have at the point where we want to
+ // perform this check.
+ //
+ // This is only a temporary measure until we ship implicit keyframes and
+ // remove the corresponding pref.
+ // So as long as this check catches most cases, and we don't do anything
+ // horrible in one of the cases we can't detect, it should be sufficient.
+
+ nsCSSPropertyIDSet properties; // All properties encountered.
+ nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
+ nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value.
+
+ auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
+ properties.AddProperty(aProperty);
+ if (aOffset == 0.0) {
+ propertiesWithFromValue.AddProperty(aProperty);
+ } else if (aOffset == 1.0) {
+ propertiesWithToValue.AddProperty(aProperty);
+ }
+ };
+
+ for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
+ const Keyframe& frame = aKeyframes[i];
+
+ // We won't have called DistributeKeyframes when this is called so
+ // we can't use frame.mComputedOffset. Instead we do a rough version
+ // of that algorithm that substitutes null offsets with 0.0 for the first
+ // frame, 1.0 for the last frame, and 0.5 for everything else.
+ double computedOffset = i == len - 1 ? 1.0 : i == 0 ? 0.0 : 0.5;
+ double offsetToUse = frame.mOffset ? frame.mOffset.value() : computedOffset;
+
+ for (const PropertyValuePair& pair : frame.mPropertyValues) {
+ if (nsCSSProps::IsShorthand(pair.mProperty)) {
+ MOZ_ASSERT(pair.mServoDeclarationBlock);
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(prop, pair.mProperty,
+ CSSEnabledState::ForAllContent) {
+ addToPropertySets(*prop, offsetToUse);
+ }
+ } else {
+ addToPropertySets(pair.mProperty, offsetToUse);
+ }
+ }
+ }
+
+ return !propertiesWithFromValue.Equals(properties) ||
+ !propertiesWithToValue.Equals(properties);
+}
+
+/**
+ * Distribute the offsets of all keyframes in between the endpoints of the
+ * given range.
+ *
+ * @param aRange The sequence of keyframes between whose endpoints we should
+ * distribute offsets.
+ */
+static void DistributeRange(const Range<Keyframe>& aRange) {
+ const Range<Keyframe> rangeToAdjust =
+ Range<Keyframe>(aRange.begin() + 1, aRange.end() - 1);
+ const size_t n = aRange.length() - 1;
+ const double startOffset = aRange[0].mComputedOffset;
+ const double diffOffset = aRange[n].mComputedOffset - startOffset;
+ for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
+ size_t index = iter - aRange.begin();
+ iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h
new file mode 100644
index 0000000000..311a091df8
--- /dev/null
+++ b/dom/animation/KeyframeUtils.h
@@ -0,0 +1,110 @@
+/* -*- 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/. */
+
+#ifndef mozilla_KeyframeUtils_h
+#define mozilla_KeyframeUtils_h
+
+#include "mozilla/KeyframeEffectParams.h" // For CompositeOperation
+#include "nsCSSPropertyID.h"
+#include "nsTArrayForwardDeclare.h" // For nsTArray
+#include "js/RootingAPI.h" // For JS::Handle
+
+struct JSContext;
+class JSObject;
+
+namespace mozilla {
+struct AnimationProperty;
+class ComputedStyle;
+
+enum class PseudoStyleType : uint8_t;
+class ErrorResult;
+struct Keyframe;
+struct PropertyStyleAnimationValuePair;
+
+namespace dom {
+class Document;
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+namespace mozilla {
+
+// Represents the set of property-value pairs on a Keyframe converted to
+// computed values.
+using ComputedKeyframeValues = nsTArray<PropertyStyleAnimationValuePair>;
+
+/**
+ * Utility methods for processing keyframes.
+ */
+class KeyframeUtils {
+ public:
+ /**
+ * Converts a JS value representing a property-indexed keyframe or a sequence
+ * of keyframes to an array of Keyframe objects.
+ *
+ * @param aCx The JSContext that corresponds to |aFrames|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aFrames The JS value, provided as an optional IDL |object?| value,
+ * that is the keyframe list specification.
+ * @param aContext Information about who is trying to get keyframes from the
+ * object, for use in error reporting. This must be be a non-null
+ * pointer representing a null-terminated ASCII string.
+ * @param aRv (out) Out-param to hold any error returned by this function.
+ * Must be initially empty.
+ * @return The set of processed keyframes. If an error occurs, aRv will be
+ * filled-in with the appropriate error code and an empty array will be
+ * returned.
+ */
+ static nsTArray<Keyframe> GetKeyframesFromObject(
+ JSContext* aCx, dom::Document* aDocument, JS::Handle<JSObject*> aFrames,
+ const char* aContext, ErrorResult& aRv);
+
+ /**
+ * Calculate the computed offset of keyframes by evenly distributing keyframes
+ * with a missing offset.
+ *
+ * @see
+ * https://drafts.csswg.org/web-animations/#calculating-computed-keyframes
+ *
+ * @param aKeyframes The set of keyframes to adjust.
+ */
+ static void DistributeKeyframes(nsTArray<Keyframe>& aKeyframes);
+
+ /**
+ * Converts an array of Keyframe objects into an array of AnimationProperty
+ * objects. This involves creating an array of computed values for each
+ * longhand property and determining the offset and timing function to use
+ * for each value.
+ *
+ * @param aKeyframes The input keyframes.
+ * @param aElement The context element.
+ * @param aStyle The computed style values.
+ * @param aEffectComposite The composite operation specified on the effect.
+ * For any keyframes in |aKeyframes| that do not specify a composite
+ * operation, this value will be used.
+ * @return The set of animation properties. If an error occurs, the returned
+ * array will be empty.
+ */
+ static nsTArray<AnimationProperty> GetAnimationPropertiesFromKeyframes(
+ const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
+ PseudoStyleType aPseudoType, const ComputedStyle* aStyle,
+ dom::CompositeOperation aEffectComposite);
+
+ /**
+ * Check if the property or, for shorthands, one or more of
+ * its subproperties, is animatable.
+ *
+ * @param aProperty The property to check.
+ * @param aBackend The style backend, Servo or Gecko, that should determine
+ * if the property is animatable or not.
+ * @return true if |aProperty| is animatable.
+ */
+ static bool IsAnimatableProperty(nsCSSPropertyID aProperty);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_KeyframeUtils_h
diff --git a/dom/animation/PendingAnimationTracker.cpp b/dom/animation/PendingAnimationTracker.cpp
new file mode 100644
index 0000000000..9993f0888a
--- /dev/null
+++ b/dom/animation/PendingAnimationTracker.cpp
@@ -0,0 +1,193 @@
+/* -*- 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 "PendingAnimationTracker.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/AnimationEffect.h"
+#include "mozilla/dom/AnimationTimeline.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsIFrame.h"
+#include "nsTransitionManager.h" // For CSSTransition
+
+using mozilla::dom::Nullable;
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker, mPlayPendingSet,
+ mPausePendingSet, mDocument)
+
+PendingAnimationTracker::PendingAnimationTracker(dom::Document* aDocument)
+ : mDocument(aDocument) {}
+
+void PendingAnimationTracker::AddPending(dom::Animation& aAnimation,
+ AnimationSet& aSet) {
+ aSet.Insert(&aAnimation);
+
+ // Schedule a paint. Otherwise animations that don't trigger a paint by
+ // themselves (e.g. CSS animations with an empty keyframes rule) won't
+ // start until something else paints.
+ EnsurePaintIsScheduled();
+}
+
+void PendingAnimationTracker::RemovePending(dom::Animation& aAnimation,
+ AnimationSet& aSet) {
+ aSet.Remove(&aAnimation);
+}
+
+bool PendingAnimationTracker::IsWaiting(const dom::Animation& aAnimation,
+ const AnimationSet& aSet) const {
+ return aSet.Contains(const_cast<dom::Animation*>(&aAnimation));
+}
+
+void PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(
+ const TimeStamp& aReadyTime) {
+ auto triggerAnimationsAtReadyTime = [aReadyTime](
+ AnimationSet& aAnimationSet) {
+ for (auto iter = aAnimationSet.begin(), end = aAnimationSet.end();
+ iter != end; ++iter) {
+ dom::Animation* animation = *iter;
+ dom::AnimationTimeline* timeline = animation->GetTimeline();
+
+ // If the animation does not have a timeline, just drop it from the map.
+ // The animation will detect that it is not being tracked and will trigger
+ // itself on the next tick where it has a timeline.
+ if (!timeline) {
+ aAnimationSet.Remove(iter);
+ continue;
+ }
+
+ MOZ_ASSERT(timeline->IsMonotonicallyIncreasing(),
+ "The non-monotonicially-increasing timeline should be in "
+ "ScrollTimelineAnimationTracker");
+
+ // When the timeline's refresh driver is under test control, its values
+ // have no correspondance to wallclock times so we shouldn't try to
+ // convert aReadyTime (which is a wallclock time) to a timeline value.
+ // Instead, the animation will be started/paused when the refresh driver
+ // is next advanced since this will trigger a call to
+ // TriggerPendingAnimationsNow.
+ if (!timeline->TracksWallclockTime()) {
+ continue;
+ }
+
+ Nullable<TimeDuration> readyTime = timeline->ToTimelineTime(aReadyTime);
+ animation->TriggerOnNextTick(readyTime);
+
+ aAnimationSet.Remove(iter);
+ }
+ };
+
+ triggerAnimationsAtReadyTime(mPlayPendingSet);
+ triggerAnimationsAtReadyTime(mPausePendingSet);
+
+ mHasPlayPendingGeometricAnimations =
+ mPlayPendingSet.Count() ? CheckState::Indeterminate : CheckState::Absent;
+}
+
+void PendingAnimationTracker::TriggerPendingAnimationsNow() {
+ auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) {
+ for (const auto& animation : aAnimationSet) {
+ animation->TriggerNow();
+ }
+ aAnimationSet.Clear();
+ };
+
+ triggerAndClearAnimations(mPlayPendingSet);
+ triggerAndClearAnimations(mPausePendingSet);
+
+ mHasPlayPendingGeometricAnimations = CheckState::Absent;
+}
+
+static bool IsTransition(const dom::Animation& aAnimation) {
+ const dom::CSSTransition* transition = aAnimation.AsCSSTransition();
+ return transition && transition->IsTiedToMarkup();
+}
+
+void PendingAnimationTracker::MarkAnimationsThatMightNeedSynchronization() {
+ // We only set mHasPlayPendingGeometricAnimations to "present" in this method
+ // and nowhere else. After setting the state to "present", if there is any
+ // change to the set of play-pending animations we will reset
+ // mHasPlayPendingGeometricAnimations to either "indeterminate" or "absent".
+ //
+ // As a result, if mHasPlayPendingGeometricAnimations is "present", we can
+ // assume that this method has already been called for the current set of
+ // play-pending animations and it is not necessary to run this method again.
+ //
+ // If mHasPlayPendingGeometricAnimations is "absent", then we can also skip
+ // the body of this method since there are no notifications to be sent.
+ //
+ // Therefore, the only case we need to be concerned about is the
+ // "indeterminate" case. For all other cases we can return early.
+ //
+ // Note that *without* this optimization, starting animations would become
+ // O(n^2) in the case where each animation is on a different element and
+ // contains a compositor-animatable property since we would end up iterating
+ // over all animations in the play-pending set for each target element.
+ if (mHasPlayPendingGeometricAnimations != CheckState::Indeterminate) {
+ return;
+ }
+
+ // We only synchronize CSS transitions with other CSS transitions (and we only
+ // synchronize non-transition animations with non-transition animations)
+ // since typically the author will not trigger both CSS animations and
+ // CSS transitions simultaneously and expect them to be synchronized.
+ //
+ // If we try to synchronize CSS transitions with non-transitions then for some
+ // content we will end up degrading performance by forcing animations to run
+ // on the main thread that really don't need to.
+
+ mHasPlayPendingGeometricAnimations = CheckState::Absent;
+ for (const auto& animation : mPlayPendingSet) {
+ if (animation->GetEffect() && animation->GetEffect()->AffectsGeometry()) {
+ mHasPlayPendingGeometricAnimations &= ~CheckState::Absent;
+ mHasPlayPendingGeometricAnimations |= IsTransition(*animation)
+ ? CheckState::TransitionsPresent
+ : CheckState::AnimationsPresent;
+
+ // If we have both transitions and animations we don't need to look any
+ // further.
+ if (mHasPlayPendingGeometricAnimations ==
+ (CheckState::TransitionsPresent | CheckState::AnimationsPresent)) {
+ break;
+ }
+ }
+ }
+
+ if (mHasPlayPendingGeometricAnimations == CheckState::Absent) {
+ return;
+ }
+
+ for (const auto& animation : mPlayPendingSet) {
+ bool isTransition = IsTransition(*animation);
+ if ((isTransition &&
+ mHasPlayPendingGeometricAnimations & CheckState::TransitionsPresent) ||
+ (!isTransition &&
+ mHasPlayPendingGeometricAnimations & CheckState::AnimationsPresent)) {
+ animation->NotifyGeometricAnimationsStartingThisFrame();
+ }
+ }
+}
+
+void PendingAnimationTracker::EnsurePaintIsScheduled() {
+ if (!mDocument) {
+ return;
+ }
+
+ PresShell* presShell = mDocument->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ if (!rootFrame) {
+ return;
+ }
+
+ rootFrame->SchedulePaintWithoutInvalidatingObservers();
+}
+
+} // namespace mozilla
diff --git a/dom/animation/PendingAnimationTracker.h b/dom/animation/PendingAnimationTracker.h
new file mode 100644
index 0000000000..5fb277b7ef
--- /dev/null
+++ b/dom/animation/PendingAnimationTracker.h
@@ -0,0 +1,113 @@
+/* -*- 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/. */
+
+#ifndef mozilla_PendingAnimationTracker_h
+#define mozilla_PendingAnimationTracker_h
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/TypedEnumBits.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashSet.h"
+
+class nsIFrame;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+/**
+ * Handle the pending animations which use document-timeline or null-timeline
+ * while playing or pausing.
+ */
+class PendingAnimationTracker final {
+ public:
+ explicit PendingAnimationTracker(dom::Document* aDocument);
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker)
+
+ void AddPlayPending(dom::Animation& aAnimation) {
+ // We'd like to assert here that IsWaitingToPause(aAnimation) is false but
+ // if |aAnimation| was tracked here as a pause-pending animation when it was
+ // removed from |mDocument|, then re-attached to |mDocument|, and then
+ // played again, we could end up here with IsWaitingToPause returning true.
+ //
+ // However, that should be harmless since all it means is that we'll call
+ // Animation::TriggerOnNextTick or Animation::TriggerNow twice, both of
+ // which will handle the redundant call gracefully.
+ AddPending(aAnimation, mPlayPendingSet);
+ mHasPlayPendingGeometricAnimations = CheckState::Indeterminate;
+ }
+ void RemovePlayPending(dom::Animation& aAnimation) {
+ RemovePending(aAnimation, mPlayPendingSet);
+ mHasPlayPendingGeometricAnimations = CheckState::Indeterminate;
+ }
+ bool IsWaitingToPlay(const dom::Animation& aAnimation) const {
+ return IsWaiting(aAnimation, mPlayPendingSet);
+ }
+
+ void AddPausePending(dom::Animation& aAnimation) {
+ // As with AddPausePending, we'd like to assert that
+ // IsWaitingToPlay(aAnimation) is false but there are some circumstances
+ // where this can be true. Fortunately adding the animation to both pending
+ // sets should be harmless.
+ AddPending(aAnimation, mPausePendingSet);
+ }
+ void RemovePausePending(dom::Animation& aAnimation) {
+ RemovePending(aAnimation, mPausePendingSet);
+ }
+ bool IsWaitingToPause(const dom::Animation& aAnimation) const {
+ return IsWaiting(aAnimation, mPausePendingSet);
+ }
+
+ void TriggerPendingAnimationsOnNextTick(const TimeStamp& aReadyTime);
+ void TriggerPendingAnimationsNow();
+ bool HasPendingAnimations() const {
+ return mPlayPendingSet.Count() > 0 || mPausePendingSet.Count() > 0;
+ }
+
+ /**
+ * Looks amongst the set of play-pending animations, and, if there are
+ * animations that affect geometric properties, notifies all play-pending
+ * animations so that they can be synchronized, if needed.
+ */
+ void MarkAnimationsThatMightNeedSynchronization();
+
+ private:
+ ~PendingAnimationTracker() = default;
+
+ void EnsurePaintIsScheduled();
+
+ using AnimationSet = nsTHashSet<nsRefPtrHashKey<dom::Animation>>;
+
+ void AddPending(dom::Animation& aAnimation, AnimationSet& aSet);
+ void RemovePending(dom::Animation& aAnimation, AnimationSet& aSet);
+ bool IsWaiting(const dom::Animation& aAnimation,
+ const AnimationSet& aSet) const;
+
+ AnimationSet mPlayPendingSet;
+ AnimationSet mPausePendingSet;
+ RefPtr<dom::Document> mDocument;
+
+ public:
+ enum class CheckState {
+ Indeterminate = 0,
+ Absent = 1 << 0,
+ AnimationsPresent = 1 << 1,
+ TransitionsPresent = 1 << 2,
+ };
+
+ private:
+ CheckState mHasPlayPendingGeometricAnimations = CheckState::Indeterminate;
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(PendingAnimationTracker::CheckState)
+
+} // namespace mozilla
+
+#endif // mozilla_PendingAnimationTracker_h
diff --git a/dom/animation/PostRestyleMode.h b/dom/animation/PostRestyleMode.h
new file mode 100644
index 0000000000..7ddf8df778
--- /dev/null
+++ b/dom/animation/PostRestyleMode.h
@@ -0,0 +1,16 @@
+/* -*- 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/. */
+
+#ifndef mozilla_PostRestyleMode_h
+#define mozilla_PostRestyleMode_h
+
+namespace mozilla {
+
+enum class PostRestyleMode { IfNeeded, Never };
+
+} // namespace mozilla
+
+#endif // mozilla_PostRestyleMode_h
diff --git a/dom/animation/PseudoElementHashEntry.h b/dom/animation/PseudoElementHashEntry.h
new file mode 100644
index 0000000000..b532d86105
--- /dev/null
+++ b/dom/animation/PseudoElementHashEntry.h
@@ -0,0 +1,51 @@
+/* -*- 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/. */
+
+#ifndef mozilla_PseudoElementHashEntry_h
+#define mozilla_PseudoElementHashEntry_h
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/HashFunctions.h"
+#include "PLDHashTable.h"
+
+namespace mozilla {
+
+// A hash entry that uses a RefPtr<dom::Element>, PseudoStyleType pair
+class PseudoElementHashEntry : public PLDHashEntryHdr {
+ public:
+ typedef NonOwningAnimationTarget KeyType;
+ typedef const NonOwningAnimationTarget* KeyTypePointer;
+
+ explicit PseudoElementHashEntry(KeyTypePointer aKey)
+ : mElement(aKey->mElement), mPseudoType(aKey->mPseudoType) {}
+ PseudoElementHashEntry(PseudoElementHashEntry&& aOther) = default;
+
+ ~PseudoElementHashEntry() = default;
+
+ KeyType GetKey() const { return {mElement, mPseudoType}; }
+ bool KeyEquals(KeyTypePointer aKey) const {
+ return mElement == aKey->mElement && mPseudoType == aKey->mPseudoType;
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ if (!aKey) return 0;
+
+ // Convert the scoped enum into an integer while adding it to hash.
+ static_assert(sizeof(PseudoStyleType) == sizeof(uint8_t), "");
+ return mozilla::HashGeneric(aKey->mElement,
+ static_cast<uint8_t>(aKey->mPseudoType));
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ RefPtr<dom::Element> mElement;
+ PseudoStyleType mPseudoType;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PseudoElementHashEntry_h
diff --git a/dom/animation/ScrollTimeline.cpp b/dom/animation/ScrollTimeline.cpp
new file mode 100644
index 0000000000..eeb9571494
--- /dev/null
+++ b/dom/animation/ScrollTimeline.cpp
@@ -0,0 +1,295 @@
+/* -*- 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 "ScrollTimeline.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/ElementInlines.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/DisplayPortUtils.h"
+#include "mozilla/ElementAnimationData.h"
+#include "mozilla/PresShell.h"
+#include "nsIFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla::dom {
+
+// ---------------------------------
+// Methods of ScrollTimeline
+// ---------------------------------
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ScrollTimeline)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ScrollTimeline,
+ AnimationTimeline)
+ tmp->Teardown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSource.mElement)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ScrollTimeline,
+ AnimationTimeline)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSource.mElement)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ScrollTimeline,
+ AnimationTimeline)
+
+ScrollTimeline::ScrollTimeline(Document* aDocument, const Scroller& aScroller,
+ StyleScrollAxis aAxis)
+ : AnimationTimeline(aDocument->GetParentObject(),
+ aDocument->GetScopeObject()->GetRTPCallerType()),
+ mDocument(aDocument),
+ mSource(aScroller),
+ mAxis(aAxis) {
+ MOZ_ASSERT(aDocument);
+ RegisterWithScrollSource();
+}
+
+/* static */ std::pair<const Element*, PseudoStyleType>
+ScrollTimeline::FindNearestScroller(Element* aSubject,
+ PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(aSubject);
+ Element* subject =
+ AnimationUtils::GetElementForRestyle(aSubject, aPseudoType);
+
+ Element* curr = subject->GetFlattenedTreeParentElement();
+ Element* root = subject->OwnerDoc()->GetDocumentElement();
+ while (curr && curr != root) {
+ const ComputedStyle* style = Servo_Element_GetMaybeOutOfDateStyle(curr);
+ MOZ_ASSERT(style, "The ancestor should be styled.");
+ if (style->StyleDisplay()->IsScrollableOverflow()) {
+ break;
+ }
+ curr = curr->GetFlattenedTreeParentElement();
+ }
+ // If there is no scroll container, we use root.
+ if (!curr) {
+ return {root, PseudoStyleType::NotPseudo};
+ }
+ return AnimationUtils::GetElementPseudoPair(curr);
+}
+
+/* static */
+already_AddRefed<ScrollTimeline> ScrollTimeline::MakeAnonymous(
+ Document* aDocument, const NonOwningAnimationTarget& aTarget,
+ StyleScrollAxis aAxis, StyleScroller aScroller) {
+ MOZ_ASSERT(aTarget);
+ Scroller scroller;
+ switch (aScroller) {
+ case StyleScroller::Root:
+ scroller = Scroller::Root(aTarget.mElement->OwnerDoc());
+ break;
+
+ case StyleScroller::Nearest: {
+ auto [element, pseudo] =
+ FindNearestScroller(aTarget.mElement, aTarget.mPseudoType);
+ scroller = Scroller::Nearest(const_cast<Element*>(element), pseudo);
+ break;
+ }
+ case StyleScroller::SelfElement:
+ scroller = Scroller::Self(aTarget.mElement, aTarget.mPseudoType);
+ break;
+ }
+
+ // Each use of scroll() corresponds to its own instance of ScrollTimeline in
+ // the Web Animations API, even if multiple elements use scroll() to refer to
+ // the same scroll container with the same arguments.
+ // https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+ return MakeAndAddRef<ScrollTimeline>(aDocument, scroller, aAxis);
+}
+
+/* static*/ already_AddRefed<ScrollTimeline> ScrollTimeline::MakeNamed(
+ Document* aDocument, Element* aReferenceElement,
+ PseudoStyleType aPseudoType, const StyleScrollTimeline& aStyleTimeline) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Scroller scroller = Scroller::Named(aReferenceElement, aPseudoType);
+ return MakeAndAddRef<ScrollTimeline>(aDocument, std::move(scroller),
+ aStyleTimeline.GetAxis());
+}
+
+Nullable<TimeDuration> ScrollTimeline::GetCurrentTimeAsDuration() const {
+ // If no layout box, this timeline is inactive.
+ if (!mSource || !mSource.mElement->GetPrimaryFrame()) {
+ return nullptr;
+ }
+
+ // if this is not a scroller container, this timeline is inactive.
+ const nsIScrollableFrame* scrollFrame = GetScrollFrame();
+ if (!scrollFrame) {
+ return nullptr;
+ }
+
+ const auto orientation = Axis();
+
+ // If there is no scrollable overflow, then the ScrollTimeline is inactive.
+ // https://drafts.csswg.org/scroll-animations-1/#scrolltimeline-interface
+ if (!scrollFrame->GetAvailableScrollingDirections().contains(orientation)) {
+ return nullptr;
+ }
+
+ const bool isHorizontal = orientation == layers::ScrollDirection::eHorizontal;
+ const nsPoint& scrollPosition = scrollFrame->GetScrollPosition();
+ const Maybe<ScrollOffsets>& offsets =
+ ComputeOffsets(scrollFrame, orientation);
+ if (!offsets) {
+ return nullptr;
+ }
+
+ // Note: For RTL, scrollPosition.x or scrollPosition.y may be negative,
+ // e.g. the range of its value is [0, -range], so we have to use the
+ // absolute value.
+ nscoord position =
+ std::abs(isHorizontal ? scrollPosition.x : scrollPosition.y);
+ double progress = static_cast<double>(position - offsets->mStart) /
+ static_cast<double>(offsets->mEnd - offsets->mStart);
+ return TimeDuration::FromMilliseconds(progress *
+ PROGRESS_TIMELINE_DURATION_MILLISEC);
+}
+
+layers::ScrollDirection ScrollTimeline::Axis() const {
+ MOZ_ASSERT(mSource && mSource.mElement->GetPrimaryFrame());
+
+ const WritingMode wm = mSource.mElement->GetPrimaryFrame()->GetWritingMode();
+ return mAxis == StyleScrollAxis::Horizontal ||
+ (!wm.IsVertical() && mAxis == StyleScrollAxis::Inline) ||
+ (wm.IsVertical() && mAxis == StyleScrollAxis::Block)
+ ? layers::ScrollDirection::eHorizontal
+ : layers::ScrollDirection::eVertical;
+}
+
+StyleOverflow ScrollTimeline::SourceScrollStyle() const {
+ MOZ_ASSERT(mSource && mSource.mElement->GetPrimaryFrame());
+
+ const nsIScrollableFrame* scrollFrame = GetScrollFrame();
+ MOZ_ASSERT(scrollFrame);
+
+ const ScrollStyles scrollStyles = scrollFrame->GetScrollStyles();
+
+ return Axis() == layers::ScrollDirection::eHorizontal
+ ? scrollStyles.mHorizontal
+ : scrollStyles.mVertical;
+}
+
+bool ScrollTimeline::APZIsActiveForSource() const {
+ MOZ_ASSERT(mSource);
+ return gfxPlatform::AsyncPanZoomEnabled() &&
+ !nsLayoutUtils::ShouldDisableApzForElement(mSource.mElement) &&
+ DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(mSource.mElement);
+}
+
+bool ScrollTimeline::ScrollingDirectionIsAvailable() const {
+ const nsIScrollableFrame* scrollFrame = GetScrollFrame();
+ MOZ_ASSERT(scrollFrame);
+ return scrollFrame->GetAvailableScrollingDirections().contains(Axis());
+}
+
+void ScrollTimeline::ReplacePropertiesWith(const Element* aReferenceElement,
+ PseudoStyleType aPseudoType,
+ const StyleScrollTimeline& aNew) {
+ MOZ_ASSERT(aReferenceElement == mSource.mElement &&
+ aPseudoType == mSource.mPseudoType);
+ mAxis = aNew.GetAxis();
+
+ for (auto* anim = mAnimationOrder.getFirst(); anim;
+ anim = static_cast<LinkedListElement<Animation>*>(anim)->getNext()) {
+ MOZ_ASSERT(anim->GetTimeline() == this);
+ // Set this so we just PostUpdate() for this animation.
+ anim->SetTimeline(this);
+ }
+}
+
+Maybe<ScrollTimeline::ScrollOffsets> ScrollTimeline::ComputeOffsets(
+ const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const {
+ const nsRect& scrollRange = aScrollFrame->GetScrollRange();
+ nscoord range = aOrientation == layers::ScrollDirection::eHorizontal
+ ? scrollRange.width
+ : scrollRange.height;
+ MOZ_ASSERT(range > 0);
+ return Some(ScrollOffsets{0, range});
+}
+
+void ScrollTimeline::RegisterWithScrollSource() {
+ if (!mSource) {
+ return;
+ }
+
+ auto& scheduler =
+ ProgressTimelineScheduler::Ensure(mSource.mElement, mSource.mPseudoType);
+ scheduler.AddTimeline(this);
+}
+
+void ScrollTimeline::UnregisterFromScrollSource() {
+ if (!mSource) {
+ return;
+ }
+
+ auto* scheduler =
+ ProgressTimelineScheduler::Get(mSource.mElement, mSource.mPseudoType);
+ if (!scheduler) {
+ return;
+ }
+
+ scheduler->RemoveTimeline(this);
+ if (scheduler->IsEmpty()) {
+ ProgressTimelineScheduler::Destroy(mSource.mElement, mSource.mPseudoType);
+ }
+}
+
+const nsIScrollableFrame* ScrollTimeline::GetScrollFrame() const {
+ if (!mSource) {
+ return nullptr;
+ }
+
+ switch (mSource.mType) {
+ case Scroller::Type::Root:
+ if (const PresShell* presShell =
+ mSource.mElement->OwnerDoc()->GetPresShell()) {
+ return presShell->GetRootScrollFrameAsScrollable();
+ }
+ return nullptr;
+ case Scroller::Type::Nearest:
+ case Scroller::Type::Name:
+ case Scroller::Type::Self:
+ return nsLayoutUtils::FindScrollableFrameFor(mSource.mElement);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unsupported scroller type");
+ return nullptr;
+}
+
+// ------------------------------------
+// Methods of ProgressTimelineScheduler
+// ------------------------------------
+/* static */ ProgressTimelineScheduler* ProgressTimelineScheduler::Get(
+ const Element* aElement, PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(aElement);
+ auto* data = aElement->GetAnimationData();
+ if (!data) {
+ return nullptr;
+ }
+
+ return data->GetProgressTimelineScheduler(aPseudoType);
+}
+
+/* static */ ProgressTimelineScheduler& ProgressTimelineScheduler::Ensure(
+ Element* aElement, PseudoStyleType aPseudoType) {
+ MOZ_ASSERT(aElement);
+ return aElement->EnsureAnimationData().EnsureProgressTimelineScheduler(
+ *aElement, aPseudoType);
+}
+
+/* static */
+void ProgressTimelineScheduler::Destroy(const Element* aElement,
+ PseudoStyleType aPseudoType) {
+ auto* data = aElement->GetAnimationData();
+ MOZ_ASSERT(data);
+ data->ClearProgressTimelineScheduler(aPseudoType);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/ScrollTimeline.h b/dom/animation/ScrollTimeline.h
new file mode 100644
index 0000000000..129872f61b
--- /dev/null
+++ b/dom/animation/ScrollTimeline.h
@@ -0,0 +1,281 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_ScrollTimeline_h
+#define mozilla_dom_ScrollTimeline_h
+
+#include "mozilla/dom/AnimationTimeline.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/PairHash.h"
+#include "mozilla/ServoStyleConsts.h"
+#include "mozilla/WritingModes.h"
+
+#define PROGRESS_TIMELINE_DURATION_MILLISEC 100000
+
+class nsIScrollableFrame;
+
+namespace mozilla {
+class ElementAnimationData;
+struct NonOwningAnimationTarget;
+namespace dom {
+class Element;
+
+/**
+ * Implementation notes
+ * --------------------
+ *
+ * ScrollTimelines do not observe refreshes the way DocumentTimelines do.
+ * This is because the refresh driver keeps ticking while it has registered
+ * refresh observers. For a DocumentTimeline, it's appropriate to keep the
+ * refresh driver ticking as long as there are active animations, since the
+ * animations need to be sampled on every frame. Scroll-linked animations,
+ * however, only need to be sampled when scrolling has occurred, so keeping
+ * the refresh driver ticking is wasteful.
+ *
+ * As a result, we schedule an animation restyle when
+ * 1) there are any scroll offsets updated (from APZ or script), via
+ * nsIScrollableFrame, or
+ * 2) there are any possible scroll range updated during the frame reflow.
+ *
+ * -------------
+ * | Animation |
+ * -------------
+ * ^
+ * | Call Animation::Tick() if there are any scroll updates.
+ * |
+ * ------------------
+ * | ScrollTimeline |
+ * ------------------
+ * ^
+ * | Try schedule the scroll-driven animations, if there are any scroll
+ * | offsets changed or the scroll range changed [1].
+ * |
+ * ----------------------
+ * | nsIScrollableFrame |
+ * ----------------------
+ *
+ * [1] nsIScrollableFrame uses its associated dom::Element to lookup the
+ * ScrollTimelineSet, and iterates the set to schedule the animations
+ * linked to the ScrollTimelines.
+ */
+class ScrollTimeline : public AnimationTimeline {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> mozilla::MakeAndAddRef(Args&&... aArgs);
+
+ public:
+ struct Scroller {
+ // FIXME: Bug 1765211. Perhaps we only need root and a specific element.
+ // This depends on how we fix this bug.
+ enum class Type : uint8_t {
+ Root,
+ Nearest,
+ Name,
+ Self,
+ };
+ Type mType = Type::Root;
+ RefPtr<Element> mElement;
+ PseudoStyleType mPseudoType;
+
+ // We use the owner doc of the animation target. This may be different from
+ // |mDocument| after we implement ScrollTimeline interface for script.
+ static Scroller Root(const Document* aOwnerDoc) {
+ // For auto, we use scrolling element as the default scroller.
+ // However, it's mutable, and we would like to keep things simple, so
+ // we always register the ScrollTimeline to the document element (i.e.
+ // root element) because the content of the root scroll frame is the root
+ // element.
+ return {Type::Root, aOwnerDoc->GetDocumentElement(),
+ PseudoStyleType::NotPseudo};
+ }
+
+ static Scroller Nearest(Element* aElement, PseudoStyleType aPseudoType) {
+ return {Type::Nearest, aElement, aPseudoType};
+ }
+
+ static Scroller Named(Element* aElement, PseudoStyleType aPseudoType) {
+ return {Type::Name, aElement, aPseudoType};
+ }
+
+ static Scroller Self(Element* aElement, PseudoStyleType aPseudoType) {
+ return {Type::Self, aElement, aPseudoType};
+ }
+
+ explicit operator bool() const { return mElement; }
+ bool operator==(const Scroller& aOther) const {
+ return mType == aOther.mType && mElement == aOther.mElement &&
+ mPseudoType == aOther.mPseudoType;
+ }
+ };
+
+ static already_AddRefed<ScrollTimeline> MakeAnonymous(
+ Document* aDocument, const NonOwningAnimationTarget& aTarget,
+ StyleScrollAxis aAxis, StyleScroller aScroller);
+
+ // Note: |aReferfenceElement| is used as the scroller which specifies
+ // scroll-timeline-name property.
+ static already_AddRefed<ScrollTimeline> MakeNamed(
+ Document* aDocument, Element* aReferenceElement,
+ PseudoStyleType aPseudoType, const StyleScrollTimeline& aStyleTimeline);
+
+ bool operator==(const ScrollTimeline& aOther) const {
+ return mDocument == aOther.mDocument && mSource == aOther.mSource &&
+ mAxis == aOther.mAxis;
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ScrollTimeline, AnimationTimeline)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override {
+ // FIXME: Bug 1676794: Implement ScrollTimeline interface.
+ return nullptr;
+ }
+
+ // AnimationTimeline methods.
+ Nullable<TimeDuration> GetCurrentTimeAsDuration() const override;
+ bool TracksWallclockTime() const override { return false; }
+ Nullable<TimeDuration> ToTimelineTime(
+ const TimeStamp& aTimeStamp) const override {
+ // It's unclear to us what should we do for this function now, so return
+ // nullptr.
+ return nullptr;
+ }
+ TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override {
+ // It's unclear to us what should we do for this function now, so return
+ // zero time.
+ return {};
+ }
+ Document* GetDocument() const override { return mDocument; }
+ bool IsMonotonicallyIncreasing() const override { return false; }
+ bool IsScrollTimeline() const override { return true; }
+ const ScrollTimeline* AsScrollTimeline() const override { return this; }
+ bool IsViewTimeline() const override { return false; }
+
+ Nullable<TimeDuration> TimelineDuration() const override {
+ // We are using this magic number for progress-based timeline duration
+ // because we don't support percentage for duration.
+ return TimeDuration::FromMilliseconds(PROGRESS_TIMELINE_DURATION_MILLISEC);
+ }
+
+ void ScheduleAnimations() {
+ // FIXME: Bug 1737927: Need to check the animation mutation observers for
+ // animations with scroll timelines.
+ // nsAutoAnimationMutationBatch mb(mDocument);
+
+ Tick();
+ }
+
+ // If the source of a ScrollTimeline is an element whose principal box does
+ // not exist or is not a scroll container, then its phase is the timeline
+ // inactive phase. It is otherwise in the active phase. This returns true if
+ // the timeline is in active phase.
+ // https://drafts.csswg.org/web-animations-1/#inactive-timeline
+ // Note: This function is called only for compositor animations, so we must
+ // have the primary frame (principal box) for the source element if it exists.
+ bool IsActive() const { return GetScrollFrame(); }
+
+ Element* SourceElement() const {
+ MOZ_ASSERT(mSource);
+ return mSource.mElement;
+ }
+
+ // A helper to get the physical orientation of this scroll-timeline.
+ layers::ScrollDirection Axis() const;
+
+ StyleOverflow SourceScrollStyle() const;
+
+ bool APZIsActiveForSource() const;
+
+ bool ScrollingDirectionIsAvailable() const;
+
+ void ReplacePropertiesWith(const Element* aReferenceElement,
+ PseudoStyleType aPseudoType,
+ const StyleScrollTimeline& aNew);
+
+ protected:
+ virtual ~ScrollTimeline() { Teardown(); }
+ ScrollTimeline() = delete;
+ ScrollTimeline(Document* aDocument, const Scroller& aScroller,
+ StyleScrollAxis aAxis);
+
+ struct ScrollOffsets {
+ nscoord mStart = 0;
+ nscoord mEnd = 0;
+ };
+ virtual Maybe<ScrollOffsets> ComputeOffsets(
+ const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const;
+
+ // Note: This function is required to be idempotent, as it can be called from
+ // both cycleCollection::Unlink() and ~ScrollTimeline(). When modifying this
+ // function, be sure to preserve this property.
+ void Teardown() { UnregisterFromScrollSource(); }
+
+ // Register this scroll timeline to the element property.
+ void RegisterWithScrollSource();
+
+ // Unregister this scroll timeline from the element property.
+ void UnregisterFromScrollSource();
+
+ const nsIScrollableFrame* GetScrollFrame() const;
+
+ static std::pair<const Element*, PseudoStyleType> FindNearestScroller(
+ Element* aSubject, PseudoStyleType aPseudoType);
+
+ RefPtr<Document> mDocument;
+
+ // FIXME: Bug 1765211: We may have to update the source element once the
+ // overflow property of the scroll-container is updated when we are using
+ // nearest scroller.
+ Scroller mSource;
+ StyleScrollAxis mAxis;
+};
+
+/**
+ * A wrapper around a hashset of ScrollTimeline objects to handle the scheduling
+ * of scroll driven animations. This is used for all kinds of progress
+ * timelines, i.e. anonymous/named scroll timelines and anonymous/named view
+ * timelines. And this object is owned by the scroll source (See
+ * ElementAnimationData and nsGfxScrollFrame for the usage).
+ */
+class ProgressTimelineScheduler {
+ public:
+ ProgressTimelineScheduler() { MOZ_COUNT_CTOR(ProgressTimelineScheduler); }
+ ~ProgressTimelineScheduler() { MOZ_COUNT_DTOR(ProgressTimelineScheduler); }
+
+ static ProgressTimelineScheduler* Get(const Element* aElement,
+ PseudoStyleType aPseudoType);
+ static ProgressTimelineScheduler& Ensure(Element* aElement,
+ PseudoStyleType aPseudoType);
+ static void Destroy(const Element* aElement, PseudoStyleType aPseudoType);
+
+ void AddTimeline(ScrollTimeline* aScrollTimeline) {
+ Unused << mTimelines.put(aScrollTimeline);
+ }
+ void RemoveTimeline(ScrollTimeline* aScrollTimeline) {
+ mTimelines.remove(aScrollTimeline);
+ }
+
+ bool IsEmpty() const { return mTimelines.empty(); }
+
+ void ScheduleAnimations() const {
+ for (auto iter = mTimelines.iter(); !iter.done(); iter.next()) {
+ iter.get()->ScheduleAnimations();
+ }
+ }
+
+ private:
+ // We let Animations own its scroll timeline or view timeline if it is
+ // anonymous. For named progress timelines, they are created and destroyed by
+ // TimelineCollection.
+ HashSet<ScrollTimeline*> mTimelines;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ScrollTimeline_h
diff --git a/dom/animation/ScrollTimelineAnimationTracker.cpp b/dom/animation/ScrollTimelineAnimationTracker.cpp
new file mode 100644
index 0000000000..ae46bca62b
--- /dev/null
+++ b/dom/animation/ScrollTimelineAnimationTracker.cpp
@@ -0,0 +1,48 @@
+/* -*- 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 "ScrollTimelineAnimationTracker.h"
+
+#include "mozilla/dom/Document.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION(ScrollTimelineAnimationTracker, mPendingSet, mDocument)
+
+void ScrollTimelineAnimationTracker::TriggerPendingAnimations() {
+ for (auto iter = mPendingSet.begin(), end = mPendingSet.end(); iter != end;
+ ++iter) {
+ dom::Animation* animation = *iter;
+
+ MOZ_ASSERT(animation->GetTimeline() &&
+ !animation->GetTimeline()->IsMonotonicallyIncreasing());
+
+ // FIXME: Trigger now may not be correct because the spec says:
+ // If a user agent determines that animation is immediately ready, it may
+ // schedule the task (i.e. ResumeAt()) as a microtask such that it runs at
+ // the next microtask checkpoint, but it must not perform the task
+ // synchronously.
+ // Note: So, for now, we put the animation into the tracker, and trigger
+ // them immediately until the frames are ready. Using TriggerOnNextTick()
+ // for scroll-driven animations may have issues because we don't tick if
+ // no one does scroll.
+ if (!animation->TryTriggerNowForFiniteTimeline()) {
+ // Note: We keep this animation pending even if its timeline is always
+ // inactive. It's pretty hard to tell its future status, for example, it's
+ // possible that the scroll container is in display:none subtree but the
+ // animating element isn't the subtree, then we need to keep tracking the
+ // situation until the scroll container gets framed. so in general we make
+ // this animation be pending (i.e. not ready) if its scroll-timeline is
+ // inactive, and this also matches the current spec definition.
+ continue;
+ }
+
+ // Note: Remove() is legitimately called once per entry during the loop.
+ mPendingSet.Remove(iter);
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/animation/ScrollTimelineAnimationTracker.h b/dom/animation/ScrollTimelineAnimationTracker.h
new file mode 100644
index 0000000000..fec2814cec
--- /dev/null
+++ b/dom/animation/ScrollTimelineAnimationTracker.h
@@ -0,0 +1,58 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ScrollTimelineAnimationTracker_h
+#define mozilla_ScrollTimelineAnimationTracker_h
+
+#include "mozilla/dom/Animation.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+/**
+ * Handle the pending animations which use scroll timeline while playing or
+ * pausing.
+ */
+class ScrollTimelineAnimationTracker final {
+ public:
+ explicit ScrollTimelineAnimationTracker(dom::Document* aDocument)
+ : mDocument(aDocument) {}
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(
+ ScrollTimelineAnimationTracker)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ScrollTimelineAnimationTracker)
+
+ void AddPending(dom::Animation& aAnimation) {
+ mPendingSet.Insert(&aAnimation);
+ }
+
+ void RemovePending(dom::Animation& aAnimation) {
+ mPendingSet.Remove(&aAnimation);
+ }
+
+ bool HasPendingAnimations() const { return mPendingSet.Count() > 0; }
+
+ bool IsWaiting(const dom::Animation& aAnimation) const {
+ return mPendingSet.Contains(const_cast<dom::Animation*>(&aAnimation));
+ }
+
+ void TriggerPendingAnimations();
+
+ private:
+ ~ScrollTimelineAnimationTracker() = default;
+
+ nsTHashSet<nsRefPtrHashKey<dom::Animation>> mPendingSet;
+ RefPtr<dom::Document> mDocument;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ScrollTimelineAnimationTracker_h
diff --git a/dom/animation/TimingParams.cpp b/dom/animation/TimingParams.cpp
new file mode 100644
index 0000000000..7ab78f52f1
--- /dev/null
+++ b/dom/animation/TimingParams.cpp
@@ -0,0 +1,291 @@
+/* -*- 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/TimingParams.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/dom/AnimatableBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/ServoCSSParser.h"
+
+namespace mozilla {
+
+template <class OptionsType>
+static const dom::EffectTiming& GetTimingProperties(
+ const OptionsType& aOptions);
+
+template <>
+/* static */
+const dom::EffectTiming& GetTimingProperties(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) {
+ MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+ return aOptions.GetAsKeyframeEffectOptions();
+}
+
+template <>
+/* static */
+const dom::EffectTiming& GetTimingProperties(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) {
+ MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+ return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+/* static */
+TimingParams TimingParams::FromOptionsType(const OptionsType& aOptions,
+ ErrorResult& aRv) {
+ TimingParams result;
+
+ if (aOptions.IsUnrestrictedDouble()) {
+ double durationInMs = aOptions.GetAsUnrestrictedDouble();
+ if (durationInMs >= 0) {
+ result.mDuration.emplace(
+ StickyTimeDuration::FromMilliseconds(durationInMs));
+ } else {
+ nsPrintfCString error("Duration value %g is less than 0", durationInMs);
+ aRv.ThrowTypeError(error);
+ return result;
+ }
+ result.Update();
+ } else {
+ const dom::EffectTiming& timing = GetTimingProperties(aOptions);
+ result = FromEffectTiming(timing, aRv);
+ }
+
+ return result;
+}
+
+/* static */
+TimingParams TimingParams::FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv) {
+ return FromOptionsType(aOptions, aRv);
+}
+
+/* static */
+TimingParams TimingParams::FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv) {
+ return FromOptionsType(aOptions, aRv);
+}
+
+/* static */
+TimingParams TimingParams::FromEffectTiming(
+ const dom::EffectTiming& aEffectTiming, ErrorResult& aRv) {
+ TimingParams result;
+
+ Maybe<StickyTimeDuration> duration =
+ TimingParams::ParseDuration(aEffectTiming.mDuration, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ TimingParams::ValidateIterations(aEffectTiming.mIterations, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ Maybe<StyleComputedTimingFunction> easing =
+ ParseEasing(aEffectTiming.mEasing, aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+
+ result.mDuration = duration;
+ result.mDelay = TimeDuration::FromMilliseconds(aEffectTiming.mDelay);
+ result.mEndDelay = TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay);
+ result.mIterations = aEffectTiming.mIterations;
+ result.mIterationStart = aEffectTiming.mIterationStart;
+ result.mDirection = aEffectTiming.mDirection;
+ result.mFill = aEffectTiming.mFill;
+ result.mFunction = std::move(easing);
+
+ result.Update();
+
+ return result;
+}
+
+/* static */
+TimingParams TimingParams::MergeOptionalEffectTiming(
+ const TimingParams& aSource, const dom::OptionalEffectTiming& aEffectTiming,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed(), "Initially return value should be ok");
+
+ TimingParams result = aSource;
+
+ // Check for errors first
+
+ Maybe<StickyTimeDuration> duration;
+ if (aEffectTiming.mDuration.WasPassed()) {
+ duration =
+ TimingParams::ParseDuration(aEffectTiming.mDuration.Value(), aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ }
+
+ if (aEffectTiming.mIterationStart.WasPassed()) {
+ TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart.Value(),
+ aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ }
+
+ if (aEffectTiming.mIterations.WasPassed()) {
+ TimingParams::ValidateIterations(aEffectTiming.mIterations.Value(), aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ }
+
+ Maybe<StyleComputedTimingFunction> easing;
+ if (aEffectTiming.mEasing.WasPassed()) {
+ easing = ParseEasing(aEffectTiming.mEasing.Value(), aRv);
+ if (aRv.Failed()) {
+ return result;
+ }
+ }
+
+ // Assign values
+
+ if (aEffectTiming.mDuration.WasPassed()) {
+ result.mDuration = duration;
+ }
+ if (aEffectTiming.mDelay.WasPassed()) {
+ result.mDelay =
+ TimeDuration::FromMilliseconds(aEffectTiming.mDelay.Value());
+ }
+ if (aEffectTiming.mEndDelay.WasPassed()) {
+ result.mEndDelay =
+ TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay.Value());
+ }
+ if (aEffectTiming.mIterations.WasPassed()) {
+ result.mIterations = aEffectTiming.mIterations.Value();
+ }
+ if (aEffectTiming.mIterationStart.WasPassed()) {
+ result.mIterationStart = aEffectTiming.mIterationStart.Value();
+ }
+ if (aEffectTiming.mDirection.WasPassed()) {
+ result.mDirection = aEffectTiming.mDirection.Value();
+ }
+ if (aEffectTiming.mFill.WasPassed()) {
+ result.mFill = aEffectTiming.mFill.Value();
+ }
+ if (aEffectTiming.mEasing.WasPassed()) {
+ result.mFunction = easing;
+ }
+
+ result.Update();
+
+ return result;
+}
+
+/* static */
+Maybe<StyleComputedTimingFunction> TimingParams::ParseEasing(
+ const nsACString& aEasing, ErrorResult& aRv) {
+ auto timingFunction = StyleComputedTimingFunction::LinearKeyword();
+ if (!ServoCSSParser::ParseEasing(aEasing, timingFunction)) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_EASING_ERROR>(aEasing);
+ return Nothing();
+ }
+
+ if (timingFunction.IsLinearKeyword()) {
+ return Nothing();
+ }
+
+ return Some(std::move(timingFunction));
+}
+
+bool TimingParams::operator==(const TimingParams& aOther) const {
+ // We don't compare mActiveDuration and mEndTime because they are calculated
+ // from other timing parameters.
+ return mDuration == aOther.mDuration && mDelay == aOther.mDelay &&
+ mEndDelay == aOther.mEndDelay && mIterations == aOther.mIterations &&
+ mIterationStart == aOther.mIterationStart &&
+ mDirection == aOther.mDirection && mFill == aOther.mFill &&
+ mFunction == aOther.mFunction;
+}
+
+// FIXME: This is a tentative way to normalize the timing which is defined in
+// [web-animations-2] [1]. I borrow this implementation and some concepts for
+// the edge cases from Chromium [2] so we can match the behavior with them. The
+// implementation here ignores the case of percentage of start delay, end delay,
+// and duration because Gecko doesn't support them. We may have to update the
+// calculation if the spec issue [3] gets any update.
+//
+// [1]
+// https://drafts.csswg.org/web-animations-2/#time-based-animation-to-a-proportional-animation
+// [2] https://chromium-review.googlesource.com/c/chromium/src/+/2992387
+// [3] https://github.com/w3c/csswg-drafts/issues/4862
+TimingParams TimingParams::Normalize(
+ const TimeDuration& aTimelineDuration) const {
+ MOZ_ASSERT(aTimelineDuration,
+ "the timeline duration of scroll-timeline is always non-zero now");
+
+ TimingParams normalizedTiming(*this);
+
+ // Handle iteration duration value of "auto" first.
+ // FIXME: Bug 1676794: Gecko doesn't support `animation-duration:auto` and we
+ // don't support JS-generated scroll animations, so we don't fall into this
+ // case for now. Need to check this again after we support ScrollTimeline
+ // interface.
+ if (!mDuration) {
+ // If the iteration duration is auto, then:
+ // Set start delay and end delay to 0, as it is not possible to mix time
+ // and proportions.
+ normalizedTiming.mDelay = TimeDuration();
+ normalizedTiming.mEndDelay = TimeDuration();
+ normalizedTiming.Update();
+ return normalizedTiming;
+ }
+
+ if (mEndTime.IsZero()) {
+ // mEndTime of zero causes division by zero so we handle it here.
+ //
+ // FIXME: The spec doesn't mention this case, so we might have to update
+ // this based on the spec issue,
+ // https://github.com/w3c/csswg-drafts/issues/7459.
+ normalizedTiming.mDelay = TimeDuration();
+ normalizedTiming.mEndDelay = TimeDuration();
+ normalizedTiming.mDuration = Some(TimeDuration());
+ } else if (mEndTime == TimeDuration::Forever()) {
+ // The iteration count or duration may be infinite; however, start and
+ // end delays are strictly finite. Thus, in the limit when end time
+ // approaches infinity:
+ // start delay / end time = finite / infinite = 0
+ // end delay / end time = finite / infinite = 0
+ // iteration duration / end time = 1 / iteration count
+ // This condition can be reached by switching to a scroll timeline on
+ // an existing infinite duration animation.
+ //
+ // FIXME: The spec doesn't mention this case, so we might have to update
+ // this based on the spec issue,
+ // https://github.com/w3c/csswg-drafts/issues/7459.
+ normalizedTiming.mDelay = TimeDuration();
+ normalizedTiming.mEndDelay = TimeDuration();
+ normalizedTiming.mDuration =
+ Some(aTimelineDuration.MultDouble(1.0 / mIterations));
+ } else {
+ // Convert to percentages then multiply by the timeline duration.
+ const double endTimeInSec = mEndTime.ToSeconds();
+ normalizedTiming.mDelay =
+ aTimelineDuration.MultDouble(mDelay.ToSeconds() / endTimeInSec);
+ normalizedTiming.mEndDelay =
+ aTimelineDuration.MultDouble(mEndDelay.ToSeconds() / endTimeInSec);
+ normalizedTiming.mDuration = Some(StickyTimeDuration(
+ aTimelineDuration.MultDouble(mDuration->ToSeconds() / endTimeInSec)));
+ }
+
+ normalizedTiming.Update();
+ return normalizedTiming;
+}
+
+} // namespace mozilla
diff --git a/dom/animation/TimingParams.h b/dom/animation/TimingParams.h
new file mode 100644
index 0000000000..4745631006
--- /dev/null
+++ b/dom/animation/TimingParams.h
@@ -0,0 +1,265 @@
+/* -*- 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/. */
+
+#ifndef mozilla_TimingParams_h
+#define mozilla_TimingParams_h
+
+#include "X11UndefineNone.h"
+#include "nsPrintfCString.h"
+#include "nsStringFwd.h"
+#include "nsPrintfCString.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString
+#include "mozilla/Maybe.h"
+#include "mozilla/StickyTimeDuration.h"
+#include "mozilla/TimeStamp.h" // for TimeDuration
+#include "mozilla/ServoStyleConsts.h"
+
+#include "mozilla/dom/AnimationEffectBinding.h" // for FillMode
+ // and PlaybackDirection
+
+namespace mozilla {
+
+namespace dom {
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+} // namespace dom
+
+struct TimingParams {
+ TimingParams() = default;
+
+ TimingParams(float aDuration, float aDelay, float aIterationCount,
+ dom::PlaybackDirection aDirection, dom::FillMode aFillMode)
+ : mIterations(aIterationCount), mDirection(aDirection), mFill(aFillMode) {
+ mDuration.emplace(StickyTimeDuration::FromMilliseconds(aDuration));
+ mDelay = TimeDuration::FromMilliseconds(aDelay);
+ Update();
+ }
+
+ TimingParams(const TimeDuration& aDuration, const TimeDuration& aDelay,
+ const TimeDuration& aEndDelay, float aIterations,
+ float aIterationStart, dom::PlaybackDirection aDirection,
+ dom::FillMode aFillMode,
+ const Maybe<StyleComputedTimingFunction>& aFunction)
+ : mDelay(aDelay),
+ mEndDelay(aEndDelay),
+ mIterations(aIterations),
+ mIterationStart(aIterationStart),
+ mDirection(aDirection),
+ mFill(aFillMode),
+ mFunction(aFunction) {
+ mDuration.emplace(aDuration);
+ Update();
+ }
+
+ template <class OptionsType>
+ static TimingParams FromOptionsType(const OptionsType& aOptions,
+ ErrorResult& aRv);
+ static TimingParams FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
+ ErrorResult& aRv);
+ static TimingParams FromOptionsUnion(
+ const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
+ ErrorResult& aRv);
+ static TimingParams FromEffectTiming(const dom::EffectTiming& aEffectTiming,
+ ErrorResult& aRv);
+ // Returns a copy of |aSource| where each timing property in |aSource| that
+ // is also specified in |aEffectTiming| is replaced with the value from
+ // |aEffectTiming|.
+ //
+ // If any of the values in |aEffectTiming| are invalid, |aRv.Failed()| will be
+ // true and an unmodified copy of |aSource| will be returned.
+ static TimingParams MergeOptionalEffectTiming(
+ const TimingParams& aSource,
+ const dom::OptionalEffectTiming& aEffectTiming, ErrorResult& aRv);
+
+ // Range-checks and validates an UnrestrictedDoubleOrString or
+ // OwningUnrestrictedDoubleOrString object and converts to a
+ // StickyTimeDuration value or Nothing() if aDuration is "auto".
+ // Caller must check aRv.Failed().
+ template <class DoubleOrString>
+ static Maybe<StickyTimeDuration> ParseDuration(DoubleOrString& aDuration,
+ ErrorResult& aRv) {
+ Maybe<StickyTimeDuration> result;
+ if (aDuration.IsUnrestrictedDouble()) {
+ double durationInMs = aDuration.GetAsUnrestrictedDouble();
+ if (durationInMs >= 0) {
+ result.emplace(StickyTimeDuration::FromMilliseconds(durationInMs));
+ } else {
+ nsPrintfCString err("Duration (%g) must be nonnegative", durationInMs);
+ aRv.ThrowTypeError(err);
+ }
+ } else if (!aDuration.GetAsString().EqualsLiteral("auto")) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_DURATION_ERROR>(
+ NS_ConvertUTF16toUTF8(aDuration.GetAsString()));
+ }
+ return result;
+ }
+
+ static void ValidateIterationStart(double aIterationStart, ErrorResult& aRv) {
+ if (aIterationStart < 0) {
+ nsPrintfCString err("Iteration start (%g) must not be negative",
+ aIterationStart);
+ aRv.ThrowTypeError(err);
+ }
+ }
+
+ static void ValidateIterations(double aIterations, ErrorResult& aRv) {
+ if (std::isnan(aIterations)) {
+ aRv.ThrowTypeError("Iterations must not be NaN");
+ return;
+ }
+
+ if (aIterations < 0) {
+ nsPrintfCString err("Iterations (%g) must not be negative", aIterations);
+ aRv.ThrowTypeError(err);
+ }
+ }
+
+ static Maybe<StyleComputedTimingFunction> ParseEasing(const nsACString&,
+ ErrorResult&);
+
+ static StickyTimeDuration CalcActiveDuration(
+ const Maybe<StickyTimeDuration>& aDuration, double aIterations) {
+ // If either the iteration duration or iteration count is zero,
+ // Web Animations says that the active duration is zero. This is to
+ // ensure that the result is defined when the other argument is Infinity.
+ static const StickyTimeDuration zeroDuration;
+ if (!aDuration || aDuration->IsZero() || aIterations == 0.0) {
+ return zeroDuration;
+ }
+
+ MOZ_ASSERT(*aDuration >= zeroDuration && aIterations >= 0.0,
+ "Both animation duration and ieration count should be greater "
+ "than zero");
+
+ StickyTimeDuration result = aDuration->MultDouble(aIterations);
+ if (result < zeroDuration) {
+ // If the result of multiplying above is less than zero, it's likely an
+ // overflow happened. we consider it's +Inf here.
+ return StickyTimeDuration::Forever();
+ }
+ return result;
+ }
+ // Return the duration of the active interval calculated by duration and
+ // iteration count.
+ StickyTimeDuration ActiveDuration() const {
+ MOZ_ASSERT(CalcActiveDuration(mDuration, mIterations) == mActiveDuration,
+ "Cached value of active duration should be up to date");
+ return mActiveDuration;
+ }
+
+ StickyTimeDuration EndTime() const {
+ MOZ_ASSERT(mEndTime == CalcEndTime(),
+ "Cached value of end time should be up to date");
+ return mEndTime;
+ }
+
+ StickyTimeDuration CalcBeforeActiveBoundary() const {
+ static constexpr StickyTimeDuration zeroDuration;
+ // https://drafts.csswg.org/web-animations-1/#before-active-boundary-time
+ return std::max(std::min(StickyTimeDuration(mDelay), mEndTime),
+ zeroDuration);
+ }
+
+ StickyTimeDuration CalcActiveAfterBoundary() const {
+ if (mActiveDuration == StickyTimeDuration::Forever()) {
+ return StickyTimeDuration::Forever();
+ }
+
+ static constexpr StickyTimeDuration zeroDuration;
+ // https://drafts.csswg.org/web-animations-1/#active-after-boundary-time
+ return std::max(
+ std::min(StickyTimeDuration(mDelay + mActiveDuration), mEndTime),
+ zeroDuration);
+ }
+
+ bool operator==(const TimingParams& aOther) const;
+ bool operator!=(const TimingParams& aOther) const {
+ return !(*this == aOther);
+ }
+
+ void SetDuration(Maybe<StickyTimeDuration>&& aDuration) {
+ mDuration = std::move(aDuration);
+ Update();
+ }
+ void SetDuration(const Maybe<StickyTimeDuration>& aDuration) {
+ mDuration = aDuration;
+ Update();
+ }
+ const Maybe<StickyTimeDuration>& Duration() const { return mDuration; }
+
+ void SetDelay(const TimeDuration& aDelay) {
+ mDelay = aDelay;
+ Update();
+ }
+ const TimeDuration& Delay() const { return mDelay; }
+
+ void SetEndDelay(const TimeDuration& aEndDelay) {
+ mEndDelay = aEndDelay;
+ Update();
+ }
+ const TimeDuration& EndDelay() const { return mEndDelay; }
+
+ void SetIterations(double aIterations) {
+ mIterations = aIterations;
+ Update();
+ }
+ double Iterations() const { return mIterations; }
+
+ void SetIterationStart(double aIterationStart) {
+ mIterationStart = aIterationStart;
+ }
+ double IterationStart() const { return mIterationStart; }
+
+ void SetDirection(dom::PlaybackDirection aDirection) {
+ mDirection = aDirection;
+ }
+ dom::PlaybackDirection Direction() const { return mDirection; }
+
+ void SetFill(dom::FillMode aFill) { mFill = aFill; }
+ dom::FillMode Fill() const { return mFill; }
+
+ void SetTimingFunction(Maybe<StyleComputedTimingFunction>&& aFunction) {
+ mFunction = std::move(aFunction);
+ }
+ const Maybe<StyleComputedTimingFunction>& TimingFunction() const {
+ return mFunction;
+ }
+
+ // This is called only for progress-based timeline (i.e. non-monotonic
+ // timeline). That is, |aTimelineDuration| should be resolved already.
+ TimingParams Normalize(const TimeDuration& aTimelineDuration) const;
+
+ private:
+ void Update() {
+ mActiveDuration = CalcActiveDuration(mDuration, mIterations);
+ mEndTime = CalcEndTime();
+ }
+
+ StickyTimeDuration CalcEndTime() const {
+ if (mActiveDuration == StickyTimeDuration::Forever()) {
+ return StickyTimeDuration::Forever();
+ }
+ return std::max(mDelay + mActiveDuration + mEndDelay, StickyTimeDuration());
+ }
+
+ // mDuration.isNothing() represents the "auto" value
+ Maybe<StickyTimeDuration> mDuration;
+ TimeDuration mDelay; // Initializes to zero
+ TimeDuration mEndDelay;
+ double mIterations = 1.0; // Can be NaN, negative, +/-Infinity
+ double mIterationStart = 0.0;
+ dom::PlaybackDirection mDirection = dom::PlaybackDirection::Normal;
+ dom::FillMode mFill = dom::FillMode::Auto;
+ Maybe<StyleComputedTimingFunction> mFunction;
+ StickyTimeDuration mActiveDuration = StickyTimeDuration();
+ StickyTimeDuration mEndTime = StickyTimeDuration();
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TimingParams_h
diff --git a/dom/animation/ViewTimeline.cpp b/dom/animation/ViewTimeline.cpp
new file mode 100644
index 0000000000..5727c5f08e
--- /dev/null
+++ b/dom/animation/ViewTimeline.cpp
@@ -0,0 +1,166 @@
+/* -*- 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 "ViewTimeline.h"
+
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/ElementInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ViewTimeline, ScrollTimeline, mSubject)
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ViewTimeline, ScrollTimeline)
+
+/* static */
+already_AddRefed<ViewTimeline> ViewTimeline::MakeNamed(
+ Document* aDocument, Element* aSubject, PseudoStyleType aPseudoType,
+ const StyleViewTimeline& aStyleTimeline) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. Lookup scroller. We have to find the nearest scroller from |aSubject|
+ // and |aPseudoType|.
+ auto [element, pseudo] = FindNearestScroller(aSubject, aPseudoType);
+ auto scroller = Scroller::Nearest(const_cast<Element*>(element), pseudo);
+
+ // 2. Create timeline.
+ return MakeAndAddRef<ViewTimeline>(aDocument, scroller,
+ aStyleTimeline.GetAxis(), aSubject,
+ aPseudoType, aStyleTimeline.GetInset());
+}
+
+/* static */
+already_AddRefed<ViewTimeline> ViewTimeline::MakeAnonymous(
+ Document* aDocument, const NonOwningAnimationTarget& aTarget,
+ StyleScrollAxis aAxis, const StyleViewTimelineInset& aInset) {
+ // view() finds the nearest scroll container from the animation target.
+ auto [element, pseudo] =
+ FindNearestScroller(aTarget.mElement, aTarget.mPseudoType);
+ Scroller scroller = Scroller::Nearest(const_cast<Element*>(element), pseudo);
+ return MakeAndAddRef<ViewTimeline>(aDocument, scroller, aAxis,
+ aTarget.mElement, aTarget.mPseudoType,
+ aInset);
+}
+
+void ViewTimeline::ReplacePropertiesWith(Element* aSubjectElement,
+ PseudoStyleType aPseudoType,
+ const StyleViewTimeline& aNew) {
+ mSubject = aSubjectElement;
+ mSubjectPseudoType = aPseudoType;
+ mAxis = aNew.GetAxis();
+ // FIXME: Bug 1817073. We assume it is a non-animatable value for now.
+ mInset = aNew.GetInset();
+
+ for (auto* anim = mAnimationOrder.getFirst(); anim;
+ anim = static_cast<LinkedListElement<Animation>*>(anim)->getNext()) {
+ MOZ_ASSERT(anim->GetTimeline() == this);
+ // Set this so we just PostUpdate() for this animation.
+ anim->SetTimeline(this);
+ }
+}
+
+Maybe<ScrollTimeline::ScrollOffsets> ViewTimeline::ComputeOffsets(
+ const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const {
+ MOZ_ASSERT(mSubject);
+ MOZ_ASSERT(aScrollFrame);
+
+ const Element* subjectElement =
+ AnimationUtils::GetElementForRestyle(mSubject, mSubjectPseudoType);
+ const nsIFrame* subject = subjectElement->GetPrimaryFrame();
+ if (!subject) {
+ // No principal box of the subject, so we cannot compute the offset. This
+ // may happen when we clear all animation collections during unbinding from
+ // the tree.
+ return Nothing();
+ }
+
+ // In order to get the distance between the subject and the scrollport
+ // properly, we use the position based on the domain of the scrolled frame,
+ // instead of the scrollable frame.
+ const nsIFrame* scrolledFrame = aScrollFrame->GetScrolledFrame();
+ MOZ_ASSERT(scrolledFrame);
+ const nsRect subjectRect(subject->GetOffsetTo(scrolledFrame),
+ subject->GetSize());
+
+ // Use scrollport size (i.e. padding box size - scrollbar size), which is used
+ // for calculating the view progress visibility range.
+ // https://drafts.csswg.org/scroll-animations/#view-progress-visibility-range
+ const nsRect scrollPort = aScrollFrame->GetScrollPortRect();
+
+ // Adjuct the positions and sizes based on the physical axis.
+ nscoord subjectPosition = subjectRect.y;
+ nscoord subjectSize = subjectRect.height;
+ nscoord scrollPortSize = scrollPort.height;
+ if (aOrientation == layers::ScrollDirection::eHorizontal) {
+ // |subjectPosition| should be the position of the start border edge of the
+ // subject, so for R-L case, we have to use XMost() as the start border
+ // edge of the subject, and compute its position by using the x-most side of
+ // the scrolled frame as the origin on the horizontal axis.
+ subjectPosition = scrolledFrame->GetWritingMode().IsPhysicalRTL()
+ ? scrolledFrame->GetSize().width - subjectRect.XMost()
+ : subjectRect.x;
+ subjectSize = subjectRect.width;
+ scrollPortSize = scrollPort.width;
+ }
+
+ // |sideInsets.mEnd| is used to adjust the start offset, and
+ // |sideInsets.mStart| is used to adjust the end offset. This is because
+ // |sideInsets.mStart| refers to logical start side [1] of the source box
+ // (i.e. the box of the scrollport), where as |startOffset| refers to the
+ // start of the timeline, and similarly for end side/offset. [1]
+ // https://drafts.csswg.org/css-writing-modes-4/#css-start
+ const auto sideInsets = ComputeInsets(aScrollFrame, aOrientation);
+
+ // Basically, we are computing the "cover" timeline range name, which
+ // represents the full range of the view progress timeline.
+ // https://drafts.csswg.org/scroll-animations-1/#valdef-animation-timeline-range-cover
+
+ // Note: `subjectPosition - scrollPortSize` means the distance between the
+ // start border edge of the subject and the end edge of the scrollport.
+ nscoord startOffset = subjectPosition - scrollPortSize + sideInsets.mEnd;
+ // Note: `subjectPosition + subjectSize` means the position of the end border
+ // edge of the subject. When it touches the start edge of the scrollport, it
+ // is 100%.
+ nscoord endOffset = subjectPosition + subjectSize - sideInsets.mStart;
+ return Some(ScrollOffsets{startOffset, endOffset});
+}
+
+ScrollTimeline::ScrollOffsets ViewTimeline::ComputeInsets(
+ const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const {
+ // If view-timeline-inset is auto, it indicates to use the value of
+ // scroll-padding. We use logical dimension to map that start/end offset to
+ // the corresponding scroll-padding-{inline|block}-{start|end} values.
+ const WritingMode wm = aScrollFrame->GetScrolledFrame()->GetWritingMode();
+ const auto& scrollPadding =
+ LogicalMargin(wm, aScrollFrame->GetScrollPadding());
+ const bool isBlockAxis =
+ mAxis == StyleScrollAxis::Block ||
+ (mAxis == StyleScrollAxis::Horizontal && wm.IsVertical()) ||
+ (mAxis == StyleScrollAxis::Vertical && !wm.IsVertical());
+
+ // The percentages of view-timelne-inset is relative to the corresponding
+ // dimension of the relevant scrollport.
+ // https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset
+ const nsRect scrollPort = aScrollFrame->GetScrollPortRect();
+ const nscoord percentageBasis =
+ aOrientation == layers::ScrollDirection::eHorizontal ? scrollPort.width
+ : scrollPort.height;
+
+ nscoord startInset =
+ mInset.start.IsAuto()
+ ? (isBlockAxis ? scrollPadding.BStart(wm) : scrollPadding.IStart(wm))
+ : mInset.start.AsLengthPercentage().Resolve(percentageBasis);
+ nscoord endInset =
+ mInset.end.IsAuto()
+ ? (isBlockAxis ? scrollPadding.BEnd(wm) : scrollPadding.IEnd(wm))
+ : mInset.end.AsLengthPercentage().Resolve(percentageBasis);
+ return {startInset, endInset};
+}
+
+} // namespace mozilla::dom
diff --git a/dom/animation/ViewTimeline.h b/dom/animation/ViewTimeline.h
new file mode 100644
index 0000000000..a5ede6345d
--- /dev/null
+++ b/dom/animation/ViewTimeline.h
@@ -0,0 +1,86 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_ViewTimeline_h
+#define mozilla_dom_ViewTimeline_h
+
+#include "mozilla/dom/ScrollTimeline.h"
+
+namespace mozilla::dom {
+
+/*
+ * A view progress timeline is a segment of a scroll progress timeline that are
+ * scoped to the scroll positions in which any part of the associated element’s
+ * principal box intersects its nearest ancestor scrollport. So ViewTimeline
+ * is a special case of ScrollTimeline.
+ */
+class ViewTimeline final : public ScrollTimeline {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> mozilla::MakeAndAddRef(Args&&... aArgs);
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ViewTimeline, ScrollTimeline)
+
+ ViewTimeline() = delete;
+
+ // Note: |aSubject| is used as the subject which specifies view-timeline-name
+ // property, and we use this subject to look up its nearest scroll container.
+ static already_AddRefed<ViewTimeline> MakeNamed(
+ Document* aDocument, Element* aSubject, PseudoStyleType aPseudoType,
+ const StyleViewTimeline& aStyleTimeline);
+
+ static already_AddRefed<ViewTimeline> MakeAnonymous(
+ Document* aDocument, const NonOwningAnimationTarget& aTarget,
+ StyleScrollAxis aAxis, const StyleViewTimelineInset& aInset);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override {
+ return nullptr;
+ }
+
+ bool IsViewTimeline() const override { return true; }
+
+ void ReplacePropertiesWith(Element* aSubjectElement,
+ PseudoStyleType aPseudoType,
+ const StyleViewTimeline& aNew);
+
+ private:
+ ~ViewTimeline() = default;
+ ViewTimeline(Document* aDocument, const Scroller& aScroller,
+ StyleScrollAxis aAxis, Element* aSubject,
+ PseudoStyleType aSubjectPseudoType,
+ const StyleViewTimelineInset& aInset)
+ : ScrollTimeline(aDocument, aScroller, aAxis),
+ mSubject(aSubject),
+ mSubjectPseudoType(aSubjectPseudoType),
+ mInset(aInset) {}
+
+ Maybe<ScrollOffsets> ComputeOffsets(
+ const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const override;
+
+ ScrollOffsets ComputeInsets(const nsIScrollableFrame* aScrollFrame,
+ layers::ScrollDirection aOrientation) const;
+
+ // The subject element.
+ // 1. For view(), the subject element is the animation target.
+ // 2. For view-timeline property, the subject element is the element who
+ // defines this property.
+ RefPtr<Element> mSubject;
+ PseudoStyleType mSubjectPseudoType;
+
+ // FIXME: Bug 1817073. view-timeline-inset is an animatable property. However,
+ // the inset from view() is not animatable, so for named view timeline, this
+ // value depends on the animation style. Therefore, we have to check its style
+ // value when using it. For now, in order to simplify the implementation, we
+ // make |mInset| be fixed.
+ StyleViewTimelineInset mInset;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ViewTimeline_h
diff --git a/dom/animation/moz.build b/dom/animation/moz.build
new file mode 100644
index 0000000000..94f0f8625f
--- /dev/null
+++ b/dom/animation/moz.build
@@ -0,0 +1,79 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Animation")
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"]
+
+EXPORTS.mozilla.dom += [
+ "Animation.h",
+ "AnimationEffect.h",
+ "AnimationTimeline.h",
+ "CSSAnimation.h",
+ "CSSPseudoElement.h",
+ "CSSTransition.h",
+ "DocumentTimeline.h",
+ "KeyframeEffect.h",
+ "ScrollTimeline.h",
+ "ViewTimeline.h",
+]
+
+EXPORTS.mozilla += [
+ "AnimationComparator.h",
+ "AnimationEventDispatcher.h",
+ "AnimationPerformanceWarning.h",
+ "AnimationPropertySegment.h",
+ "AnimationTarget.h",
+ "AnimationUtils.h",
+ "ComputedTiming.h",
+ "EffectCompositor.h",
+ "EffectSet.h",
+ "ElementAnimationData.h",
+ "Keyframe.h",
+ "KeyframeEffectParams.h",
+ "KeyframeUtils.h",
+ "PendingAnimationTracker.h",
+ "PostRestyleMode.h",
+ "PseudoElementHashEntry.h",
+ "ScrollTimelineAnimationTracker.h",
+ "TimingParams.h",
+]
+
+UNIFIED_SOURCES += [
+ "Animation.cpp",
+ "AnimationEffect.cpp",
+ "AnimationEventDispatcher.cpp",
+ "AnimationPerformanceWarning.cpp",
+ "AnimationTimeline.cpp",
+ "AnimationUtils.cpp",
+ "CSSAnimation.cpp",
+ "CSSPseudoElement.cpp",
+ "CSSTransition.cpp",
+ "DocumentTimeline.cpp",
+ "EffectCompositor.cpp",
+ "EffectSet.cpp",
+ "ElementAnimationData.cpp",
+ "KeyframeEffect.cpp",
+ "KeyframeUtils.cpp",
+ "PendingAnimationTracker.cpp",
+ "ScrollTimeline.cpp",
+ "ScrollTimelineAnimationTracker.cpp",
+ "TimingParams.cpp",
+ "ViewTimeline.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/layout/base",
+ "/layout/painting",
+ "/layout/style",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini
new file mode 100644
index 0000000000..4001899a50
--- /dev/null
+++ b/dom/animation/test/chrome.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+prefs =
+ dom.animations-api.autoremove.enabled=true
+ dom.animations-api.compositing.enabled=true
+ dom.animations.mainthread-synchronization-with-geometric-animations=true
+ gfx.omta.background-color=true
+ layout.css.individual-transform.enabled=true
+ layout.css.motion-path.enabled=true
+support-files =
+ testcommon.js
+ ../../imptests/testharness.js
+ ../../imptests/testharnessreport.js
+ !/dom/animation/test/chrome/file_animate_xrays.html
+
+[chrome/test_animate_xrays.html]
+# file_animate_xrays.html needs to go in mochitest.ini since it is served
+# over HTTP
+[chrome/test_keyframe_effect_xrays.html]
+[chrome/test_animation_observers_async.html]
+[chrome/test_animation_observers_sync.html]
+[chrome/test_animation_performance_warning.html]
+[chrome/test_animation_properties.html]
+[chrome/test_animation_properties_display.html]
+[chrome/test_cssanimation_missing_keyframes.html]
+[chrome/test_generated_content_getAnimations.html]
+[chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html]
+[chrome/test_running_on_compositor.html]
+[chrome/test_simulate_compute_values_failure.html]
+skip-if = !debug
diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html
new file mode 100644
index 0000000000..2fa15b1764
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+for (let name of ["KeyframeEffect", "Animation"]) {
+ this[name] = function() {
+ throw `Called overridden ${name} constructor`;
+ };
+}
+</script>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html
new file mode 100644
index 0000000000..64df6db720
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674"
+ target="_blank">Mozilla Bug 1414674</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var anim = target.animate({opacity: [ 1, 0 ]}, 100 * MS_PER_SEC);
+ // The frames object should be accessible via x-ray.
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for Element.animate should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for Element.animate should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for Element.animate should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for Element.animate should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for Element.animate should be 1");
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers_async.html b/dom/animation/test/chrome/test_animation_observers_async.html
new file mode 100644
index 0000000000..7505baca53
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_async.html
@@ -0,0 +1,665 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (async tests)
+</title>
+<!--
+
+ This file contains tests for animation mutation observers that require
+ some asynchronous steps (e.g. waiting for animation events).
+
+ Where possible, however, we prefer to write synchronous tests since they are
+ less to timeout when run on automation. These synchronous tests are located
+ in test_animation_observers_sync.html.
+
+-->
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(newRecords => {
+ gRecords.push(...newRecords);
+});
+
+function setupAsynchronousObserver(t, options) {
+
+ gRecords = [];
+ t.add_cleanup(() => {
+ gObserver.disconnect();
+ });
+ gObserver.observe(options.subtree ? div.parentNode : div,
+ { animations: true, subtree: options.subtree });
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(aResolve => {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ assert_equals(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+function assert_records_any_order(expected, desc) {
+ // Generate a unique label for each Animation object.
+ let animation_labels = new Map();
+ let animation_counter = 0;
+ for (let record of gRecords) {
+ for (let a of [...record.addedAnimations, ...record.changedAnimations, ...record.removedAnimations]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+ for (let record of expected) {
+ for (let a of [...record.added, ...record.changed, ...record.removed]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+
+ function record_label(record) {
+ // Generate a label of the form:
+ //
+ // <added-animations>:<changed-animations>:<removed-animations>
+ let added = record.addedAnimations || record.added;
+ let changed = record.changedAnimations || record.changed;
+ let removed = record.removedAnimations || record.removed;
+ return [added .map(a => animation_labels.get(a)).sort().join(),
+ changed.map(a => animation_labels.get(a)).sort().join(),
+ removed.map(a => animation_labels.get(a)).sort().join()]
+ .join(":");
+ }
+
+ // Sort records by their label.
+ gRecords.sort((a, b) => record_label(a) < record_label(b));
+ expected.sort((a, b) => record_label(a) < record_label(b));
+
+ // Assert the sorted record lists are equal.
+ assert_records(expected, desc);
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+function runTest() {
+ [
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+ ].forEach(aOptions => {
+
+ var e = aOptions.target;
+
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ // Clear all styles once test finished since we re-use the same element
+ // in all test cases.
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to
+ // finish.
+ animations[0].currentTime = 99900;
+ }).then(() => {
+ return transitionEnd;
+ }).then(() => {
+ // After the transition has finished, the Animation should disappear.
+ assert_equals(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the the removal MutationRecord.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+ });
+ }, `single_transition ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ return animationEnd;
+ }).then(() => {
+ // After the animation has finished, the Animation should disappear.
+ assert_equals(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ return animationEnd;
+ }).then(() => {
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+ // Explicitly flush style to make sure the above style change happens.
+ // Normally we don't need explicit style flush if there is a waitForFrame()
+ // call but in this particular case we are in the middle of animation events'
+ // callback handling and requestAnimationFrame handling so that we have no
+ // chance to process styling even after the requestAnimationFrame handling.
+ flushComputedStyle(e);
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation_cancelled_fill ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ return animations[0].ready;
+ }).then(() => {
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ return animations[0].ready;
+ }).then(() => {
+ assert_true(
+ !animations[0].pending && animations[0].playState === "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ assert_equals(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `finish_from_pause ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ return animations[0].ready;
+ }).then(() => {
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ return animations[0].ready;
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ return waitForFrame();
+ }).then(() => {
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `play ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {;
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `coalesce_change_cancel ${aOptions.subtree ? ': subtree' : ''}`);
+
+ });
+}
+
+promise_test(t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: true });
+ t.add_cleanup(() => {
+ div.style = "";
+ flushComputedStyle(div);
+ });
+
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s; content: '';",
+ ".after::after" : "animation: anim 100s, anim 100s; " +
+ "content: '';"};
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ assert_equals(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+
+ var divBeforeAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::before"));
+ var divAfterAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::after"));
+ var childBPseudoAnimations =
+ docAnims.filter(x => (x.effect.target == childB &&
+ x.effect.pseudoElement == "::before"));
+
+ var seekRecords;
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the
+ // right records are generated, but we allow them to occur in any order.
+ return waitForFrame().then(() => {
+ assert_records_any_order(
+ [{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ return await_event(div, "animationend");
+ }).then(() => {
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+ });
+}, "tree_ordering: subtree");
+
+// Test that animations removed by auto-removal trigger an event
+promise_test(async t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: false });
+
+ // Start two animations such that one will be auto-removed
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+
+ // Wait for the MutationRecords corresponding to each addition.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [animA], changed: [], removed: [] },
+ { added: [animB], changed: [], removed: [] },
+ ],
+ 'records after animation start'
+ );
+
+ // Finish the animations -- this should cause animA to be replaced, and
+ // automatically removed.
+ animA.finish();
+ animB.finish();
+
+ // Wait for the MutationRecords corresponding to the timing changes and the
+ // subsequent removal to be delivered.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [animA], removed: [] },
+ { added: [], changed: [animB], removed: [] },
+ { added: [], changed: [], removed: [animA] },
+ ],
+ 'records after finishing'
+ );
+
+ // Restore animA.
+ animA.persist();
+
+ // Wait for the MutationRecord corresponding to the re-addition of animA.
+ await waitForNextFrame();
+
+ assert_records(
+ [{ added: [animA], changed: [], removed: [] }],
+ 'records after persisting'
+ );
+
+ // Tidy up
+ animA.cancel();
+ animB.cancel();
+
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [], removed: [animA] },
+ { added: [], changed: [], removed: [animB] },
+ ],
+ 'records after tidying up end'
+ );
+}, 'Animations automatically removed are reported');
+
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.autoremove.enabled", true],
+ ["dom.animations-api.implicit-keyframes.enabled", true],
+ ],
+ },
+ function() {
+ runTest();
+ done();
+ }
+);
+</script>
diff --git a/dom/animation/test/chrome/test_animation_observers_sync.html b/dom/animation/test/chrome/test_animation_observers_sync.html
new file mode 100644
index 0000000000..7a890fd2f4
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_sync.html
@@ -0,0 +1,1587 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (sync tests)
+</title>
+<!--
+
+ This file contains synchronous tests for animation mutation observers.
+
+ In general we prefer to write synchronous tests since they are less likely to
+ timeout when run on automation. Tests that require asynchronous steps (e.g.
+ waiting on events) should be added to test_animations_observers_async.html
+ instead.
+
+-->
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+</style>
+<script>
+
+/**
+ * Return a new MutationObserver which observing |target| element
+ * with { animations: true, subtree: |subtree| } option.
+ *
+ * NOTE: This observer should be used only with takeRecords(). If any of
+ * MutationRecords are observed in the callback of the MutationObserver,
+ * it will raise an assertion.
+ */
+function setupSynchronousObserver(t, target, subtree) {
+ var observer = new MutationObserver(records => {
+ assert_unreached("Any MutationRecords should not be observed in this " +
+ "callback");
+ });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ observer.observe(target, { animations: true, subtree });
+ return observer;
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_equals_records(actual, expected, desc) {
+ assert_equals(actual.length, expected.length, `${desc} - number of records`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_record_list(actual[i].addedAnimations,
+ expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(actual[i].changedAnimations,
+ expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(actual[i].removedAnimations,
+ expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+function runTest() {
+ [ { subtree: false },
+ { subtree: true }
+ ].forEach(aOptions => {
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = 109 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after currentTime during endDelay");
+
+ anim.effect.updateTiming({ endDelay: -110 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "change_enddelay_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after animation is added");
+ }, "zero_end_time");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ iterations: 0 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ iterations: Infinity });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_iterations");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ delay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ delay: 0 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_delay");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: "steps(2, start)" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ easing: "steps(2, end)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.updateTiming({ easing: "steps(2, end)" });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+ }, "change_easing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100, delay: -100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "negative_delay_in_constructor");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after animation is added");
+ }, "create_animation_without_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after setting the same target");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after setting redundant null");
+ }, "set_redundant_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is removed");
+ }, "set_null_animation_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = new Animation();
+ anim.play();
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+ }, "set_effect_on_null_effect_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after replace effects");
+ }, "replace_effect_targeting_on_the_same_element");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after replacing effects");
+ }, "replace_effect_targeting_on_the_same_element_not_in_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after composite is changed");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same composite");
+
+ }, "set_composite");
+
+ // Test that starting a single animation that is cancelled by calling
+ // cancel() dispatches an added notification and then a removed
+ // notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].cancel();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ // Re-trigger the animation.
+ animations[0].play();
+
+ // Single MutationRecord for the Animation (re-)addition.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+ }, "single_animation_cancelled_api");
+
+ // Test that updating a property on the Animation object dispatches a changed
+ // notification.
+ [
+ { prop: "playbackRate", val: 0.5 },
+ { prop: "startTime", val: 50 * MS_PER_SEC },
+ { prop: "currentTime", val: 50 * MS_PER_SEC },
+ ].forEach(aChangeTest => {
+ test(t => {
+ // We use a forwards fill mode so that even if the change we make causes
+ // the animation to become finished, it will still be "relevant" so we
+ // won't mark it as removed.
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the property.
+ animations[0][aChangeTest.prop] = aChangeTest.val;
+
+ // Make a redundant change.
+ animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation property change");
+ }, `single_animation_api_change_${aChangeTest.prop}`);
+ });
+
+ // Test that making a redundant change to currentTime while an Animation
+ // is pause-pending still generates a change MutationRecord since setting
+ // the currentTime to any value in this state aborts the pending pause.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].pause();
+
+ // We are now pause-pending. Even if we make a redundant change to the
+ // currentTime, we should still get a change record because setting the
+ // currentTime while pause-pending has the effect of cancelling a pause.
+ animations[0].currentTime = animations[0].currentTime;
+
+ // Two MutationRecords for the Animation changes: one for pausing, one
+ // for aborting the pause.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after pausing then seeking");
+ }, "change_currentTime_while_pause_pending");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ // a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Redundant finish.
+ animations[0].finish();
+
+ // Ensure no change records.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant finish()");
+ }, "finish_with_forwards_fill");
+
+ // Test that calling finish() on an Animation that does not fill forwards,
+ // dispatches a removal notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ // Single MutationRecord for the Animation removal.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finishing");
+ }, "finish_without_fill");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animation = div.getAnimations()[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [animation], changed: [], removed: []}],
+ "records after creation");
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [animation], removed: []}],
+ "records after id is changed");
+
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value with id");
+ }, "change_id");
+
+ // Test that calling reverse() dispatches a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].reverse();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after calling reverse()");
+ }, "reverse");
+
+ // Test that calling reverse() does *not* dispatch a changed notification
+ // when playbackRate == 0.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the middle and set playbackRate to zero.
+ animations[0].currentTime = 50 * MS_PER_SEC;
+ animations[0].playbackRate = 0;
+
+ // Two MutationRecords, one for each change.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after seeking and setting playbackRate");
+
+ animations[0].reverse();
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_zero_playbackRate");
+
+ // Test that reverse() on an Animation does *not* dispatch a changed
+ // notification when it throws an exception.
+ test(t => {
+ // Start an infinite animation
+ var div = addDiv(t, { style: "animation: anim 10s infinite" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Shift the animation into the future such that when we call reverse
+ // it will try to seek to the (infinite) end.
+ animations[0].startTime = 100 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after adjusting startTime");
+
+ // Reverse: should throw
+ assert_throws('InvalidStateError', () => {
+ animations[0].reverse();
+ }, 'reverse() on future infinite animation throws an exception');
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_exception");
+
+ // Test that attempting to start an animation that should already be finished
+ // does not send any notifications.
+ test(t => {
+ // Start an animation that should already be finished.
+ var div = addDiv(t, { style: "animation: anim 1s -2s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause no Animations to be created.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ "getAnimations().length after animation start");
+
+ // And we should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after attempted animation start");
+ }, "already_finished");
+
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: []}],
+ "records after creation");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+ animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: []}],
+ "records after the order is changed");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after applying the same order");
+ }, "animtion_order_change");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ iterationComposite: 'replace' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterationComposite is changed");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same iterationComposite");
+
+ }, "set_iterationComposite");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are changed");
+
+ anim.effect.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same keyframes");
+
+ anim.effect.setKeyframes(null);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are set to empty");
+
+ }, "set_keyframes");
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_property");
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ test(t => {
+ // A long transition with a predictable value.
+ var div =
+ addDiv(t, { style: "transition: z-index 100s -51s; " +
+ "z-index: 10;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+ div.style.zIndex = "100";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ let value = "83";
+ assert_equals(getComputedStyle(div).zIndex, value,
+ "half-way transition value");
+ div.style.zIndex = value;
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_value");
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: line-height 100s; " +
+ "line-height: 16px;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.lineHeight = "100px";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ div.style.lineHeight = "normal";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_noninterpolable");
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s step-start; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ var animations = div.getAnimations();
+
+ // The transition should cause the creation of a single Animation.
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ div.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ assert_true(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [], removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+ }, "single_transition_reversed");
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height" +
+ "background-color: yellow; line-height: 16px" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+
+ div.style.backgroundColor = "lime";
+ div.style.color = "blue";
+ div.style.lineHeight = "24px";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 3,
+ "getAnimations().length after transition starts");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ assert_equals(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ div.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ assert_equals(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ assert_equals(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ assert_equals_records(observer.takeRecords(),
+ seekRecords.concat({ added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+ }, "multiple_transitions");
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_name");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ div.style.animationDuration = "0.1s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_duration");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ div.style.animationDelay = "-200s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_delay");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ test(t => {
+ // A short, repeated animation.
+ var div =
+ addDiv(t, { style: "animation: anim 0.5s infinite;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ div.style.animationIterationCount = "1";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_iteration_count");
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(aChangeTest => {
+ test(t => {
+ // Start a long animation.
+ var div = addDiv(t, { style: "animation: anim 100s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ div.style[aChangeTest.prop] = aChangeTest.val;
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, `single_animation_change_${aChangeTest.name}`);
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ assert_true(animations[0].pending && animations[0].playState === "paused",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ assert_equals(animations[0].playState, "finished",
+ "playState after finishing again");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "finish_from_pause_pending");
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ test(t => {
+ // Animation with a forwards fill
+ var div =
+ addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_filling_forwards");
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause");
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause_while_pause_pending");
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "aborted_pause");
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_after_finish");
+
+ });
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var child = document.createElement("div");
+ div.appendChild(child);
+
+ var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
+ 100 * MS_PER_SEC);
+ var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim1], changed: [], removed: [] },
+ { added: [anim2], changed: [], removed: [] }],
+ "records after animation is added");
+
+ // After setting a new effect, we remove the current animation, anim1,
+ // because it is no longer attached to |div|, and then remove the previous
+ // animation, anim2. Finally, add back the anim1 which is in effect on
+ // |child| now. In addition, we sort them by tree order and they are
+ // batched.
+ anim1.effect = anim2.effect;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim1] }, // div
+ { added: [anim1], changed: [], removed: [anim2] }], // child
+ "records after animation effects are changed");
+ }, "set_effect_with_previous_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, document, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ var newTarget = document.createElement("div");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after setting a target");
+
+ anim.effect.target = addDiv(t);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] },
+ { added: [anim], changed: [], removed: [] }],
+ "records after setting a different target");
+ }, "set_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 200 * MS_PER_SEC,
+ pseudoElement: '::before' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: "auto" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ duration: "auto" });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime_on_pseudo_elements");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ pseudoElement: "::before" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.finish();
+ pAnim.finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is finished");
+ }, "exclude_animations_targeting_pseudo_elements");
+}
+
+W3CTest.runner.expectAssertions(0, 12); // bug 1189015
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.core.enabled", true],
+ ["dom.animations-api.implicit-keyframes.enabled", true],
+ ["dom.animations-api.timelines.enabled", true],
+ ],
+ },
+ function() {
+ runTest();
+ done();
+ }
+);
+
+</script>
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 0000000000..ca0572cfaa
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,1692 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1196114 - Test metadata related to which animation properties
+ are running on the compositor</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+.compositable {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+@keyframes fade {
+ from { opacity: 1 }
+ to { opacity: 0 }
+}
+@keyframes translate {
+ from { transform: none }
+ to { transform: translate(100px) }
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
+ target="_blank">Mozilla Bug 1196114</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This is used for obtaining localized strings.
+var gStringBundle;
+
+W3CTest.runner.requestLongerTimeout(2);
+
+const Services = SpecialPowers.Services;
+Services.locale.requestedLocales = ["en-US"];
+
+SpecialPowers.pushPrefEnv({ "set": [
+ // Need to set devPixelsPerPx explicitly to gain
+ // consistent pixel values in warning messages
+ // regardless of platform DPIs.
+ ["layout.css.devPixelsPerPx", 1],
+ ["layout.animation.prerender.partial", false],
+ ] },
+ start);
+
+function compare_property_state(a, b) {
+ if (a.property > b.property) {
+ return -1;
+ } else if (a.property < b.property) {
+ return 1;
+ }
+ if (a.runningOnCompositor != b.runningOnCompositor) {
+ return a.runningOnCompositor ? 1 : -1;
+ }
+ return a.warning > b.warning ? -1 : 1;
+}
+
+function assert_animation_property_state_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, 'Number of properties');
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(sortedActual[i].runningOnCompositor,
+ sortedExpected[i].runningOnCompositor,
+ 'runningOnCompositor property should match');
+ if (sortedExpected[i].warning instanceof RegExp) {
+ assert_regexp_match(sortedActual[i].warning,
+ sortedExpected[i].warning,
+ 'warning message should match');
+ } else if (sortedExpected[i].warning) {
+ assert_equals(sortedActual[i].warning,
+ gStringBundle.GetStringFromName(sortedExpected[i].warning),
+ 'warning message should match');
+ }
+ }
+}
+
+// Check that the animation is running on compositor and
+// warning property is not set for the CSS property regardless
+// expected values.
+function assert_all_properties_running_on_compositor(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_true(sortedActual[i].runningOnCompositor,
+ 'runningOnCompositor property should be true on ' +
+ sortedActual[i].property);
+ assert_not_exists(sortedActual[i], 'warning',
+ 'warning property should not be set');
+ }
+}
+
+function testBasicOperation() {
+ [
+ {
+ desc: 'animations on compositor',
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'animations on main thread',
+ frames: {
+ zIndex: ['0', '999']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ runningOnCompositor: false
+ }
+ ]
+ },
+ {
+ desc: 'animations on both threads',
+ frames: {
+ zIndex: ['0', '999'],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two animation properties on compositor thread',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two transform-like animation properties on compositor thread',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'opacity on compositor with animation of geometric properties',
+ frames: {
+ width: ['100px', '200px'],
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' property on the same animation object.
+function testKeyframesWithGeometricProperties() {
+ [
+ {
+ desc: 'transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ desc: 'translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ desc: 'opacity and transform-like properties',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ // First, a transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+
+ // Add a 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withGeometric);
+
+ // Remove the 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Finally, the transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ }, 'An animation has: ' + subtest.desc);
+ });
+}
+
+// Test that the expected set of geometric properties all block transform
+// animations.
+function testSetOfGeometricProperties() {
+ const geometricProperties = [
+ 'width', 'height',
+ 'top', 'right', 'bottom', 'left',
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
+ ];
+
+ geometricProperties.forEach(property => {
+ promise_test(async t => {
+ const keyframes = {
+ [propertyToIDL(property)]: [ '100px', '200px' ],
+ transform: [ 'translate(0px)', 'translate(100px)' ]
+ };
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ keyframes, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [
+ {
+ property,
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ }
+ ]);
+ }, `${property} is treated as a geometric property`);
+ });
+}
+
+// Performance warning tests that set and clear a style property.
+function testStyleChanges() {
+ [
+ {
+ desc: 'preserve-3d transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'preserve-3d translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'transform with backface-visibility:hidden',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ desc: 'translate with backface-visibility:hidden',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = subtest.style;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Performance warning tests that set and clear the id property
+function testIdChanges() {
+ [
+ {
+ desc: 'moz-element referencing a transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate and transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ if (subtest.createelement) {
+ addDiv(t, { style: subtest.createelement });
+ }
+
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = subtest.id;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testMultipleAnimations() {
+ [
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ style: 'transform-style: preserve-3d',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ style: 'backface-visibility: hidden;',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = subtest.style;
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = '';
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple animations: ' + subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' keyframe on the same animation object, where
+// multiple animation objects belong to the same element.
+// The 'width' property is added to animations[1].
+function testMultipleAnimationsWithGeometricKeyframes() {
+ [
+ {
+ desc: 'transform and opacity with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ desc: 'opacity and translate with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+ // First, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+
+ // Add a 'width' property to animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animations[1].effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withGeometric);
+ });
+
+ // Remove the 'width' property from animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animations[1].effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Finally, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ }, 'Multiple animations with geometric property: ' + subtest.desc);
+ });
+}
+
+// Tests adding/removing 'width' animation on the same element which has async
+// animations.
+function testMultipleAnimationsWithGeometricAnimations() {
+ [
+ {
+ desc: 'transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'translate',
+ animations: [
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity, transform, and translate',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+
+ var widthAnimation;
+
+ await waitForPaints();
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+
+ // Append 'width' animation on the same element.
+ widthAnimation = div.animate({ width: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ await waitForFrame();
+
+ // Now transform animations are not running on compositor because of
+ // the 'width' animation.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ // Remove the 'width' animation.
+ widthAnimation.cancel();
+ await waitForFrame();
+
+ // Now all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple async animations and geometric animation: ' + subtest.desc);
+ });
+}
+
+function testSmallElements() {
+ [
+ {
+ desc: 'opacity on small element',
+ frames: {
+ opacity: [0, 1]
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' +
+ // We need to set transform here to try creating an
+ // individual frame for this opacity element.
+ // Without this, this small element is created on the same
+ // nsIFrame of mochitest iframe, i.e. the document which are
+ // running this test, as a result the layer corresponding
+ // to the frame is sent to compositor.
+ 'transform: translateX(100px);' },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'transform on small element',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'translate on small element',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, subtest.style);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testSynchronizedAnimations() {
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [
+ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first/second)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ assert_animation_property_state_equals(
+ animC.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created second/third)');
+
+ promise_test(async t => {
+ const attrs = { class: 'compositable',
+ style: 'transition: all 100s' };
+ const elemA = addDiv(t, attrs);
+ const elemB = addDiv(t, attrs);
+ elemA.style.transform = 'translate(0px)';
+ elemB.style.marginLeft = '0px';
+ getComputedStyle(elemA).transform;
+ getComputedStyle(elemB).marginLeft;
+
+ // Generally the sequence of steps is as follows:
+ //
+ // Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
+ //
+ // In this test we want to set up two transitions during the "Events"
+ // stage but only flush style for one such that the second one is actually
+ // generated during the "Style" stage of the *next* tick.
+ //
+ // Web content often generates transitions in this way (that is, it doesn't
+ // pay regard to when style is flushed and nor should it). However, we
+ // still want transitions generated in this way to be synchronized.
+ let timeForFirstFrame;
+ await waitForIdle();
+
+ timeForFirstFrame = document.timeline.currentTime;
+ elemA.style.transform = 'translate(100px)';
+ // Flush style to trigger first transition
+ getComputedStyle(elemA).transform;
+ elemB.style.marginLeft = '100px';
+ // DON'T flush style here (this includes calling getAnimations!)
+ await waitForFrame();
+
+ assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
+ 'Should be on the other side of a tick');
+ // Wait another tick so we can let the transition be started
+ // by regular style resolution.
+ await waitForFrame();
+
+ const transitionA = elemA.getAnimations()[0];
+ assert_animation_property_state_equals(
+ transitionA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, 'Transitions created before and after a tick are synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ],
+ opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ },
+ { property: 'opacity',
+ runningOnCompositor: true
+ } ]);
+ }, 'Opacity animations on the same element continue running on the'
+ + ' compositor when transform animations are synchronized with geometric'
+ + ' animations');
+
+ promise_test(async t => {
+ const transitionElem = addDiv(t, {
+ style: 'margin-left: 0px; transition: margin-left 100s',
+ });
+ getComputedStyle(transitionElem).marginLeft;
+
+ await waitForFrame();
+
+ transitionElem.style.marginLeft = '100px';
+ const cssTransition = transitionElem.getAnimations()[0];
+
+ const animationElem = addDiv(t, {
+ class: 'compositable',
+ style: 'animation: translate 100s',
+ });
+ const cssAnimation = animationElem.getAnimations()[0];
+
+ await Promise.all([cssTransition.ready, cssAnimation.ready]);
+
+ assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
+ [{ property: 'transform',
+ runningOnCompositor: true }]);
+ }, 'CSS Animations are NOT synchronized with CSS Transitions');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ let animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the previous frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the next frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ animB.pause();
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ }, 'Paused animations are not synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Seek one of the animations so that their start times will differ
+ animA.currentTime = 5000;
+
+ await waitForPaints();
+
+ assert_not_equals(animA.startTime, animB.startTime,
+ 'Animations should have different start times');
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, 'Animations are synchronized based on when they are started'
+ + ' and NOT their start time');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false } ]);
+ // Restart animation
+ animA.pause();
+ animA.play();
+ await animA.ready;
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'An initially synchronized animation may be unsynchronized if restarted');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Clear target effect
+ animB.effect.target = null;
+
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'A geometric animation with no target element is not synchronized');
+}
+
+function testTooLargeFrame() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 10000px; height: 10000px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the area of the frame " +
+ "\\(\\d+\\) is too large relative to the viewport " +
+ "\\(larger than \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ // With WebRender we appear to stick to the previous layerization decision
+ // after changing the bounds back to a smaller object.
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - area');
+
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 20000px; height: 1px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the frame size " +
+ "\\(20000, 1\\) is too large relative to the viewport " +
+ "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
+ "maximum allowed value \\(\\d+, \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - dimensions');
+ });
+}
+
+function testTransformSVG() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ {
+ property: 'rotate',
+ frames: { rotate: ['0deg', '45deg'] },
+ },
+ {
+ property: 'scale',
+ frames: { scale: ['1', '2'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '100');
+ svg.setAttribute('height', '100');
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('width', '100');
+ rect.setAttribute('height', '100');
+ rect.setAttribute('fill', 'red');
+ svg.appendChild(rect);
+ document.body.appendChild(svg);
+ t.add_cleanup(() => {
+ svg.remove();
+ });
+
+ var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ svg.setAttribute('transform', 'translate(10, 20)');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformSVG'
+ } ]);
+ svg.removeAttribute('transform');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ }, subtest.property + ' of nsIFrame with SVG transform');
+ });
+}
+
+function testImportantRuleOverride() {
+ promise_test(async t => {
+ const elem = addDiv(t, { class: 'compositable' });
+ const anim = elem.animate({ translate: [ '0px', '100px' ],
+ rotate: ['0deg', '90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(anim);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [ { property: 'translate', runningOnCompositor: true },
+ { property: 'rotate', runningOnCompositor: true } ]
+ );
+
+ elem.style.setProperty('rotate', '45deg', 'important');
+ getComputedStyle(elem).rotate;
+
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ {
+ property: 'rotate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ ]
+ );
+ }, 'The animations of transform-like properties are not running on the ' +
+ 'compositor because any of the properties has important rules');
+}
+
+function testCurrentColor() {
+ if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) {
+ return; // skip this test until bug 1510030 landed.
+ }
+ promise_test(async t => {
+ const animation = addDivAndAnimate(t, { class: 'compositable' },
+ { backgroundColor: [ 'currentColor',
+ 'red' ] },
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'background-color',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasCurrentColor'
+ } ]);
+ }, 'Background color animations with `current-color` don\'t run on the '
+ + 'compositor');
+}
+
+function start() {
+ var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+ gStringBundle = bundleService
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ testBasicOperation();
+ testKeyframesWithGeometricProperties();
+ testSetOfGeometricProperties();
+ testStyleChanges();
+ testIdChanges();
+ testMultipleAnimations();
+ testMultipleAnimationsWithGeometricKeyframes();
+ testMultipleAnimationsWithGeometricAnimations();
+ testSmallElements();
+ testSynchronizedAnimations();
+ testTooLargeFrame();
+ testTransformSVG();
+ testImportantRuleOverride();
+ testCurrentColor();
+
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable',
+ style: 'animation: fade 100s' });
+ var cssAnimation = div.getAnimations()[0];
+ var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ cssAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ assert_animation_property_state_equals(
+ scriptAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ }, 'overridden animation');
+
+ done();
+}
+
+</script>
+
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html
new file mode 100644
index 0000000000..b9d6dcbef1
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,838 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffect.getProperties()</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
+ target="_blank">Mozilla Bug 1254419</a>
+<div id="log"></div>
+<style>
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a one-shorthand-property two-value property-indexed'
+ + ' specification',
+ frames: { margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (one shorthand and one of its longhand'
+ + ' components) two-value property-indexed specification',
+ frames: { marginTop: ['50px', '60px'],
+ margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '50px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property property-indexed specification with different'
+ + ' numbers of values',
+ frames: { left: ['10px', '20px', '30px'],
+ top: ['40px', '50px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed specification with an invalid value',
+ frames: { left: ['10px', '20px', '30px', '40px', '50px'],
+ top: ['15px', '25px', 'invalid', '45px', '55px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '15px', 'replace', 'linear'),
+ valueFormat(0.25, '25px', 'replace', 'linear'),
+ valueFormat(0.75, '45px', 'replace', 'linear'),
+ valueFormat(1, '55px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-value property-indexed specification that'
+ + ' needs to stringify its values',
+ frames: { opacity: [0, 1] },
+ expected: [ { property: 'opacity',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ + ' a greater shorthand',
+ frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ],
+ border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ + ' a lesser shorthand',
+ frames: { border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ],
+ borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for keyframe sequences
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a keyframe sequence specification with repeated values at'
+ + ' offset 0/1 with different easings',
+ frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
+ { offset: 0.0, left: '200px', easing: 'ease' },
+ { offset: 0.5, left: '300px', easing: 'linear' },
+ { offset: 1.0, left: '400px', easing: 'ease-out' },
+ { offset: 1.0, left: '500px', easing: 'step-end' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '100px', 'replace'),
+ valueFormat(0, '200px', 'replace', 'ease'),
+ valueFormat(0.5, '300px', 'replace', 'linear'),
+ valueFormat(1, '400px', 'replace'),
+ valueFormat(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a two-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a one shorthand property two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px' },
+ { offset: 1, margin: '20px 30px 40px 50px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (a shorthand and one of its component longhands)'
+ + ' two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
+ { offset: 1, marginTop: '70px',
+ margin: '30px 40px 50px 60px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for a given interior'
+ + ' offset',
+ frames: [ { offset: 0.0, left: '10px' },
+ { offset: 0.5, left: '20px' },
+ { offset: 0.5, left: '30px' },
+ { offset: 0.5, left: '40px' },
+ { offset: 1.0, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, left: '20px' },
+ { offset: 0, left: '30px' },
+ { offset: 1, left: '40px' },
+ { offset: 1, left: '50px' },
+ { offset: 1, left: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace'),
+ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a two-property four-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, top: '20px' },
+ { offset: 1, top: '30px' },
+ { offset: 1, left: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { offset: 1.00, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px', top: '20px' },
+ { offset: 0.25, left: '30px' },
+ { left: '40px' },
+ { left: '50px', top: '60px' },
+ { offset: 1.00, left: '70px', top: '80px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '30px', 'replace', 'linear'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(0.75, '50px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(0.75, '60px', 'replace', 'linear'),
+ valueFormat(1, '80px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with all omitted offsets',
+ frames: [ { left: '10px' },
+ { left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with different easing values, but the'
+ + ' same easing value for a given offset',
+ frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
+ { offset: 0.0, easing: 'ease', top: '20px'},
+ { offset: 0.5, easing: 'linear', left: '30px' },
+ { offset: 0.5, easing: 'linear', top: '40px' },
+ { offset: 1.0, easing: 'step-end', left: '50px' },
+ { offset: 1.0, easing: 'step-end', top: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'ease'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'ease'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence that needs to'
+ + ' stringify its values',
+ frames: [ { offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 } ],
+ expected: [ { property: 'opacity',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where shorthand precedes longhand',
+ frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where longhand precedes shorthand',
+ frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ + ' shorthand',
+ frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
+ border: '2px dotted rgb(4, 5, 6)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'a keyframe sequence where greater shorthand precedes'
+ + ' lesser shorthand',
+ frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
+ borderLeft: '1px solid rgb(1, 2, 3)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '100px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '110px', 'replace', 'linear'),
+ valueFormat(1, 'calc(10% + 100px)', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for CSS variable handling conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'CSS variables are resolved to their corresponding values',
+ frames: { left: ['10px', 'var(--var-100px)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, 'calc(-10% + 50px)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a missing property in initial keyframe',
+ frames: [ { },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe and there are some ' +
+ 'keyframes with the same offset',
+ frames: [ { },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { margin: '30px'} ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe',
+ frames: [ { margin: '5px' },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe and there are some ' +
+ 'keyframes with the same offsets',
+ frames: [ { margin: '5px' },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe where it forms the last'
+ + ' segment in the series',
+ frames: [ { margin: '5px' },
+ { marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px' } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe along with other values',
+ frames: [ { left: '10px' },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing properties in both of initial and final keyframe',
+ frames: [ { left: '5px', offset: 0.5 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing propertes in both of initial and final keyframe along '
+ + 'with other values',
+ frames: [ { left: '5px', offset: 0 },
+ { right: '5px', offset: 0.5 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in final keyframe with duplicate offset ' +
+ + 'along with other values',
+ frames: [ { left: '5px', right: '5px', offset: 0 },
+ { left: '8px', right: '8px', offset: 0 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in initial keyframe with duplicate offset '
+ + 'along with other values',
+ frames: [ { left: '10px', offset: 0 },
+ { left: '8px', right: '8px', offset: 1 },
+ { left: '5px', right: '5px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+];
+
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.core.enabled", true],
+ ["dom.animations-api.implicit-keyframes.enabled", true],
+ ],
+ },
+ function() {
+ gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ // Flush styles since getProperties currently does not. Rather, it
+ // returns the actual properties in use at the current time.
+ // However, we want to test what these properties will look like
+ // after the next restyle.
+ getComputedStyle(div).opacity;
+ assert_properties_equal(
+ animation.effect.getProperties(),
+ subtest.expected
+ );
+ }, subtest.desc);
+ });
+
+ done();
+ }
+);
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties_display.html b/dom/animation/test/chrome/test_animation_properties_display.html
new file mode 100644
index 0000000000..184fc9d3bc
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties_display.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1536688 - Test that 'display' is not included in
+ KeyframeEffect.getProperties() when using shorthand 'all'</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1536688"
+ target="_blank">Mozilla Bug 1536688</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+SpecialPowers.pushPrefEnv(
+ {
+ set: [['dom.animations-api.core.enabled', true]],
+ },
+ function() {
+ test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(
+ { all: ['unset', 'unset'] },
+ 100 * MS_PER_SEC
+ );
+ // Flush styles since getProperties does not.
+ getComputedStyle(div).opacity;
+
+ const properties = animation.effect.getProperties();
+ assert_false(
+ properties.some(property => property.property === 'display'),
+ 'Should not have a property for display'
+ );
+ });
+
+ done();
+ }
+);
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
new file mode 100644
index 0000000000..1d19386712
--- /dev/null
+++ b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1339332 - Test for missing keyframes in CSS Animation</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1339332"
+ target="_blank">Mozilla Bug 1339332</a>
+<div id="log"></div>
+<style>
+@keyframes missingFrom {
+ to {
+ text-align: right;
+ }
+}
+@keyframes missingBoth {
+ 50% {
+ text-align: right;
+ }
+}
+@keyframes missingTo {
+ from {
+ text-align: right;
+ }
+}
+</style>
+<script>
+'use strict';
+
+const gTests = [
+ { desc: 'missing "from" keyframe',
+ animationName: 'missingFrom',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(1, 'right', 'replace')] } ]
+ },
+ { desc: 'missing "to" keyframe',
+ animationName: 'missingTo',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+ { desc: 'missing "from" and "to" keyframes',
+ animationName: 'missingBoth',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(.5, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+];
+
+SpecialPowers.pushPrefEnv(
+ { set: [["dom.animations-api.core.enabled", true]] },
+ function() {
+ gTests.forEach(function(subtest) {
+ test(function(t) {
+ const div = addDiv(t);
+ div.style.animation = `${ subtest.animationName } 1000s`;
+ const animation = div.getAnimations()[0];
+ assert_properties_equal(animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+
+ done();
+ }
+);
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html
new file mode 100644
index 0000000000..08de27f57d
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() for generated-content elements</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+@keyframes anim2 { }
+.before::before {
+ content: '';
+ animation: anim 100s;
+}
+.after::after {
+ content: '';
+ animation: anim 100s, anim2 100s;
+}
+</style>
+</head>
+<body>
+<div id='root' class='before after'>
+ <div class='before'></div>
+ <div></div>
+</div>
+<script>
+'use strict';
+
+const {Cc, Ci, Cu} = SpecialPowers;
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ return walker;
+}
+
+test(function(t) {
+ var root = document.getElementById('root');
+ // Flush first to make sure the generated-content elements are ready
+ // in the tree.
+ flushComputedStyle(root);
+ var before = getWalker(root).firstChild();
+ var after = getWalker(root).lastChild();
+
+ // Sanity Checks
+ assert_equals(document.getAnimations().length, 4,
+ 'All animations in this document');
+ assert_equals(before.tagName, '_moz_generated_content_before',
+ 'First child is ::before element');
+ assert_equals(after.tagName, '_moz_generated_content_after',
+ 'Last child is ::after element');
+
+ // Test Element.getAnimations() for generated-content elements
+ assert_equals(before.getAnimations().length, 1,
+ 'Animations of ::before generated-content element');
+ assert_equals(after.getAnimations().length, 2,
+ 'Animations of ::after generated-content element');
+}, 'Element.getAnimations() used on generated-content elements');
+
+test(function(t) {
+ var root = document.getElementById('root');
+ flushComputedStyle(root);
+ var walker = getWalker(root);
+
+ var animations = [];
+ var element = walker.currentNode;
+ while (element) {
+ if (element.getAnimations) {
+ animations = [...animations, ...element.getAnimations()];
+ }
+ element = walker.nextNode();
+ }
+
+ assert_equals(animations.length, document.getAnimations().length,
+ 'The number of animations got by DeepTreeWalker and ' +
+ 'document.getAnimations() should be the same');
+}, 'Element.getAnimations() used by traversing DeepTreeWalker');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_keyframe_effect_xrays.html b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
new file mode 100644
index 0000000000..ca3e712ac5
--- /dev/null
+++ b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674"
+ target="_blank">Mozilla Bug 1414674</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var effect = new win.KeyframeEffect(target, [
+ {opacity: 1, offset: 0},
+ {opacity: 0, offset: 1},
+ ], {duration: 100 * MS_PER_SEC, fill: "forwards"});
+ // The frames object should be accessible via x-ray.
+ var frames = effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for KeyframeEffect ctor should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for KeyframeEffect ctor should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for KeyframeEffect ctor should be 1");
+ var animation = new win.Animation(effect, document.timeline);
+ animation.play();
+ t.done();
+ }));
+}, 'Calling KeyframeEffect() ctor across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
new file mode 100644
index 0000000000..f8efaa6baf
--- /dev/null
+++ b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ // Set up a MutationObserver for animations.
+ const observer = new MutationObserver(() => {});
+ observer.observe(document.documentElement, {
+ animations: true,
+ subtree: true,
+ });
+
+ // Create a CSS transition in a shadow tree.
+ let s = document.createElement('shadow-test');
+ document.documentElement.appendChild(s);
+ s.attachShadow({mode:"open"});
+
+ let property = 'opacity';
+ let initial = '1';
+ let finalValue = '0';
+
+ let div = document.createElement('div');
+ div.style = `${property}:${initial};transition:${property} 2s;`
+
+ s.shadowRoot.appendChild(div);
+ div.offsetWidth;
+
+ div.style[property] = finalValue;
+
+ const eventWatcher = new EventWatcher(t, div, ['transitionstart']);
+
+ // Trigger a CSS transition.
+ getComputedStyle(div)[property];
+
+ // Wait for a transitionend event to make sure the transition has been started.
+ await eventWatcher.wait_for('transitionstart');
+
+ // Now remove the element to notify it to the observer
+ div.parentNode.removeChild(div);
+});
+</script>
diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html
new file mode 100644
index 0000000000..532c11258f
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,1516 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
+ running on the compositor or not</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+@keyframes transform-starts-with-none {
+ 0% { transform: none }
+ 99% { transform: none }
+ 100% { transform: translate(100px) }
+}
+@keyframes opacity {
+ to { opacity: 0 }
+}
+@keyframes zIndex_and_translate {
+ to { z-index: 999; transform: translate(100px); }
+}
+@keyframes z-index {
+ to { z-index: 999; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes rotate-and-opacity {
+ from { transform: rotate(0deg); opacity: 1;}
+ to { transform: rotate(360deg); opacity: 0;}
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+/** Test for bug 1045994 - Add a chrome-only property to inspect if an
+ animation is running on the compositor or not **/
+
+const omtaEnabled = isOMTAEnabled();
+
+function assert_animation_is_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, omtaEnabled,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+function assert_animation_is_not_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, false,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+promise_test(async t => {
+ // FIXME: When we implement Element.animate, use that here instead of CSS
+ // so that we remove any dependency on the CSS mapping.
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ // If the animation starts at the current timeline time, we need to wait for
+ // one more frame to avoid receiving the fake timer-based MozAfterPaint event.
+ // FIXME: Bug 1419226: Drop this 'animation.ready' and 'waitForFrame'. Once
+ // MozAfterPaint is fired reliably, we just need to wait for a MozAfterPaint
+ // here.
+ await animation.ready;
+
+ if (animationStartsRightNow(animation)) {
+ await waitForNextFrame();
+ }
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ await animation.ready;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+}, '');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: z-index 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "z-index"');
+}, 'isRunningOnCompositor is false for animation of "z-index"');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: zIndex_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when the animation has two properties, where one can run'
+ + ' on the compositor, the other cannot');
+}, 'isRunningOnCompositor is true if the animation has at least one ' +
+ 'property can run on compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.pause();
+ await animation.ready;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when animation.pause() is called');
+}, 'isRunningOnCompositor is false when the animation.pause() is called');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.finish();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.finish() is called');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.finish() is called');
+}, 'isRunningOnCompositor is false when the animation.finish() is called');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after manually seeking the animation to the end');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after manually seeking the animation to the end');
+}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
+ 'the end');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.cancel();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.cancel() is called');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.cancel() is called');
+}, 'isRunningOnCompositor is false when animation.cancel() is called');
+
+// This is to test that we don't simply clobber the flag when ticking
+// animations and then set it again during painting.
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ t.step(() => {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ resolve();
+ });
+ });
+}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var observer = new MutationObserver(records => {
+ var changedAnimation;
+
+ records.forEach(record => {
+ changedAnimation =
+ record.changedAnimations.find(changedAnim => {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(() => {
+ assert_true(!!changedAnimation, 'The animation should be recorded '
+ + 'as one of the changedAnimations');
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in MutationObserver callback');
+ });
+
+ resolve();
+ });
+ observer.observe(div, { animations: true, subtree: false });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ div.style.animationDuration = "200s";
+ });
+}, 'isRunningOnCompositor is true in MutationObserver callback');
+
+// This is to test that we don't temporarily clear the flag when forcing
+// an unthrottled sample.
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(() => {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ // we have to wait at least 200ms because this animation is
+ // unthrottled on every 200ms.
+ // See https://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
+ if (window.performance.now() - timeAtStart > 200) {
+ resolve();
+ return;
+ }
+ window.requestAnimationFrame(handleFrame);
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
+ 'overflow animation');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor'
+ + ' during playback for opacity transition');
+}, 'isRunningOnCompositor for transitions');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'If an animation has a property that can run on the compositor and a '
+ + 'property that cannot (due to Gecko limitations) but where the latter'
+ + 'property is overridden in the CSS cascade, the animation should '
+ + 'still report that it is running on the compositor');
+}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
+ 'running on the compositor is overridden in the CSS cascade');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when the animation is set a shorter duration than current time');
+}, 'animation is immediately removed from compositor' +
+ 'when the duration is made shorter than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when finished');
+
+ animation.effect.updateTiming({ duration: 1000 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+}, 'animation is added to compositor' +
+ ' when the duration is made longer than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC });
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endDelay is changed');
+
+ animation.currentTime = 110 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is during endDelay');
+}, 'animation is removed from compositor' +
+ ' when current time is made longer than the duration even during endDelay');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when endTime is negative value');
+}, 'animation is removed from compositor' +
+ ' when endTime is negative value');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -100 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endTime is positive and endDelay is negative');
+ animation.currentTime = 110 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is after endTime');
+}, 'animation is NOT running on compositor' +
+ ' when endTime is positive and endDelay is negative');
+
+promise_test(async t => {
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var div = addDiv(t);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation with null target reports that it is not running ' +
+ 'on the compositor');
+
+ animation.effect.target = div;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor ' +
+ 'after setting a valid target');
+}, 'animation is added to the compositor when setting a valid target');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.target = null;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the ' +
+ 'compositor after setting null target');
+}, 'animation is removed from the compositor when setting null target');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor after delay phase');
+}, 'animation with fill:backwards in delay phase is running on the ' +
+ ' compositor while it is in delay phase');
+
+promise_test(async t => {
+ const animation = addDiv(t).animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 200 * MS_PER_SEC;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with negative playback rate is runnning on the'
+ + ' compositor even before it reaches the active interval');
+}, 'animation with negative playback rate is sent to the compositor even in'
+ + ' after phase');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
+
+ var another = addDiv(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe reports ' +
+ 'that it is running on the compositor from the begining');
+
+ animation.effect.target = another;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe keeps ' +
+ 'running on the compositor after changing the target ' +
+ 'element');
+}, '100% opacity animations with keeps running on the ' +
+ 'compositor after changing the target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }]);
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set by using setKeyframes reports ' +
+ 'that it is running on the compositor');
+}, '100% opacity animation set up by converting an existing animation with ' +
+ 'cannot be run on the compositor, is running on the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+ var effect = new KeyframeEffect(div,
+ [{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }],
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set up by changing effects reports ' +
+ 'that it is running on the compositor');
+}, '100% opacity animation set up by changing the effects on an existing ' +
+ 'animation which cannot be run on the compositor, is running on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has 100% opacity style with ' +
+ '!important flag reports that it is not running on the compositor');
+ // Clear important flag from the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor after '
+ + 'clearing the !important flag');
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority opacity animation on an element ' +
+ 'reports that it is running on the compositor');
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority opacity animation on the same ' +
+ 'element also reports that it is running on the compositor');
+}, 'Opacity animations on the same element run on the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ getComputedStyle(div).opacity;
+
+ var transition = div.getAnimations()[0];
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'An opacity animation on an element reports that' +
+ 'that it is running on the compositor');
+ assert_animation_is_running_on_compositor(transition,
+ 'An opacity transition on the same element reports that ' +
+ 'it is running on the compositor');
+}, 'Both of transition and script animation on the same element run on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is no longer running on the compositor after ' +
+ 'removing from the element');
+ animation.effect.target = importantOpacityElement;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is NOT running on the compositor even after ' +
+ 'being applied to a different element which has an ' +
+ '!important opacity declaration');
+}, 'Animation continues not running on the compositor after being ' +
+ 'applied to an element which has an important declaration and ' +
+ 'having previously been temporarily associated with no target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ lowerAnimation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(lowerAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ lowerAnimation.effect.target = another;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority animation begins running ' +
+ 'on the compositor after being applied to an element ' +
+ 'which has a higher-priority animation');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation continues to run on the ' +
+ 'compositor even after a lower-priority animation is ' +
+ 'applied to the same element');
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a higher-priority animation and after ' +
+ 'being temporarily associated with no target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ higherAnimation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(higherAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ higherAnimation.effect.target = div;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'Animation continues running on the compositor after ' +
+ 'a higher-priority animation applied to the same element');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation begins to running on the ' +
+ 'compositor after being applied to an element which has ' +
+ 'a lower-priority-animation');
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a lower-priority animation once after ' +
+ 'disassociating with an element');
+
+var delayPhaseTests = [
+ {
+ desc: 'script animation of opacity',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of opacity',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ return div.getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in the delay phase');
+});
+
+// The purpose of thie test cases is to check that
+// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
+// after transform style on the frame is removed.
+var delayPhaseWithTransformStyleTests = [
+ {
+ desc: 'script animation of transform with transform style',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of transform with transform style',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
+ 'transform: translateX(10px)'});
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseWithTransformStyleTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ await animation.ready;
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it keeps running on the '
+ + 'compositor after removing the initial transform style');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
+ 'the initial transform style during the delay phase');
+});
+
+var startsWithNoneTests = [
+ {
+ desc: 'script animation of transform starts with transform:none segment',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
+ },
+ },
+ {
+ desc: 'CSS animation of transform starts with transform:none segment',
+ setupAnimation: t => {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in transform:none segment');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in transform:none segment');
+});
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'opacity: 1 ! important' });
+
+ var animation = div.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Clear the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'clearing the opacity style on the element');
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor even if the animation is in the delay phase');
+
+promise_test(async t => {
+ var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
+ var anotherDiv = addDiv(t);
+
+ var animation = opaqueDiv.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Changing target element to another element which has no opacity style.
+ animation.effect.target = anotherDiv;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'changing the target element to another elemenent having no '
+ + 'opacity style');
+}, 'Changing target element of opacity animation sends the animation to the ' +
+ 'the compositor even if the animation is in the delay phase');
+
+promise_test(async t => {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Width animation reports that it is not running on the compositor '
+ + 'in the delay phase');
+ // Changing to property runnable on the compositor.
+ animation.effect.setKeyframes({ opacity: [0, 1] });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor '
+ + 'after changing the property from width property in the delay phase');
+}, 'Dynamic change to a property runnable on the compositor ' +
+ 'in the delay phase');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor even if the ' +
+ 'property is overridden by an !important rule');
+}, 'Transitions override important rules');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var [transition, animation] = div.getAnimations();
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(transition,
+ 'Transition suppressed by an animation which is overridden by an ' +
+ '!important rule reports that it is NOT running on the compositor');
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+}, 'Neither transition nor animation does run on the compositor if the ' +
+ 'property is overridden by an !important rule');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'display: table' });
+ var animation =
+ div.animate({ transform: ['rotate(0deg)', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transform animation on display:table element should be running on the'
+ + ' compositor');
+}, 'Transform animation on display:table element runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'display: table' });
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ const effect = new KeyframeEffect(div,
+ { transform: ['none', 'none']},
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.effect = effect;
+
+ await waitForNextFrame();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(
+ animation,
+ 'Transform animation on table element should be running on the compositor'
+ );
+}, 'Empty transform effect assigned after the fact to display:table content'
+ + ' runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+}, 'background-color animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+
+ // Add a red opaque background image covering the background color animation.
+ div.style.backgroundImage =
+ 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64' +
+ 'paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC)';
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ // Bug 1712246. We should optimize this case eventually.
+ //assert_animation_is_not_running_on_compositor(animation,
+ // 'Opaque background image stops background-color animations from running ' +
+ // 'on the compositor');
+}, 'Opaque background image stops background-color animations from running ' +
+ ' on the compositor');
+
+promise_test(async t => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["gfx.omta.background-color", false]]
+ });
+
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'background-color animation should NOT be running on the compositor ' +
+ 'if the pref is disabled');
+}, 'background-color animation does not run on the compositor if the pref ' +
+ 'is disabled');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'translate animation should be running on the compositor');
+}, 'translate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+}, 'rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ scale: ['1 1', '2 2'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'scale animation should be running on the compositor');
+}, 'scale animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'multiple transform-like properties animation should be running on the ' +
+ 'compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['none', 'none'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path animation should be running on the compositor even if ' +
+ 'it is always none');
+}, 'offset-path none animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['path("M0 0l100 100")',
+ 'path("M0 0l200 200")'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path animation should be running on the compositor');
+}, 'offset-path animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-distance animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-distance animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-distance animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetRotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-rotate animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-rotate animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetAnchor: ['0% 0%', '100% 100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-anchor animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-anchor animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-anchor animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor even though we do not have ' +
+ 'offset-path');
+
+ div.style.offsetPath = 'path("M50 0v100")';
+ getComputedStyle(div).offsetPath;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor');
+
+}, 'Multiple transform-like properties (include motion-path) animation runs ' +
+ 'on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the transform-like property is overridden ' +
+ 'by an !important rule');
+
+// FIXME: Bug 1593106: We should still run the animations on the compositor if
+// offset-* doesn't have any effect.
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('offset-distance', '50%', 'important');
+ getComputedStyle(div).offsetDistance;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the offset-* property is overridden ' +
+ 'by an !important rule');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is still running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation still runs on the ' +
+ 'compositor because the overridden-by-!important property does not have ' +
+ 'animation');
+
+promise_test(async t => {
+ // We should run the animations on the compositor for this case:
+ // 1. A transition of 'translate'
+ // 2. An !important rule on 'translate'
+ // 3. An animation of 'scale'
+ const div = addDiv(t, { style: 'translate: 100px !important;' });
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+ div.style.transition = 'translate 100s';
+ getComputedStyle(div).transition;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+
+ div.style.setProperty('translate', '200px', 'important');
+ getComputedStyle(div).translate;
+
+ const anims = div.getAnimations();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(anims[0],
+ `${anims[0].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+ assert_animation_is_running_on_compositor(anims[1],
+ `${anims[1].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+}, 'Transform-like animations and transitions still runs on the compositor ' +
+ 'because the !important rule is overridden by a transition, and the ' +
+ 'transition property does not have animations');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const targetA = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetB = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetC = addDiv(t);
+ container.appendChild(targetA);
+ targetA.append(targetB);
+ targetB.append(targetC);
+
+ const animation1 = targetA.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = targetC.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+}, 'Transform-like animations in the 3d rendering context should runs on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const innerA = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ // The frame of innerB is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerB = addDiv(t, { style: 'rotate: 0 1 1 100deg; ' +
+ 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerB2 = addDiv(t, { style: 'rotate: 0 1 1 45deg;' });
+ const innerBText = document.createTextNode("innerB");
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerB.appendChild(innerBText);
+ innerB.appendChild(innerB2);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB2.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its parent frames is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its parent frames is too large');
+ }
+ innerBText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if it is the ancestor of ones whose frames are too ' +
+ 'large or its ancestor is not allowed to run on the compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ // The frame of innerA is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerA = addDiv(t, { style: 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerAText = document.createTextNode("innerA");
+ const innerB = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerA.appendChild(innerAText);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its frame size is too large');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its previous sibling frame is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_not_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its frame size is too large');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its previous sibling frame is too large');
+ }
+ innerAText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if its previous sibling frame size is too large');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_simulate_compute_values_failure.html b/dom/animation/test/chrome/test_simulate_compute_values_failure.html
new file mode 100644
index 0000000000..2ee041c9f8
--- /dev/null
+++ b/dom/animation/test/chrome/test_simulate_compute_values_failure.html
@@ -0,0 +1,371 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1276688 - Test for properties that parse correctly but which we fail
+ to convert to computed values</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276688"
+ target="_blank">Mozilla Bug 1276688</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var compareProperties = (a, b) =>
+ a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
+
+ var sortedActual = actual.sort(compareProperties);
+ var sortedExpected = expected.sort(compareProperties);
+
+ // We want to serialize the values in the following form:
+ //
+ // { offset: 0, easing: linear, composite: replace, value: 5px }, ...
+ //
+ // So that we can just compare strings and, in the failure case,
+ // easily see where the differences lie.
+ var serializeMember = value => {
+ return typeof value === 'undefined' ? '<not set>' : value;
+ }
+ var serializeValues = values =>
+ values.map(value =>
+ '{ ' +
+ [ 'offset', 'value', 'easing', 'composite' ].map(
+ member => `${member}: ${serializeMember(value[member])}`
+ ).join(', ') +
+ ' }')
+ .join(', ');
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(serializeValues(sortedActual[i].values),
+ serializeValues(sortedExpected[i].values),
+ `Values arrays do not match for `
+ + `${sortedActual[i].property} property`);
+ }
+}
+
+// Shorthand for constructing a value object
+function value(offset, value, composite, easing) {
+ return { offset, value, easing, composite };
+}
+
+var gTests = [
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe where we have enough values to create'
+ + ' a final segment',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where it forms the last segment in the series',
+ frames: [ { margin: '5px' },
+ { margin: '5px',
+ marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px',
+ // margin-top sorts last and only it will be missing since
+ // the other longhand components are specified
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where we have enough values to create'
+ + ' an initial segment',
+ frames: [ { margin: '5px' },
+ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' intermediate keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values',
+ // simulateComputeValuesFailure only applies to shorthands so we can set
+ // it on the same keyframe and it will only apply to |margin| and not
+ // |left|.
+ frames: [ { margin: '77%', left: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '77%', right: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', right: '20px' } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '5px', right: '10px' },
+ { margin: '5px', right: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe by itself',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+];
+
+setup({explicit_done: true});
+
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.core.enabled", false],
+ ["dom.animations-api.implicit-keyframes.enabled", false],
+ ],
+ },
+ function() {
+ gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ assert_properties_equal(
+ animation.effect.getProperties(),
+ subtest.expected
+ );
+ }, subtest.desc);
+ });
+
+ done();
+ }
+);
+</script>
+</body>
diff --git a/dom/animation/test/crashtests/1134538.html b/dom/animation/test/crashtests/1134538.html
new file mode 100644
index 0000000000..136d63deee
--- /dev/null
+++ b/dom/animation/test/crashtests/1134538.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<div contenteditable=true style="transition-property: width;"></div>
+<style>
+html {
+ transition-delay: 18446744073709551584s;
+ transform: rotate(0deg);
+}
+</style>
diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html
new file mode 100644
index 0000000000..6ac40b2fb8
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-1.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html
new file mode 100644
index 0000000000..7bae8a3116
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-2.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html
new file mode 100644
index 0000000000..1bc2179a86
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-3.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html
new file mode 100644
index 0000000000..eba13c396a
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-4.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html
new file mode 100644
index 0000000000..73b6f22c4b
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-5.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces negative values passed
+ to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html
new file mode 100644
index 0000000000..aaf80eeec3
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-6.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces values greater than 1
+ which are passed to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffect(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html
new file mode 100644
index 0000000000..aa10ff3ab8
--- /dev/null
+++ b/dom/animation/test/crashtests/1239889-1.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1239889</title>
+ </head>
+ <body>
+ </body>
+ <script>
+ var div = document.createElement('div');
+ var effect = new KeyframeEffect(div, { opacity: [0, 1] });
+ requestAnimationFrame(() => {
+ document.body.appendChild(div);
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html
new file mode 100644
index 0000000000..13b2e2d7e7
--- /dev/null
+++ b/dom/animation/test/crashtests/1244595-1.html
@@ -0,0 +1,3 @@
+<div id=target><script>
+ var player = target.animate([{background: 'green'}, {background: 'green'}]);
+</script>
diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html
new file mode 100644
index 0000000000..e0b0495388
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - scale function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 1px; height: 1px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "scale(8)" },
+ { "transform": "scale(9.5e+307)" },
+ { "transform": "scale(32)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html
new file mode 100644
index 0000000000..da0e8605bd
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-2.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - rotate function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 100px; height: 100px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "rotate(8rad)" },
+ { "transform": "rotate(9.5e+307rad)" },
+ { "transform": "rotate(32rad)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html
new file mode 100644
index 0000000000..2565aa6eb8
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1-inner.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<head>
+<script>
+function start() {
+ var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12);
+ document.write('<html><body></body></html>');
+
+ setTimeout(function() { animation.play(); }, 4);
+ setTimeout(function() {
+ animation.timeline = undefined;
+
+ SpecialPowers.Cu.forceGC();
+ window.top.continueTest();
+ }, 5);
+}
+</script>
+</head>
+<body onload="start()"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html
new file mode 100644
index 0000000000..71b6c24148
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<script>
+var count = 0;
+
+function start() {
+ if (++count > 10) {
+ document.documentElement.className = "";
+ return;
+ }
+
+ var frame = document.getElementById("frame");
+ frame.src = "./1277272-1-inner.html";
+}
+
+function continueTest() {
+ setTimeout(start.bind(window), 1);
+}
+
+</script>
+</head>
+<body onload="start()"></body>
+<iframe id="frame">
+</html>
diff --git a/dom/animation/test/crashtests/1278485-1.html b/dom/animation/test/crashtests/1278485-1.html
new file mode 100644
index 0000000000..e7347f5d84
--- /dev/null
+++ b/dom/animation/test/crashtests/1278485-1.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, -1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, -1e+39)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, 1e+39)" });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1282691-1.html b/dom/animation/test/crashtests/1282691-1.html
new file mode 100644
index 0000000000..ab6e1a26c0
--- /dev/null
+++ b/dom/animation/test/crashtests/1282691-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset=utf-8>
+<script>
+
+function boom() {
+ const div = document.createElement('div');
+ const anim = div.animate([{}], {});
+ document.documentElement.appendChild(div);
+ anim.pause();
+ document.documentElement.removeChild(div);
+
+ requestAnimationFrame(() => {
+ document.documentElement.appendChild(div);
+ anim.play();
+ document.documentElement.className = '';
+ });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1291413-1.html b/dom/animation/test/crashtests/1291413-1.html
new file mode 100644
index 0000000000..691a746c6e
--- /dev/null
+++ b/dom/animation/test/crashtests/1291413-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html class=reftest-wait>
+<script>
+window.onload = () => {
+ const div = document.createElement('div');
+
+ document.documentElement.appendChild(div);
+ const anim = div.animate([], 1000);
+
+ anim.ready.then(() => {
+ anim.pause();
+ anim.reverse();
+ anim.playbackRate = 0;
+ anim.ready.then(() => {
+ document.documentElement.className = '';
+ });
+ });
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1291413-2.html b/dom/animation/test/crashtests/1291413-2.html
new file mode 100644
index 0000000000..fca3a93800
--- /dev/null
+++ b/dom/animation/test/crashtests/1291413-2.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html class=reftest-wait>
+<script>
+window.onload = () => {
+ const div = document.createElement('div');
+
+ document.documentElement.appendChild(div);
+ const anim = div.animate([], 1000);
+
+ anim.ready.then(() => {
+ anim.pause();
+ anim.reverse();
+ anim.playbackRate = 0;
+ anim.playbackRate = 1;
+ anim.ready.then(() => {
+ document.documentElement.className = '';
+ });
+ });
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1304886-1.html b/dom/animation/test/crashtests/1304886-1.html
new file mode 100644
index 0000000000..703ef902b9
--- /dev/null
+++ b/dom/animation/test/crashtests/1304886-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<script>
+window.onload=function(){
+ var e = document.createElement("div");
+ document.documentElement.appendChild(e);
+ e.animate([{"font":"status-bar"},
+ {"font":"unset"}],
+ {duration:6,
+ iterationStart:4,
+ iterationComposite:"accumulate"});
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1309198-1.html b/dom/animation/test/crashtests/1309198-1.html
new file mode 100644
index 0000000000..7fad5782c0
--- /dev/null
+++ b/dom/animation/test/crashtests/1309198-1.html
@@ -0,0 +1,40 @@
+<script>
+function start() {
+ o53=document.createElement('frameset');
+ o254=document.createElement('iframe');
+ o280=document.createElement('audio');
+ o317=document.documentElement;
+ o508=document.createElement('li');
+ o508.appendChild(o317);
+ o590=document.createElement('li');
+ o594=document.createElement('track');
+ o280.appendChild(o594);
+ o647=document.createElement('ol');
+ o654=document.createElement('li');
+ o647.appendChild(o654);
+ o654.insertAdjacentHTML('beforebegin','<iframe>');
+ document.write('<html><body></body></html>');
+ o955=document.documentElement;
+ document.documentElement.appendChild(o647);
+ o590.appendChild(o955);
+ document.close();
+ document.write('<html><body></body></html>');
+ document.documentElement.appendChild(o590);
+ document.documentElement.appendChild(o254);
+ o280.controls^=1;
+ SpecialPowers.forceGC();
+ o317.insertAdjacentHTML('afterend','<iframe>');
+ document.documentElement.appendChild(o280);
+ o2695=document.implementation.createHTMLDocument();
+ o2695.body.appendChild(o254);
+ o53.onerror=f0;
+ document.documentElement.appendChild(o508);
+ o2803=frames[1].document;
+ o2803.getAnimations();
+}
+function f0() {
+ o2803.write('<html><body></body></html>');
+ SpecialPowers.forceCC();
+}
+</script>
+<body onload="start()"></body>
diff --git a/dom/animation/test/crashtests/1322291-1.html b/dom/animation/test/crashtests/1322291-1.html
new file mode 100644
index 0000000000..87def99ba8
--- /dev/null
+++ b/dom/animation/test/crashtests/1322291-1.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+document.addEventListener("DOMContentLoaded", boom);
+function boom(){
+ let o1 = (function(){
+ let e=document.createElement("frameset");
+ document.documentElement.appendChild(e);
+ return e;
+ })();
+ let a1 = o1.animate({ "transform": "rotate3d(22,73,26,374grad)" },
+ { duration: 10, delay: 100 });
+
+ // We need to wait the end of the delay to ensure that the animation is
+ // composited on the compositor, but there is no event for script animation
+ // that fires after the delay phase finished. So we wait for finished promise
+ // instead.
+ a1.finished.then(function() {
+ document.documentElement.className = "";
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1322291-2.html b/dom/animation/test/crashtests/1322291-2.html
new file mode 100644
index 0000000000..b93d53224d
--- /dev/null
+++ b/dom/animation/test/crashtests/1322291-2.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+}
+</style>
+<body>
+<div id=o_0></div>
+<script>
+function boom(){
+ var anim = o_0.animate([
+ {},
+ {"transform": "scale(2)"},
+ {"transform": "none"}
+ ], {
+ duration: 100,
+ iterationStart: 0.5,
+ });
+ // We need to wait for finished promise just like we do in 1322291-1.html.
+ anim.finished.then(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1322382-1.html b/dom/animation/test/crashtests/1322382-1.html
new file mode 100644
index 0000000000..6ca9c1b836
--- /dev/null
+++ b/dom/animation/test/crashtests/1322382-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<style>
+details {
+ background-color: blue;
+ width: 100px;
+ height: 100px;
+}
+</style>
+<html>
+<details id=o1><div></div></details>
+<script>
+window.onload = function(){
+ o1.animate([{'transform': 'none'}], 100);
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1323114-1.html b/dom/animation/test/crashtests/1323114-1.html
new file mode 100644
index 0000000000..344fd87db0
--- /dev/null
+++ b/dom/animation/test/crashtests/1323114-1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id=a />
+<script>
+addEventListener("DOMContentLoaded", function(){
+ a.animate([{"transform": "matrix3d(25,8788,-69,-24,-3,85,52,3,63,0,12,36810,-68,15,82,0) rotate(77rad)"}],
+ {fill: "both", iterationStart: 45, iterationComposite: "accumulate"});
+});
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1323114-2.html b/dom/animation/test/crashtests/1323114-2.html
new file mode 100644
index 0000000000..527d05effa
--- /dev/null
+++ b/dom/animation/test/crashtests/1323114-2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+ transform: rotate(45deg);
+}
+</style>
+<div id="div"></div>
+<script>
+addEventListener('DOMContentLoaded', function (){
+ var target = document.getElementById('div');
+ target.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
+ { transform: 'none' }],
+ 1000);
+});
+</script>
diff --git a/dom/animation/test/crashtests/1323119-1.html b/dom/animation/test/crashtests/1323119-1.html
new file mode 100644
index 0000000000..fd979822b8
--- /dev/null
+++ b/dom/animation/test/crashtests/1323119-1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body>
+<script>
+addEventListener("DOMContentLoaded", function() {
+ let a = document.createElement("th");
+ document.documentElement.appendChild(a);
+ a.animate([{"mask": "repeat-y "}], 484);
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1324554-1.html b/dom/animation/test/crashtests/1324554-1.html
new file mode 100644
index 0000000000..b3f9435a18
--- /dev/null
+++ b/dom/animation/test/crashtests/1324554-1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Bug 1324554 - missing final keyframes and zero-length segments together</title>
+ </head>
+ <script>
+ function go() {
+ var div = document.getElementById('target');
+ div.animate([ { "flex": "none" },
+ { "flex": "initial", offset: 0.5 },
+ { "flex": "0.0 ", offset: 0.5 },
+ {} ]);
+ }
+ </script>
+ <body onload="go()">
+ <div id='target' ></div>
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1325193-1.html b/dom/animation/test/crashtests/1325193-1.html
new file mode 100644
index 0000000000..bd0666497c
--- /dev/null
+++ b/dom/animation/test/crashtests/1325193-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+function boom(){
+ o_0.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"});
+ o_0.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439);
+ o_0.animate([{"font": "icon"}], 1849);
+ setTimeout(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ }, 500);
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+</head>
+<body><details id=o_0><q></q></details></body>
+</html>
diff --git a/dom/animation/test/crashtests/1330190-1.html b/dom/animation/test/crashtests/1330190-1.html
new file mode 100644
index 0000000000..fa14e0f741
--- /dev/null
+++ b/dom/animation/test/crashtests/1330190-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<span id=a />
+<script>
+addEventListener("DOMContentLoaded", function(){
+ a.animate([{"left": "38%"}], 100);
+ a.appendChild(document.createElement("div"));
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1330190-2.html b/dom/animation/test/crashtests/1330190-2.html
new file mode 100644
index 0000000000..57e5d31b28
--- /dev/null
+++ b/dom/animation/test/crashtests/1330190-2.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<style>
+@keyframes anim {
+}
+
+#o_0:before {
+ animation: anim 10s;
+ content: "";
+}
+</style>
+<meta charset="UTF-8">
+<script>
+function boom(){
+ function getPseudoElement() {
+ var anim = document.getAnimations()[0];
+ anim.cancel();
+ return anim.effect.target;
+ }
+
+ var target = getPseudoElement();
+ target.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"});
+ target.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439);
+ target.animate([{"font": "icon"}], 1849);
+ setTimeout(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ }, 500);
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+</head>
+<body>
+<div id=o_0></div>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1330513-1.html b/dom/animation/test/crashtests/1330513-1.html
new file mode 100644
index 0000000000..a497cc9e27
--- /dev/null
+++ b/dom/animation/test/crashtests/1330513-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<body id=a></body>
+<script>
+document.getElementById("a")
+ .animate([{"filter": "grayscale(28%)"}], {fill:"forwards", composite:"add"});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1332588-1.html b/dom/animation/test/crashtests/1332588-1.html
new file mode 100644
index 0000000000..11719a86ca
--- /dev/null
+++ b/dom/animation/test/crashtests/1332588-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<style>
+span {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+}
+</style>
+<script>
+window.onload = function() {
+ let body = document.getElementsByTagName("body")[0];
+ let o_0 = document.createElement("span");
+ body.appendChild(o_0);
+ o_0.animate([{ "padding": "57pt", "transform": "none" },
+ { "padding": "57pt", "transform": "rotate(90deg)" }] , 10000);
+ body.appendChild(document.createElement("colgroup"));
+ document.documentElement.classList.remove("reftest-wait");
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1333539-1.html b/dom/animation/test/crashtests/1333539-1.html
new file mode 100644
index 0000000000..c9111890b0
--- /dev/null
+++ b/dom/animation/test/crashtests/1333539-1.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let body = document.getElementsByTagName("body")[0];
+ let target = document.createElement("div");
+ let anim1 = new Animation();
+ let anim2 = new Animation();
+ let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000);
+ body.appendChild(target);
+ target.appendChild(document.createElement("meter"));
+ anim1.startTime = 88;
+ anim1.timeline = null;
+ anim1.pause();
+ anim1.effect = effect;
+ anim2.effect = effect;
+ anim1.effect = effect;
+
+ // anim1, since it doesn't have a timeline, will remain pause-pending,
+ // so just wait on anim2.
+ anim2.ready.then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1333539-2.html b/dom/animation/test/crashtests/1333539-2.html
new file mode 100644
index 0000000000..b00700eccb
--- /dev/null
+++ b/dom/animation/test/crashtests/1333539-2.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+}
+</style>
+<script>
+window.onload = function(){
+ let body = document.getElementsByTagName("body")[0];
+ let target = document.createElement("div");
+ let anim1 = new Animation();
+ let anim2 = new Animation();
+ let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000);
+ body.appendChild(target);
+ anim1.startTime = 88;
+ anim1.timeline = null;
+ anim1.pause();
+ anim1.effect = effect;
+ anim2.effect = effect;
+ anim1.effect = effect;
+ // Put another opacity animation on the top of the effect stack so that we
+ // try to send a lower priority animation that has no timeline to the
+ // compositor.
+ let anim3 = target.animate({ opacity : [ 1, 0 ] }, 1000);
+
+ Promise.all([anim1.ready, anim2.ready, anim2.ready]).then(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1334582-1.html b/dom/animation/test/crashtests/1334582-1.html
new file mode 100644
index 0000000000..d67ddc3c52
--- /dev/null
+++ b/dom/animation/test/crashtests/1334582-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate([], {"iterations": 1.7976931348623157e+308, "fill": "both"});
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1334582-2.html b/dom/animation/test/crashtests/1334582-2.html
new file mode 100644
index 0000000000..d3b223650d
--- /dev/null
+++ b/dom/animation/test/crashtests/1334582-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate([], {"iterationStart": 1.7976931348623157e+308, "fill": "both"});
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1334583-1.html b/dom/animation/test/crashtests/1334583-1.html
new file mode 100644
index 0000000000..b4d4109f0d
--- /dev/null
+++ b/dom/animation/test/crashtests/1334583-1.html
@@ -0,0 +1,9 @@
+<div style="width: 200px; height: 200px; background: purple" id=div>
+</div>
+<script>
+const animation = div.animate(
+ [ { transform: "scale(1)" },
+ { transform: "scale(2)" } ],
+ { iterations: Infinity, duration: 512 } );
+animation.currentTime = 2147483647;
+</script>
diff --git a/dom/animation/test/crashtests/1335998-1.html b/dom/animation/test/crashtests/1335998-1.html
new file mode 100644
index 0000000000..72353a9692
--- /dev/null
+++ b/dom/animation/test/crashtests/1335998-1.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1335998 - Handle {Interpolate, Accumulate}Matrix of mismatched transform lists
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ transform: rotate(45deg);
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var div = document.getElementById("target");
+ var animation = div.animate([ { transform: 'translateX(200px) scale(2.0)',
+ composite: 'accumulate' },
+ { transform: 'rotate(-45deg)' } ],
+ 2000);
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1343589-1.html b/dom/animation/test/crashtests/1343589-1.html
new file mode 100644
index 0000000000..a494b83da3
--- /dev/null
+++ b/dom/animation/test/crashtests/1343589-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate(null,
+ { duration: 100, iterations: Number.POSITIVE_INFINITY });
+ a.startTime = 100000; // Set the start time far in the future
+ // Try reversing (this should throw because the target effect end is infinity)
+ try { a.reverse(); } catch(e) {}
+ // Do something that will trigger a timing update
+ a.effect.target = document.createElement("span");
+ document.documentElement.className = '';
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1359658-1.html b/dom/animation/test/crashtests/1359658-1.html
new file mode 100644
index 0000000000..972ec497fa
--- /dev/null
+++ b/dom/animation/test/crashtests/1359658-1.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <meta charset=utf-8>
+ <title>Bug 1359658: Animation-only dirty descendants bit should be cleared
+ for display:none content</title>
+ </head>
+ <body>
+ <div id="ancestor">
+ <svg>
+ <rect id="target" width="100%" height="100%" fill="lime"/>
+ </svg>
+ </div>
+ </body>
+ <script>
+'use strict';
+
+const ancestor = document.getElementById('ancestor');
+const target = document.getElementById('target');
+
+document.addEventListener('DOMContentLoaded', () => {
+ const animation = target.animate({ color: [ 'red', 'lime' ] },
+ { duration: 1000, iterations: Infinity });
+ requestAnimationFrame(() => {
+ // Tweak animation to cause animation dirty bit to be set
+ animation.effect.updateTiming({ duration: 2000 });
+ ancestor.style.display = "none";
+ getComputedStyle(ancestor).display;
+ document.documentElement.className = '';
+ });
+});
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1373712-1.html b/dom/animation/test/crashtests/1373712-1.html
new file mode 100644
index 0000000000..8b5c121c85
--- /dev/null
+++ b/dom/animation/test/crashtests/1373712-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Bug 1373712 - Assertions of SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy) with large color value
+</title>
+<meta charset="UTF-8">
+<script>
+document.documentElement.animate([{ "color": "hsl(63e292,41%,34%)" }]);
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1379606-1.html b/dom/animation/test/crashtests/1379606-1.html
new file mode 100644
index 0000000000..89f756bf06
--- /dev/null
+++ b/dom/animation/test/crashtests/1379606-1.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<style>
+div {
+ display: none;
+}
+</style>
+<div>
+<div>
+<div>
+<div>
+<div>
+<div id="target">
+</div>
+</div>
+</div>
+</div>
+</div>
+</div>
+<script>
+ target.animate({ color: "red" })
+</script>
diff --git a/dom/animation/test/crashtests/1393605-1.html b/dom/animation/test/crashtests/1393605-1.html
new file mode 100644
index 0000000000..9f282e58ba
--- /dev/null
+++ b/dom/animation/test/crashtests/1393605-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <title>
+ Bug 1393605 - Still work if determinant of 2d matrix is not 1 or -1
+ </title>
+ </head>
+ <script>
+ document.documentElement.animate(
+ [ { 'transform': 'scale(4)' },
+ { 'transform': 'rotate(3grad) scaleX(0) ' +
+ 'translate(2mm) matrix(2,7,1,.32,7,0)' } ],
+ { fill: 'both' });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1400022-1.html b/dom/animation/test/crashtests/1400022-1.html
new file mode 100644
index 0000000000..8256091e1e
--- /dev/null
+++ b/dom/animation/test/crashtests/1400022-1.html
@@ -0,0 +1,10 @@
+<script>
+requestIdleCallback(function(){ location.reload() })
+a = document.createElement("x")
+document.documentElement.appendChild(a)
+b = document.createElement('link')
+b.setAttribute('rel', 'stylesheet')
+b.setAttribute('href', 'data:,*{border-block-start:solid}')
+document.head.appendChild(b)
+a.insertAdjacentHTML("afterBegin", "<d id='id0' style='transition-duration:1s'><svg filter='url(#id0)'>")
+</script>
diff --git a/dom/animation/test/crashtests/1401809.html b/dom/animation/test/crashtests/1401809.html
new file mode 100644
index 0000000000..7a3adcc60c
--- /dev/null
+++ b/dom/animation/test/crashtests/1401809.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <style></style>
+ <script>
+ o1 = document.createElement('t');
+ document.documentElement.appendChild(o1);
+ document.styleSheets[0].insertRule('* { will-change:an }', 0);
+ k = new KeyframeEffect(o1, [{'willChange':'s'}], {'':''});
+ k = null;
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ </script>
+ </head>
+</html>
diff --git a/dom/animation/test/crashtests/1411318-1.html b/dom/animation/test/crashtests/1411318-1.html
new file mode 100644
index 0000000000..5c8e7211c1
--- /dev/null
+++ b/dom/animation/test/crashtests/1411318-1.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <script>
+ o1 = (new DOMParser).parseFromString('', 'text/html');
+ o2 = document.createElement('canvas');
+ document.documentElement.appendChild(o2);
+ o3 = o2.animate([{'transform':'unset'}], {'delay':32});
+ o4 = o3.effect;
+ o5 = o1.createElement('d');
+ o6 = new Animation(o4, document.timeline);
+ o7 = o5.animate([], {});
+ o7.effect = o6.effect;
+ </script>
+ </head>
+</html>
diff --git a/dom/animation/test/crashtests/1467277-1.html b/dom/animation/test/crashtests/1467277-1.html
new file mode 100644
index 0000000000..c58fc64493
--- /dev/null
+++ b/dom/animation/test/crashtests/1467277-1.html
@@ -0,0 +1,6 @@
+<script>
+addEventListener("DOMContentLoaded", () => {
+ document.documentElement.animate(
+ [ { "transform": "rotate3d(1e58, 2, 6, 0turn)" } ], 1000)
+})
+</script>
diff --git a/dom/animation/test/crashtests/1468294-1.html b/dom/animation/test/crashtests/1468294-1.html
new file mode 100644
index 0000000000..e4092046ac
--- /dev/null
+++ b/dom/animation/test/crashtests/1468294-1.html
@@ -0,0 +1,7 @@
+<script>
+addEventListener("DOMContentLoaded", () => {
+ document.documentElement.animate([{ "transform": "matrix(2,1,1,5,2,8)" }],
+ { duration: 1000,
+ easing: "cubic-bezier(1,-15,.6,4)" });
+})
+</script>
diff --git a/dom/animation/test/crashtests/1524480-1.html b/dom/animation/test/crashtests/1524480-1.html
new file mode 100644
index 0000000000..89e5a412d9
--- /dev/null
+++ b/dom/animation/test/crashtests/1524480-1.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html class="reftest-wait">
+<meta charset=utf-8>
+<style>
+div {
+ display: none;
+ width: 100px;
+ height: 100px;
+ background: blue;
+}
+</style>
+<div id=div></div>
+<script>
+async function test() {
+ const animation = div.animate({ transform: ['none', 'none'] }, 1000);
+ animation.cancel();
+
+ await waitForFrame();
+
+ div.style.display = 'block';
+
+ await waitForFrame();
+ await waitForFrame();
+
+ animation.play();
+ await animation.finished;
+
+ document.documentElement.className = "";
+}
+
+function waitForFrame() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+test();
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1575926.html b/dom/animation/test/crashtests/1575926.html
new file mode 100644
index 0000000000..cc37c94235
--- /dev/null
+++ b/dom/animation/test/crashtests/1575926.html
@@ -0,0 +1,24 @@
+<style>
+ @keyframes animation_0 {
+ 88% { }
+ }
+
+ * {
+ animation-name: animation_0;
+ animation-delay: 4s;
+ }
+</style>
+<script>
+ function start () {
+ const input = document.createElement('input')
+ document.documentElement.appendChild(input)
+ const animations = input.getAnimations({})
+ const animation = animations[(3782796448 % animations.length)]
+ const effect = animation.effect
+ effect.setKeyframes({ 'borderLeft': ['inherit'] })
+ effect.target = null
+ input.contentEditable = 'true'
+ }
+
+ document.addEventListener('DOMContentLoaded', start)
+</script>
diff --git a/dom/animation/test/crashtests/1585770.html b/dom/animation/test/crashtests/1585770.html
new file mode 100644
index 0000000000..018d688582
--- /dev/null
+++ b/dom/animation/test/crashtests/1585770.html
@@ -0,0 +1,22 @@
+<html class="reftest-wait">
+<script>
+function start () {
+ const kf_effect =
+ new KeyframeEffect(document.documentElement,
+ { opacity: ['', '1'] },
+ { easing: 'step-end',
+ duration: 10000 } );
+ const copy = new KeyframeEffect(kf_effect);
+ const animation = new Animation(copy);
+
+ animation.reverse();
+ document.documentElement.getBoundingClientRect();
+
+ requestAnimationFrame(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+}
+
+document.addEventListener('DOMContentLoaded', start);
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1604500-1.html b/dom/animation/test/crashtests/1604500-1.html
new file mode 100644
index 0000000000..01a6eafd1f
--- /dev/null
+++ b/dom/animation/test/crashtests/1604500-1.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<script>
+function start () {
+ const keyframe = new KeyframeEffect(undefined, {});
+ const animation = new Animation(keyframe, undefined);
+ // Make animation run backwards...
+ animation.playbackRate = -100;
+ // But then set the current time to the future so it becomes "current"...
+ animation.currentTime = 2055;
+ // After updating the playback rate to zero, however, it should no longer
+ // be "current" (and this takes effect immediately because |animation| is
+ // paused)...
+ animation.updatePlaybackRate(0);
+ // Now update the target and hope nothing goes wrong...
+ keyframe.target = div;
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+<div id=div></div>
+</html>
diff --git a/dom/animation/test/crashtests/1611847.html b/dom/animation/test/crashtests/1611847.html
new file mode 100644
index 0000000000..720ce1179b
--- /dev/null
+++ b/dom/animation/test/crashtests/1611847.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <style>
+ * {
+ transition-duration: 2s;
+ }
+ </style>
+ <script>
+ function start () {
+ const element = document.createElementNS('', 's');
+ const effect = new KeyframeEffect(document.documentElement, {}, 196);
+ document.documentElement.setAttribute('style', 'padding-left:3');
+ effect.updateTiming({ 'delay': 2723 });
+ const animations = document.getAnimations();
+ animations[0].effect = effect;
+ animations[0].updatePlaybackRate(-129);
+ effect.target = element;
+ }
+
+ document.addEventListener('DOMContentLoaded', start);
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-1.html b/dom/animation/test/crashtests/1612891-1.html
new file mode 100644
index 0000000000..44cf022e88
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-1.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ function start() {
+ const element = document.createElement('img')
+ element.animate([
+ { 'easing': '' },
+ { 'offset': 'o' },
+ ], {})
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-2.html b/dom/animation/test/crashtests/1612891-2.html
new file mode 100644
index 0000000000..a9779ba70f
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-2.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ function start() {
+ const element = document.createElement('img')
+ element.animate([
+ { 'easing': '' },
+ 123,
+ ], {})
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-3.html b/dom/animation/test/crashtests/1612891-3.html
new file mode 100644
index 0000000000..89e71b6ca8
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-3.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <script>
+ document.addEventListener('DOMContentLoaded', () => {
+ const keyframe = new KeyframeEffect(document.documentElement, [{}], {})
+ keyframe.setKeyframes([{ 'easing': '' }, { 'offset': 'o', }])
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1633442.html b/dom/animation/test/crashtests/1633442.html
new file mode 100644
index 0000000000..cb4beedebc
--- /dev/null
+++ b/dom/animation/test/crashtests/1633442.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<script>
+ document.addEventListener('DOMContentLoaded', () => {
+ document.documentElement.style.setProperty('transition-duration', '3s', '')
+ document.documentElement.style.setProperty('rotate', '2deg', undefined)
+ document.documentElement.style.setProperty('border-radius', '2%', '')
+ const [anim_1, anim_0] = document.documentElement.getAnimations({})
+ anim_1.effect = anim_0.effect
+ document.documentElement.classList.remove("reftest-wait");
+ })
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1633486.html b/dom/animation/test/crashtests/1633486.html
new file mode 100644
index 0000000000..20b88f6327
--- /dev/null
+++ b/dom/animation/test/crashtests/1633486.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', async () => {
+ const element = document.getElementById('target');
+ element.animate({
+ 'all': ['initial']
+ }, {
+ 'duration': 500,
+ 'pseudoElement': '::marker',
+ })
+ element.hidden = false;
+ })
+ </script>
+</head>
+<ul>
+ <li id='target' hidden></li>
+</ul>
+</html>
diff --git a/dom/animation/test/crashtests/1656419.html b/dom/animation/test/crashtests/1656419.html
new file mode 100644
index 0000000000..4e76cb0a55
--- /dev/null
+++ b/dom/animation/test/crashtests/1656419.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+#target {
+ width: 200vw;
+ height: 200vh;
+}
+</style>
+<div id="target"></div>
+<script>
+const animA = target.animate(
+ { transform: 'translateX(100px)' },
+ { duration: 50 }
+);
+const animB = target.animate(
+ { transform: 'translateX(100px)', composite: 'add' },
+ { duration: 100 }
+);
+animB.finished.then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1699890.html b/dom/animation/test/crashtests/1699890.html
new file mode 100644
index 0000000000..95aa1e190c
--- /dev/null
+++ b/dom/animation/test/crashtests/1699890.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<style>
+@keyframes anim {
+ from { background-color: rgba(0, 0, 0, 0); }
+ to { background-color: rgba(255, 0, 0, 255); }
+}
+body {
+ animation: anim 100s;
+ width: 100vw;
+ height: 100vh;
+}
+</style>
diff --git a/dom/animation/test/crashtests/1706157.html b/dom/animation/test/crashtests/1706157.html
new file mode 100644
index 0000000000..cfa7b70b56
--- /dev/null
+++ b/dom/animation/test/crashtests/1706157.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ @keyframes animation {
+ to {
+ left: 100px;
+ }
+ }
+ * {
+ animation: animation linear 1s;
+ }
+ #target {
+ animation-timing-function: steps(2965566999, jump-both);
+ }
+ </style>
+</head>
+<div id="target"></div>
+</html>
diff --git a/dom/animation/test/crashtests/1714421.html b/dom/animation/test/crashtests/1714421.html
new file mode 100644
index 0000000000..04099b55b7
--- /dev/null
+++ b/dom/animation/test/crashtests/1714421.html
@@ -0,0 +1,8 @@
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ document.documentElement.style.setProperty('scale', '89%', undefined)
+ document.documentElement.style.setProperty('-moz-transition-duration', '2009216159ms', '')
+ document.getAnimations()[0].timeline = undefined
+ document.documentElement.animate({'scale': ['none', 'none', 'none']}, 1807)
+})
+</script>
diff --git a/dom/animation/test/crashtests/1807966.html b/dom/animation/test/crashtests/1807966.html
new file mode 100644
index 0000000000..d2759d2b5c
--- /dev/null
+++ b/dom/animation/test/crashtests/1807966.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<style>
+ #target {
+ transition-timing-function: linear(calc(15 / 0), 10 10%);
+ transition-duration: 10s;
+ }
+</style>
+<script>
+ window.addEventListener('load', () => {
+ target.style.paddingBlock = "10px 10px";
+ })
+</script>
+<slot id="target"></slot>
diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..6f5dd140e7
--- /dev/null
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -0,0 +1,61 @@
+load 1134538.html
+pref(dom.animations-api.core.enabled,true) load 1239889-1.html
+load 1244595-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-2.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-3.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-4.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-5.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1216842-6.html
+load 1272475-1.html
+load 1272475-2.html
+load 1278485-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1277272-1.html
+load 1282691-1.html
+pref(dom.animations-api.core.enabled,true) load 1291413-1.html
+pref(dom.animations-api.core.enabled,true) load 1291413-2.html
+pref(dom.animations-api.compositing.enabled,true) load 1304886-1.html
+pref(dom.animations-api.getAnimations.enabled,true) load 1309198-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1322382-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) load 1322291-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) load 1322291-2.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) pref(dom.animations-api.compositing.enabled,true) load 1323114-1.html
+pref(dom.animations-api.compositing.enabled,true) load 1323114-2.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1323119-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1324554-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) pref(dom.animations-api.compositing.enabled,true) load 1325193-1.html
+load 1332588-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1330190-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) pref(dom.animations-api.compositing.enabled,true) pref(dom.animations-api.getAnimations.enabled,true) load 1330190-2.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) pref(dom.animations-api.compositing.enabled,true) load 1330513-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1333539-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1333539-2.html
+load 1334582-1.html
+load 1334582-2.html
+load 1334583-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.compositing.enabled,true) load 1335998-1.html
+pref(dom.animations-api.core.enabled,true) load 1343589-1.html
+pref(dom.animations-api.core.enabled,true) load 1359658-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1373712-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1379606-1.html
+load 1393605-1.html
+load 1400022-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) load 1401809.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.timelines.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) load 1411318-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1468294-1.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1467277-1.html
+load 1524480-1.html
+load 1575926.html
+pref(dom.animations-api.implicit-keyframes.enabled,true) load 1585770.html
+load 1604500-1.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.getAnimations.enabled,true) load 1611847.html
+pref(dom.animations-api.core.enabled,true) load 1612891-1.html
+pref(dom.animations-api.core.enabled,true) load 1612891-2.html
+pref(dom.animations-api.core.enabled,true) load 1612891-3.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.getAnimations.enabled,true) load 1633442.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.implicit-keyframes.enabled,true) load 1633486.html
+pref(layout.animation.prerender.partial,true) load 1656419.html
+pref(layout.css.step-position-jump.enabled,true) load 1706157.html
+pref(gfx.omta.background-color,true) load 1699890.html
+pref(dom.animations-api.core.enabled,true) pref(dom.animations-api.getAnimations.enabled,true) pref(dom.animations-api.timelines.enabled,true) load 1714421.html
+pref(layout.css.linear-easing-function.enabled,true) load 1807966.html
diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html
new file mode 100644
index 0000000000..92a709e2f6
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_document-timeline.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Web Animations API: DocumentTimeline tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<iframe srcdoc='<html><meta charset=utf-8></html>' width="10" height="10" id="iframe"></iframe>
+<iframe srcdoc='<html style="display:none"><meta charset=utf-8></html>' width="10" height="10" id="hidden-iframe"></iframe>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ assert_equals(document.timeline, document.timeline,
+ 'document.timeline returns the same object every time');
+ var iframe = document.getElementById('iframe');
+ assert_not_equals(document.timeline, iframe.contentDocument.timeline,
+ 'document.timeline returns a different object for each document');
+ assert_not_equals(iframe.contentDocument.timeline, null,
+ 'document.timeline on an iframe is not null');
+},
+'document.timeline identity tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline',
+ assert: [ 'Each document has a timeline called the document timeline' ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+
+ if (AppConstants.platform == "android") {
+ // Skip this test case on Android since it frequently fails on the
+ // environments. See bug 1761900.
+ t.done();
+ }
+
+ assert_greater_than_equal(document.timeline.currentTime, 0,
+ 'document.timeline.currentTime is positive or zero');
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_true(document.readyState !== 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(t.step_func(function(rafTime) {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'document.timeline.currentTime matches' +
+ ' requestAnimationFrame time');
+ t.done();
+ }));
+},
+'document.timeline.currentTime value tests',
+{
+ help: [
+ 'http://dev.w3.org/fxtf/web-animations/#the-global-clock',
+ 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline'
+ ],
+ assert: [
+ 'The global clock is a source of monotonically increasing time values',
+ 'The time values of the document timeline are calculated as a fixed' +
+ ' offset from the global clock',
+ 'the zero time corresponds to the navigationStart moment',
+ 'the time value of each document timeline must be equal to the time ' +
+ 'passed to animation frame request callbacks for that browsing context'
+ ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ var valueAtStart = document.timeline.currentTime;
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {
+ // Wait 100ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'document.timeline.currentTime does not change within a script block');
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(document.timeline.currentTime > valueAtStart,
+ 'document.timeline.currentTime increases between script blocks');
+ t.done();
+ }));
+},
+'document.timeline.currentTime liveness tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#script-execution-and-live-updates-to-the-model',
+ assert: [ 'The value returned by the currentTime attribute of a' +
+ ' document timeline will not change within a script block' ],
+ author: 'Brian Birtles'
+});
+
+test(function() {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+ assert_equals(typeof hiddenIFrame.contentDocument.timeline.currentTime,
+ 'number',
+ 'currentTime of an initially hidden subframe\'s timeline is a number');
+ assert_true(hiddenIFrame.contentDocument.timeline.currentTime >= 0,
+ 'currentTime of an initially hidden subframe\'s timeline is >= 0');
+}, 'document.timeline.currentTime hidden subframe test');
+
+async_test(function(t) {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+
+ // Don't run the test until after the iframe has completed loading or else the
+ // contentDocument may change.
+ var testToRunOnLoad = t.step_func(function() {
+ // Remove display:none
+ hiddenIFrame.style.display = 'block';
+ getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_greater_than(hiddenIFrame.contentDocument.timeline.currentTime, 0,
+ 'document.timeline.currentTime is positive after removing'
+ + ' display:none');
+ var previousValue = hiddenIFrame.contentDocument.timeline.currentTime;
+
+ // Re-introduce display:none
+ hiddenIFrame.style.display = 'none';
+ getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(
+ hiddenIFrame.contentDocument.timeline.currentTime >= previousValue,
+ 'document.timeline.currentTime does not go backwards after'
+ + ' re-setting display:none');
+ t.done();
+ }));
+ }));
+ });
+
+ if (hiddenIFrame.contentDocument.readyState === 'complete') {
+ testToRunOnLoad();
+ } else {
+ hiddenIFrame.addEventListener("load", testToRunOnLoad);
+ }
+}, 'document.timeline.currentTime hidden subframe dynamic test');
+
+</script>
diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html
new file mode 100644
index 0000000000..3da4e4deb2
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_request_animation_frame.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test RequestAnimationFrame Timestamps are monotonically increasing</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ var lastRequestAnimationFrameTimestamp = 0;
+ var requestAnimationFrameCount = 20;
+ var currentCount = 0;
+
+ // Test that all timestamps are always increasing
+ // and do not ever go backwards
+ function rafCallback(aTimestamp) {
+ SimpleTest.ok(aTimestamp > lastRequestAnimationFrameTimestamp,
+ "New RequestAnimationFrame timestamp should be later than the previous RequestAnimationFrame timestamp");
+ lastRequestAnimationFrameTimestamp = aTimestamp;
+ if (currentCount == requestAnimationFrameCount) {
+ SimpleTest.finish();
+ } else {
+ currentCount++;
+ window.requestAnimationFrame(rafCallback);
+ }
+ }
+
+ window.requestAnimationFrame(rafCallback);
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
new file mode 100644
index 0000000000..bd4bfe8a61
--- /dev/null
+++ b/dom/animation/test/mochitest.ini
@@ -0,0 +1,81 @@
+[DEFAULT]
+prefs =
+ dom.animations-api.compositing.enabled=true
+ dom.animations-api.core.enabled=true
+ dom.animations-api.getAnimations.enabled=true
+ dom.animations-api.implicit-keyframes.enabled=true
+ dom.animations-api.timelines.enabled=true
+ gfx.omta.background-color=true
+ layout.css.motion-path.enabled=true
+ layout.css.individual-transform.enabled=true
+ layout.css.scroll-driven-animations.enabled=true
+ gfx.font_loader.delay=0
+# Support files for chrome tests that we want to load over HTTP need
+# to go in here, not chrome.ini.
+support-files =
+ chrome/file_animate_xrays.html
+ mozilla/xhr_doc.html
+ mozilla/file_deferred_start.html
+ mozilla/file_disable_animations_api_autoremove.html
+ mozilla/file_disable_animations_api_compositing.html
+ mozilla/file_disable_animations_api_get_animations.html
+ mozilla/file_disable_animations_api_implicit_keyframes.html
+ mozilla/file_disable_animations_api_timelines.html
+ mozilla/file_discrete_animations.html
+ mozilla/file_transition_finish_on_compositor.html
+ ../../../layout/style/test/property_database.js
+ testcommon.js
+ !/dom/events/test/event_leak_utils.js
+
+[document-timeline/test_document-timeline.html]
+[document-timeline/test_request_animation_frame.html]
+[mozilla/test_cascade.html]
+[mozilla/test_cubic_bezier_limits.html]
+[mozilla/test_deferred_start.html]
+skip-if = (os == 'win' && bits == 64) # Bug 1363957
+[mozilla/test_disable_animations_api_autoremove.html]
+[mozilla/test_disable_animations_api_compositing.html]
+[mozilla/test_disable_animations_api_get_animations.html]
+[mozilla/test_disable_animations_api_implicit_keyframes.html]
+[mozilla/test_disable_animations_api_timelines.html]
+[mozilla/test_disabled_properties.html]
+[mozilla/test_discrete_animations.html]
+[mozilla/test_distance_of_basic_shape.html]
+[mozilla/test_distance_of_filter.html]
+[mozilla/test_distance_of_path_function.html]
+[mozilla/test_distance_of_transform.html]
+[mozilla/test_document_timeline_origin_time_range.html]
+[mozilla/test_get_animations_on_scroll_animations.html]
+[mozilla/test_hide_and_show.html]
+[mozilla/test_mainthread_synchronization_pref.html]
+[mozilla/test_moz_prefixed_properties.html]
+[mozilla/test_pending_animation_tracker.html]
+[mozilla/test_restyles.html]
+support-files =
+ mozilla/file_restyles.html
+ mozilla/empty.html
+skip-if =
+ (os == 'android' && debug) #Bug 1784931
+ (os == 'linux' && tsan) #Bug 1784931
+ http3
+[mozilla/test_restyling_xhr_doc.html]
+[mozilla/test_set_easing.html]
+[mozilla/test_style_after_finished_on_compositor.html]
+[mozilla/test_transform_limits.html]
+[mozilla/test_transition_finish_on_compositor.html]
+skip-if = toolkit == 'android'
+[mozilla/test_underlying_discrete_value.html]
+[mozilla/test_unstyled.html]
+[mozilla/test_event_listener_leaks.html]
+skip-if = (os == "win" && processor == "aarch64") #bug 1535784
+[style/test_animation-seeking-with-current-time.html]
+[style/test_animation-seeking-with-start-time.html]
+[style/test_animation-setting-effect.html]
+[style/test_composite.html]
+skip-if = xorigin
+[style/test_interpolation-from-interpolatematrix-to-none.html]
+[style/test_missing-keyframe.html]
+[style/test_missing-keyframe-on-compositor.html]
+skip-if =
+ fission && xorigin # Bug 1716403 - New fission platform triage
+[style/test_transform-non-normalizable-rotate3d.html]
diff --git a/dom/animation/test/mozilla/empty.html b/dom/animation/test/mozilla/empty.html
new file mode 100644
index 0000000000..739422cbfa
--- /dev/null
+++ b/dom/animation/test/mozilla/empty.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="../testcommon.js"></script>
diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html
new file mode 100644
index 0000000000..863fc80fec
--- /dev/null
+++ b/dom/animation/test/mozilla/file_deferred_start.html
@@ -0,0 +1,179 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+@keyframes empty { }
+.target {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForDocLoad() {
+ return new Promise((resolve, reject) => {
+ if (document.readyState === 'complete') {
+ resolve();
+ } else {
+ window.addEventListener('load', resolve);
+ }
+ });
+}
+
+function waitForPaints() {
+ return new Promise((resolve, reject) => {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(async t => {
+ // Test that empty animations actually start.
+ //
+ // Normally we tie the start of animations to when their first frame of
+ // the animation is rendered. However, for animations that don't actually
+ // trigger a paint (e.g. because they are empty, or are animating something
+ // that doesn't render or is offscreen) we want to make sure they still
+ // start.
+ //
+ // Before we start, wait for the document to finish loading, then create
+ // div element, and wait for painting. This is because during loading we will
+ // have other paint events taking place which might, by luck, happen to
+ // trigger animations that otherwise would not have been triggered, leading to
+ // false positives.
+ //
+ // As a result, it's better to wait until we have a more stable state before
+ // continuing.
+ await waitForDocLoad();
+
+ const div = addDiv(t);
+
+ await waitForPaints();
+
+ div.style.animation = 'empty 1000s';
+ const animation = div.getAnimations()[0];
+
+ let promiseCallbackDone = false;
+ animation.ready.then(() => {
+ promiseCallbackDone = true;
+ }).catch(() => {
+ assert_unreached('ready promise was rejected');
+ });
+
+ // We need to wait for up to three frames. This is because in some
+ // cases it can take up to two frames for the initial layout
+ // to take place. Even after that happens we don't actually resolve the
+ // ready promise until the following tick.
+ await waitForAnimationFrames(3);
+
+ assert_true(promiseCallbackDone,
+ 'ready promise for an empty animation was resolved'
+ + ' within three animation frames');
+}, 'Animation.ready is resolved for an empty animation');
+
+// Test that compositor animations with delays get synced correctly
+//
+// NOTE: It is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(async t => {
+ assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes,
+ 'Test should run without the refresh driver being under'
+ + ' test control');
+
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ const div = addDiv(t, { class: 'target' });
+
+ // As with the above test, any stray paints can cause this test to produce
+ // a false negative (that is, pass when it should fail). To avoid this we
+ // wait for paints and only then do we commence the test.
+ await waitForPaints();
+
+ const animation =
+ div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+ { duration: 400 * MS_PER_SEC,
+ delay: -200 * MS_PER_SEC });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForPaints();
+
+ const transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ const translateX = getTranslateXFromTransform(transformStr);
+
+ // If the delay has been applied we should be about half-way through
+ // the animation. However, if we applied it twice we will be at the
+ // end of the animation already so check that we are roughly half way
+ // through.
+ assert_between_inclusive(translateX, 40, 75,
+ 'Animation is about half-way through on the compositor');
+}, 'Starting an animation with a delay starts from the correct point');
+
+// Test that compositor animations with a playback rate start at the
+// appropriate point.
+//
+// NOTE: As with the previous test, it is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(async t => {
+ assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes,
+ 'Test should run without the refresh driver being under'
+ + ' test control');
+
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ const div = addDiv(t, { class: 'target' });
+
+ // Wait for the document to load and painting (see notes in previous test).
+ await waitForPaints();
+
+ const animation =
+ div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+ 200 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.playbackRate = 0.1;
+
+ await waitForPaints();
+
+ const transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ const translateX = getTranslateXFromTransform(transformStr);
+
+ // We pass the playback rate to the compositor independently and we have
+ // tests to ensure that it is correctly applied there. However, if, when
+ // we resolve the start time of the pending animation, we fail to
+ // incorporate the playback rate, we will end up starting from the wrong
+ // point and the current time calculated on the compositor will be wrong.
+ assert_between_inclusive(translateX, 25, 75,
+ 'Animation is about half-way through on the compositor');
+}, 'Starting an animation with a playbackRate starts from the correct point');
+
+function getTranslateXFromTransform(transformStr) {
+ const matrixComponents =
+ transformStr.startsWith('matrix(')
+ ? transformStr.substring('matrix('.length, transformStr.length-1)
+ .split(',')
+ .map(component => Number(component))
+ : [];
+ assert_equals(matrixComponents.length, 6,
+ 'Got a valid transform matrix on the compositor'
+ + ' (got: "' + transformStr + '")');
+
+ return matrixComponents[4];
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html
new file mode 100644
index 0000000000..79cb508467
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = addDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ // This should be assert_not_own_property but our local copy of testharness.js
+ // is old.
+ assert_equals(
+ animA.replaceState,
+ undefined,
+ 'Should not have a replaceState member'
+ );
+
+ animA.addEventListener(
+ 'remove',
+ t.step_func(() => {
+ assert_unreached('Should not fire a remove event');
+ })
+ );
+
+ // Allow a chance for the remove event to be fired
+
+ await animA.finished;
+ await waitForNextFrame();
+}, 'Remove events should not be fired if the pref is not set');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3, composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ assert_approx_equals(
+ parseFloat(getComputedStyle(div).opacity),
+ 0.5,
+ 0.0001,
+ 'Covered animation should still contribute to effect stack when adding'
+ );
+
+ animB.cancel();
+
+ assert_approx_equals(
+ parseFloat(getComputedStyle(div).opacity),
+ 0.2,
+ 0.0001,
+ 'Covered animation should still contribute to animated style when replacing'
+ );
+}, 'Covered animations should still affect style if the pref is not set');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_compositing.html b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html
new file mode 100644
index 0000000000..6d9ba35dc0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html
@@ -0,0 +1,137 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ const anim = addDiv(t).animate(
+ { marginLeft: ['0px', '10px'] },
+ {
+ duration: 100 * MS_PER_SEC,
+ iterations: 10,
+ iterationComposite: 'accumulate',
+ composite: 'add',
+ }
+ );
+ assert_false(
+ 'iterationComposite' in anim.effect,
+ 'The KeyframeEffect.iterationComposite member is not present'
+ );
+ assert_false(
+ 'composite' in anim.effect,
+ 'The KeyframeEffect.composite member is not present'
+ );
+}, 'The iterationComposite and composite members are not present on Animation'
+ + ' when the compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim = div.animate(
+ { marginLeft: ['0px', '10px'] },
+ {
+ duration: 100 * MS_PER_SEC,
+ iterations: 10,
+ iterationComposite: 'accumulate',
+ }
+ );
+ anim.pause();
+ anim.currentTime = 200 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '0px',
+ 'Animated style should NOT accumulate'
+ );
+}, 'KeyframeEffectOptions.iterationComposite should be ignored if the'
+ + ' compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ { duration: 100 * MS_PER_SEC }
+ );
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ { duration: 100 * MS_PER_SEC, composite: 'add' }
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'KeyframeEffectOptions.composite should be ignored if the'
+ + ' compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate({ marginLeft: ['0px', '100px'] }, 100 * MS_PER_SEC);
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ [
+ { marginLeft: '0px', composite: 'add' },
+ { marginLeft: '100px', composite: 'add' },
+ ],
+ 100 * MS_PER_SEC
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'composite member is ignored on keyframes when using array notation');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ 100 * MS_PER_SEC
+ );
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ { marginLeft: ['0px', '100px'], composite: ['add', 'add'] },
+ 100 * MS_PER_SEC
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'composite member is ignored on keyframes when using object notation');
+
+test(t => {
+ const anim = addDiv(t).animate(
+ { marginLeft: ['0px', '10px'] },
+ 100 * MS_PER_SEC
+ );
+
+ for (let frame of anim.effect.getKeyframes()) {
+ assert_false(
+ 'composite' in frame,
+ 'The BaseComputedKeyframe.composite member is not present'
+ );
+ }
+}, 'composite member is hidden from the result of ' +
+ 'KeyframeEffect::getKeyframes()');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html
new file mode 100644
index 0000000000..3d484444a7
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ assert_false('getAnimations' in addDiv(t));
+}, 'Element.getAnimations() is not available when getAnimations pref is'
+ + ' disabled');
+
+test(t => {
+ assert_false('getAnimations' in document);
+}, 'Document.getAnimations() is not available when getAnimations pref is'
+ + ' disabled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html
new file mode 100644
index 0000000000..9cd05e7d40
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// Tests for cases we should throw an exception for if implicit keyframes are
+// disabled.
+var gTests = [
+ { desc: "single Keyframe value",
+ keyframes: { left: "100px" } },
+ { desc: "single Keyframe with no offset",
+ keyframes: [{ left: "100px" }] },
+ { desc: "single Keyframe with 0% offset",
+ keyframes: [{ left: "100px", offset: 0 }] },
+ { desc: "single Keyframe with 100% offset",
+ keyframes: [{ left: "100px", offset: 1 }] },
+ { desc: "multiple Keyframes with missing 0% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.25 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 1.00 }] },
+ { desc: "multiple Keyframes with missing 100% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.00 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 0.75 }] },
+ { desc: "multiple Keyframes with missing properties on first Keyframe",
+ keyframes: [{ left: "100px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", top: "300px", offset: 1.0 }] },
+ { desc: "multiple Keyframes with missing properties on last Keyframe",
+ keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", offset: 1.0 }] },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ assert_throws("NotSupportedError", function() {
+ div.animate(subtest.keyframes, 100 * MS_PER_SEC);
+ });
+ }, "Element.animate() throws with " + subtest.desc);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_timelines.html b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html
new file mode 100644
index 0000000000..39fedb299a
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ assert_false(
+ window.hasOwnProperty('DocumentTimeline'),
+ 'DocumentTimeline should not be exposed on the global'
+ );
+ assert_false(
+ window.hasOwnProperty('AnimationTimeline'),
+ 'AnimationTimeline should not be exposed on the global'
+ );
+ assert_false(
+ 'timeline' in document,
+ 'document should not have a timeline property'
+ );
+
+ const anim = addDiv(t).animate(null);
+ assert_false(
+ 'timeline' in anim,
+ 'Animation should not have a timeline property'
+ );
+}, 'Timeline-related interfaces and members are disabled');
+
+done();
+</script>
diff --git a/dom/animation/test/mozilla/file_discrete_animations.html b/dom/animation/test/mozilla/file_discrete_animations.html
new file mode 100644
index 0000000000..e0de609bc5
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete_animations.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test Mozilla-specific discrete animatable properties</title>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+const gMozillaSpecificProperties = {
+ "-moz-box-align": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-align
+ from: "center",
+ to: "stretch"
+ },
+ "-moz-box-direction": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-direction
+ from: "reverse",
+ to: "normal"
+ },
+ "-moz-box-ordinal-group": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
+ from: "1",
+ to: "5"
+ },
+ "-moz-box-orient": {
+ // https://www.w3.org/TR/css-flexbox-1/
+ from: "horizontal",
+ to: "vertical"
+ },
+ "-moz-box-pack": {
+ // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
+ from: "center",
+ to: "end"
+ },
+ "-moz-float-edge": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
+ from: "margin-box",
+ to: "content-box"
+ },
+ "-moz-force-broken-image-icon": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
+ from: "1",
+ to: "0"
+ },
+ "-moz-text-size-adjust": {
+ // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
+ from: "none",
+ to: "auto"
+ },
+ "-webkit-text-stroke-width": {
+ // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
+ from: "10px",
+ to: "50px"
+ }
+}
+
+for (let property in gMozillaSpecificProperties) {
+ const testData = gMozillaSpecificProperties[property];
+ const from = testData.from;
+ const to = testData.to;
+ const idlName = propertyToIDL(property);
+ const keyframes = {};
+ keyframes[idlName] = [from, to];
+
+ test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 499, expected: from.toLowerCase() },
+ { time: 500, expected: to.toLowerCase() },
+ { time: 1000, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with linear easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68,0,1,0.01)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with effect easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with keyframe easing");
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ testSamples.forEach(testSample => {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+ "The value should be " + testSample.expected +
+ " at " + testSample.time + "ms");
+ });
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_restyles.html b/dom/animation/test/mozilla/file_restyles.html
new file mode 100644
index 0000000000..8d72cb6c44
--- /dev/null
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -0,0 +1,2275 @@
+<!doctype html>
+<head>
+<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script>
+const ok = opener.ok.bind(opener);
+const is = opener.is.bind(opener);
+const todo = opener.todo.bind(opener);
+const todo_is = opener.todo_is.bind(opener);
+const info = opener.info.bind(opener);
+const original_finish = opener.SimpleTest.finish;
+const SimpleTest = opener.SimpleTest;
+const add_task = opener.add_task;
+SimpleTest.finish = function finish() {
+ self.close();
+ original_finish();
+}
+</script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<style>
+@keyframes background-position {
+ 0% {
+ background-position: -25px center;
+ }
+
+ 40%,
+ 100% {
+ background-position: 36px center;
+ }
+}
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes opacity-without-end-value {
+ from { opacity: 0; }
+}
+@keyframes on-main-thread {
+ from { z-index: 0; }
+ to { z-index: 999; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes move-in {
+ from { transform: translate(120%, 120%); }
+ to { transform: translate(0%, 0%); }
+}
+@keyframes background-color {
+ from { background-color: rgb(255, 0, 0,); }
+ to { background-color: rgb(0, 255, 0,); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+progress:not(.stop)::-moz-progress-bar {
+ animation: on-main-thread 100s;
+}
+body {
+ /*
+ * set overflow:hidden to avoid accidentally unthrottling animations to update
+ * the overflow region.
+ */
+ overflow: hidden;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+// Returns observed animation restyle markers when |funcToMakeRestyleHappen|
+// is called.
+// NOTE: This function is synchronous version of the above observeStyling().
+// Unlike the above observeStyling, this function takes a callback function,
+// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous
+// restyles, and returns any restyle markers produced by calling that function.
+function observeAnimSyncStyling(funcToMakeRestyleHappen) {
+ const docShell = getDocShellForObservingRestylesForWindow(window);
+
+ funcToMakeRestyleHappen();
+
+ const markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ return Array.prototype.filter.call(markers, (marker, index) => {
+ return marker.name == 'Styles' && marker.isAnimationOnly;
+ });
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(resolve => {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+function waitForWheelEvent(aTarget) {
+ return new Promise(resolve => {
+ // Get the scrollable target element position in this window coordinate
+ // system to send a wheel event to the element.
+ const targetRect = aTarget.getBoundingClientRect();
+ const centerX = targetRect.left + targetRect.width / 2;
+ const centerY = targetRect.top + targetRect.height / 2;
+
+ sendWheelAndPaintNoFlush(aTarget, centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: targetRect.height },
+ resolve);
+ });
+}
+
+const omtaEnabled = isOMTAEnabled();
+
+function add_task_if_omta_enabled(test) {
+ if (!omtaEnabled) {
+ info(test.name + " is skipped because OMTA is disabled");
+ return;
+ }
+ add_task(test);
+}
+
+// We need to wait for all paints before running tests to avoid contaminations
+// from styling of this document itself.
+waitForAllPaints(() => {
+ add_task(async function () {
+ // Start vsync rate measurement in after a RAF callback.
+ await waitForNextFrame();
+
+ const timeAtStart = document.timeline.currentTime;
+ await waitForAnimationFrames(5);
+ const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5;
+
+ // In this test we basically observe restyling counts in 5 frames, if it
+ // takes over 200ms during the 5 frames, this test will fail. So
+ // "200ms / 5 = 40ms" is a threshold whether the test works as expected or
+ // not. We'd take 5ms additional tolerance here.
+ // Note that the 200ms is a period we unthrottle throttled animations that
+ // at least one of the animating styles produces change hints causing
+ // overflow, the value is defined in
+ // KeyframeEffect::OverflowRegionRefreshInterval.
+ if (vsyncRate > 40 - 5) {
+ ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`);
+ SimpleTest.finish();
+ }
+ });
+
+ add_task(async function restyling_for_main_thread_animations() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() {
+ const progress = document.createElement("progress");
+ document.body.appendChild(progress);
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ let markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread on ::-moz-progress-bar');
+ progress.classList.add("stop");
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 0, 'Animation is correctly removed');
+ await ensureElementRemoval(progress);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.finish();
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the compositor should only update style once ' +
+ 'after finish() is called');
+
+ markers = await observeStyling(1);
+ todo_is(markers.length, 0,
+ 'Bug 1415457: Animations running on the compositor should only ' +
+ 'update style once after finish() is called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Finished animations should never update style after one ' +
+ 'restyle happened for finish()');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_transition() {
+ const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Finished transitions should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ markers = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Finished transitions should never cause restyles when mouse is moved ' +
+ 'on the transitions');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_animation() {
+ const div = addDiv(null, { style: 'animation: opacity 1ms' });
+ const animation = div.getAnimations()[0];
+
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Finished animations should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ markers = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Finished animations should never cause restyles when mouse is moved ' +
+ 'on the animations');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity-without-end-value 100s; ' +
+ 'position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Opacity animations on scrolled out elements should never cause ' +
+ 'restyles even if the animation has missing keyframes');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_transform_animations_in_scrolled_out_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation is not running on the compositor');
+
+ let markers;
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ markers = await observeStyling(1);
+ if (markers.length) {
+ break;
+ }
+ }
+ // If the current time has elapsed over 200ms since the animation was
+ // created, it means that the animation should have already
+ // unthrottled in this tick, let's see what we observe in this tick's
+ // restyling process.
+ // We use toPrecision here and below so 199.99999999999977 will turn into 200.
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ markers = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = markers.length < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = markers.length == 1;
+ else
+ expectedMarkersLengthValid = !markers.length;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_out_of_view_transform_animations_in_another_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow: hidden;' });
+ const div = addDiv(null,
+ { style: 'animation: move-in 100s infinite;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation on out of view element ' +
+ 'is not running on the compositor');
+
+ // Structure copied from restyling_transform_animations_in_scrolled_out_element
+ let markers;
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ markers = await observeStyling(1);
+ if (markers.length) {
+ break;
+ }
+ }
+
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on out of view element ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ markers = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = markers.length < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = markers.length == 1;
+ else
+ expectedMarkersLengthValid = !markers.length;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on out of view element ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function finite_transform_animations_in_out_of_view_element() {
+ const parentElement = addDiv(null, { style: 'overflow: hidden;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] },
+ // This animation will move a bit but
+ // will remain out-of-view.
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor");
+
+ const markers = await observeStyling(20);
+ is(markers.length, 20,
+ 'Finite transform animation in out-of-view element should never be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(parentElement);
+
+ // Make sure we are ready to restyle before counting restyles.
+ await waitForFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ const grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: relative; ' +
+ 'top: 20px;' }); // This element is in-view in the parent, but
+ // out of view in the grandparent.
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(grandParent);
+
+ await waitForFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on the main-thread which were in nested scrolled ' +
+ 'out elements should update restyle soon after the element moved ' +
+ 'in view by scrolling');
+
+ await ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in visibility hidden element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForWheelEvent(parentElement);
+
+ await waitForFrame();
+
+ const markers = await observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the main-thread which is in scrolled out ' +
+ 'elements should not update restyling');
+
+ parentElement.style.height = '100px';
+ markers = await observeStyling(1);
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which was in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by resizing');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_changed_element_having_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const childElement = addDiv(null);
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element which ' +
+ 'has a child whose visiblity is inherited from the element and ' +
+ 'the element was initially visible');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_which_gets_visible() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on visibility hidden element should never ' +
+ 'cause restyles');
+
+ div.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running that was on visibility hidden element which ' +
+ 'gets visible should not throttle restyling any more');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_animations_in_visibility_changed_parent() {
+ const parentDiv = addDiv(null, { style: 'visibility: hidden' });
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(div);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running in visibility hidden parent should never cause ' +
+ 'restyles');
+
+ parentDiv.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations that was in visibility hidden parent should not ' +
+ 'throttle restyling any more');
+
+ parentDiv.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations that the parent element became visible should throttle ' +
+ 'restyling again');
+
+ await ensureElementRemoval(parentDiv);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations on visibility hidden element having no visible children ' +
+ 'should never cause restyles');
+
+ const childElement = addDiv(null, { style: 'visibility: visible' });
+ div.appendChild(childElement);
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible child should not throttle restyling');
+
+ childElement.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element that a child ' +
+ 'has become invisible should throttle restyling');
+
+ childElement.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element should not throttle ' +
+ 'restyling after the invisible element changed to visible');
+
+ childElement.remove();
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations running on visibility hidden element should throttle ' +
+ 'restyling again after all visible descendants were removed');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visiblity_hidden_element_having_oof_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: absolute' });
+ const childElement = addDiv(null,
+ { style: 'float: left; visibility: hidden' });
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on visibility hidden element which has an ' +
+ 'out-of-flow child should throttle restyling');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_having_grandchild() {
+ // element tree:
+ //
+ // root(visibility:hidden)
+ // / \
+ // childA childB
+ // / \ / \
+ // AA AB BA BB
+
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+
+ const childA = addDiv(null);
+ div.appendChild(childA);
+ const childB = addDiv(null);
+ div.appendChild(childB);
+
+ const grandchildAA = addDiv(null);
+ childA.appendChild(grandchildAA);
+ const grandchildAB = addDiv(null);
+ childA.appendChild(grandchildAB);
+
+ const grandchildBA = addDiv(null);
+ childB.appendChild(grandchildBA);
+ const grandchildBB = addDiv(null);
+ childB.appendChild(grandchildBB);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations on visibility hidden element having no visible ' +
+ 'descendants should never cause restyles');
+
+ childA.style.visibility = 'visible';
+ grandchildAA.style.visibility = 'visible';
+ grandchildAB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible children should not throttle restyling');
+
+ // Make childA hidden again but both of grandchildAA and grandchildAB are
+ // still visible.
+ childA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element that a child has ' +
+ 'become invisible again but there are still visible children should ' +
+ 'not throttle restyling');
+
+ // Make grandchildAA hidden but grandchildAB is still visible.
+ grandchildAA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element that a grandchild ' +
+ 'become invisible again but another grandchild is still visible ' +
+ 'should not throttle restyling');
+
+
+ // Make childB and grandchildBA visible.
+ childB.style.visibility = 'visible';
+ grandchildBA.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible descendants should not throttle restyling');
+
+ // Make childB hidden but grandchildAB and grandchildBA are still visible.
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible grandchildren should not throttle restyling');
+
+ // Make grandchildAB hidden but grandchildBA is still visible.
+ grandchildAB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible grandchild should not throttle restyling');
+
+ // Make grandchildBA hidden. Now all descedants are invisible.
+ grandchildBA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations on visibility hidden element that all descendants have ' +
+ 'become invisible again should never cause restyles');
+
+ // Make childB visible.
+ childB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animations on visibility hidden element that has a visible child ' +
+ 'should never cause restyles');
+
+ // Make childB invisible again
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animations on visibility hidden element that the visible child ' +
+ 'has become invisible again should never cause restyles');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.pause();
+
+ await animation.ready;
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the compositor should restyle once after ' +
+ 'Animation.pause() was called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Paused animations running on the compositor should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.pause();
+
+ await animation.ready;
+
+ let markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Animations running on the main-thread should restyle once after ' +
+ 'Animation.pause() was called');
+
+ markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Paused animations running on the main-thread should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ const markers = await observeStyling(5);
+ is(markers.length, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function change_duration_and_currenttime() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ // Set currentTime to a time longer than duration.
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ // Now the animation immediately get back from compositor.
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.updateTiming({ duration: 800 * MS_PER_SEC });
+ const markers = await observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when duration is made longer than the current time');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ markers = await observeStyling(1);
+ is(markers.length, 1,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles soon');
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor in the next frame');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_empty_keyframes() {
+ const div = addDiv(null);
+ const animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ let markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ zIndex: ['0', '999'] });
+ markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ markers = await observeStyling(5);
+
+ is(markers.length, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function necessary_update_should_be_invoked() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'on-main-thread 110s';
+ const markers = await observeStyling(1);
+ // There should be two restyles.
+ // 1) Animation-only restyle for before applying the new animation style
+ // 2) Animation-only restyle for after applying the new animation style
+ is(markers.length, 2,
+ 'Applying animation style with different duration ' +
+ 'should restyle twice');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ async function changing_cascading_result_for_main_thread_animation() {
+ const div = addDiv(null, { style: 'on-main-thread: blue' });
+ const animation = div.animate({ opacity: [0, 1],
+ zIndex: ['0', '999'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the z-index style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The z-index animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('z-index', '0', 'important');
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function animation_visibility_and_opacity() {
+ const div = addDiv(null);
+ const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ const markers = await observeStyling(5);
+ is(markers.length, 5, 'The animation should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_for_animation_on_orphaned_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.remove();
+
+ let markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ await waitForNextFrame();
+ markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Animation on re-attached to the document begins to update style, got ' + markers.length);
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ // Tests that if we remove an element from the document whose animation
+ // cascade needs recalculating, that it is correctly updated when it is
+ // re-attached to the document.
+ async function restyling_for_opacity_animation_on_re_attached_element() {
+ const div = addDiv(null, { style: 'opacity: 1 ! important' });
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation overridden by an !important rule is NOT ' +
+ 'running on the compositor');
+
+ // Drop the !important rule to update the cascade.
+ div.style.setProperty('opacity', '1', '');
+
+ div.remove();
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ // Need a frame to give the animation a chance to be sent to the
+ // compositor.
+ await waitForNextFrame();
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation which is no longer overridden by the ' +
+ '!important rule begins running on the compositor even if the ' +
+ '!important rule had been dropped before the target element was ' +
+ 'removed');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_additive_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }],
+ { duration: 100 * MS_PER_SEC, composite: 'add' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Additive animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Tests that missing keyframes animations don't throttle at all.
+ add_task(async function no_throttling_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should not be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that missing keyframes animation on scrolled out element that the
+ // animation is not able to be throttled.
+ add_task(
+ async function no_throttling_missing_keyframe_animations_out_of_view_element() {
+ const div =
+ addDiv(null, { style: 'transform: translateY(-400px);' +
+ 'visibility: collapse;' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'visibility animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element and produces change hint other than paint-only ' +
+ 'change hint should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Counter part of the above test.
+ add_task(async function no_restyling_discrete_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Discrete animation running on the main-thread in an out-of-view ' +
+ 'element should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_while_computed_timing_is_not_changed() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'step-end' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ // We possibly expect one restyle from the initial animation compose, in
+ // order to update animations, but nothing else.
+ ok(markers.length <= 1,
+ 'Animation running on the main-thread while computed timing is not ' +
+ 'changed should not cause extra restyles, got ' + markers.length);
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { x: '-10',
+ y: '-10',
+ width: '10',
+ height: '10',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations on an in-view svg element with post-transform should ' +
+ 'not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_transformed_parent() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'transform: translateX(50px);' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250',
+ width: '40px',
+ height: '40px' });
+ const rect = addSVGElement(svg, 'rect', { x: '0',
+ y: '0',
+ width: '1250',
+ height: '1250',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations on an in-view svg element which is inside transformed ' +
+ 'parent should not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function throttling_animations_out_of_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { width: '10',
+ height: '10',
+ fill: 'red' });
+
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations on an out-of-view svg element with post-transform ' +
+ 'should be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(-50px, -50px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animation on an in-view element with pre-transform should not ' +
+ 'be throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(async function throttling_animations_out_of_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(100px, 100px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view element with pre-transform should be ' +
+ 'throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_absolute_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: absolute; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation in an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_absolute_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: absolute; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_fixed_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: fixed; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_fixed_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: fixed; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_animations_in_view_position_fixed_element() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById('target');
+ target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 5,
+ 'CSS animation on an in-view position:fixed element should NOT be ' +
+ 'throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_iframe() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ iframe.style.height = '0px';
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'position: absolute; top: 50%; width: 100px; height: 100px';
+
+ const animation = target.animate({ opacity: [0, 1] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 0,
+ 'Animation on position:absolute element in collapsed iframe should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' +
+ 'width: 100px; height: 100px;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 5,
+ 'Animation on position:absolute element in collapsed element ' +
+ 'should not be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Animation on collapsed position:absolute element in collapsed ' +
+ 'element should be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function no_restyling_for_compositor_animation_on_unrelated_style_change() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+
+ div.style.setProperty('color', 'blue', '');
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'The opacity animation keeps running on the compositor when ' +
+ 'color style is changed');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_overflow_transform_animations_in_scrollable_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] },
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(20);
+ is(markers.length, 0,
+ 'No-overflow transform animations running on the compositor should ' +
+ 'never update style on the main thread');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function no_flush_on_getAnimations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ is(div.getAnimations().length, 1, 'There should be one animation');
+ });
+ is(markers.length, 0,
+ 'Element.getAnimations() should not flush throttled animation style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_throttled_animation_on_getAnimations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.animationDuration = '0s';
+ is(div.getAnimations().length, 0, 'There should be no animation');
+ });
+
+ is(markers.length, 1, // For discarding the throttled animation.
+ 'Element.getAnimations() should flush throttled animation style so ' +
+ 'that the throttled animation is discarded');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function no_restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ const sibling = addDiv(null);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(animation.playState, 'running',
+ 'Animation.playState should be running');
+ });
+ is(markers.length, 0,
+ 'Animation.playState should not flush throttled animation in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.animationPlayState = 'paused';
+ is(animation.playState, 'paused',
+ 'Animation.playState should be reflected by pending style');
+ });
+
+ is(markers.length, 1,
+ 'Animation.playState should flush throttled animation style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ const sibling = addDiv(null);
+
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(transition.playState, 'running',
+ 'Animation.playState should be running');
+ });
+
+ is(markers.length, 0,
+ 'Animation.playState should not flush throttled transition in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const markers = observeAnimSyncStyling(() => {
+ div.style.transitionProperty = 'none';
+ is(transition.playState, 'idle',
+ 'Animation.playState should be reflected by pending style change ' +
+ 'which cancel the transition');
+ });
+
+ is(markers.length, 1,
+ 'Animation.playState should flush throttled transition style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_visibility_animations_on_in_view_element() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Visibility animation running on the main-thread on in-view element ' +
+ 'should not be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_outline_offset_animations_on_invisible_element() {
+ const div = addDiv(null,
+ { style: 'visibility: hidden; ' +
+ 'outline-style: solid; ' +
+ 'outline-width: 1px;' });
+ const animation =
+ div.animate({ outlineOffset: [ '0px', '10px' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Outline offset animation running on the main-thread on invisible ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animations on visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { transform: 'rotate(360deg)' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_translate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { translate: '100px' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Translate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_rotate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { rotate: '45deg' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Rotate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_scale_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { scale: '2 2' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Scale animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+ const child = addDiv(null, { style: 'position: absolute; top: 100px;' });
+ div.appendChild(child);
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Transform animation having an absolutely positioned child on ' +
+ 'visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_animations_in_out_of_view_iframe() {
+ const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' });
+
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute(
+ 'srcdoc',
+ '<div style="height: 100px;"></div><div id="target"></div>');
+ div.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(markers.length, 0,
+ 'Animation in out-of-view iframe should be throttled');
+
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that transform animations are not able to run on the compositor due
+ // to layout restrictions (e.g. animations on a large size frame) doesn't
+ // flush layout at all.
+ add_task(async function flush_layout_for_transform_animations() {
+ // Set layout.animation.prerender.partial to disallow transform animations
+ // on large frames to be sent to the compositor.
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', false]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate([ { transform: 'rotate(360deg)', } ],
+ { duration: 100 * MS_PER_SEC,
+ // Set step-end to skip further restyles.
+ easing: 'step-end' });
+
+ const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT;
+ ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'Flush is needed for the appended div');
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor");
+
+ // We expect one restyle from the initial animation compose.
+ await waitForNextFrame();
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor");
+ ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'No further layout flush needed');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function partial_prerendered_transform_animations() {
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', true]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate(
+ // Use the same value both for `from` and `to` to avoid jank on the
+ // compositor.
+ { transform: ['rotate(0deg)', 'rotate(0deg)'] },
+ 100 * MS_PER_SEC
+ );
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Transform animation with partial pre-rendered should never cause ' +
+ 'restyles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_on_create_animation() {
+ const div = addDiv();
+ const docShell = getDocShellForObservingRestylesForWindow(window);
+
+ const animationA = div.animate(
+ { transform: ['none', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC
+ );
+ const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animationC = div.animate(
+ { color: ['blue', 'green'] },
+ 100 * MS_PER_SEC
+ );
+ const animationD = div.animate(
+ { width: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+ const animationE = div.animate(
+ { height: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+
+ const markers = docShell
+ .popProfileTimelineMarkers()
+ .filter(marker => marker.name === 'Styles' && !marker.isAnimationOnly);
+ docShell.recordProfileTimelineMarkers = false;
+
+ is(markers.length, 0, 'Creating animations should not flush styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function out_of_view_background_position() {
+ const div = addDiv(null, {
+ style: `
+ background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%);
+ background-size: 80px;
+ animation: background-position 100s infinite;
+ transform: translateY(-400px);
+ `,
+ })
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0, 'background-position animations can be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the main thread in opacity: 0 element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero abspos descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() {
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' });
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(child);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ // Animate the container from 1 to zero opacity and ensure the child animation is throttled then.
+ const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100);
+ await containerAnimation.finished;
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles after root animation has finished');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0; animation: opacity 100s infinite' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const markers = await observeStyling(5);
+
+ is(markers.length, 5,
+ 'Animations in opacity zero descendant element ' +
+ 'should not be throttled if root is animating opacity');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function transparent_background_color_animations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)',
+ 'rgb(200, 0, 0, 0.1)' ] },
+ { duration: 100 * MS_PER_SEC,
+ // An easing function producing zero in the first half of
+ // the duration.
+ easing: 'cubic-bezier(1, 0, 1, 0)' });
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const markers = await observeStyling(5);
+ is(markers.length, 0,
+ 'transparent background-color animation should not update styles on ' +
+ 'the main thread');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+
+ // Load a cross origin iframe.
+ const targetURL = SimpleTest.getTestFileURL("empty.html")
+ .replace(window.location.origin, "http://example.com/");
+ iframe.src = targetURL;
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => {
+ // Create a flex item with "preserve-3d" having an abs-pos child inside
+ // a grid container.
+ // These styles make the the flex item size (0x0).
+ const gridContainer = content.document.createElement("div");
+ gridContainer.style.display = "grid";
+ gridContainer.style.placeItems = "center";
+
+ const target = content.document.createElement("div");
+ target.style.display = "flex";
+ target.style.transformStyle = "preserve-3d";
+ gridContainer.appendChild(target);
+
+ const child = content.document.createElement("div");
+ child.style.position = "absolute";
+ child.style.transform = "rotateY(0deg)";
+ child.style.width = "100px";
+ child.style.height = "100px";
+ child.style.backgroundColor = "green";
+ target.appendChild(child);
+
+ content.document.body.appendChild(gridContainer);
+
+ const animation =
+ target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] },
+ { duration: 100 * MS_PER_SEC,
+ id: "test",
+ easing: 'step-end' });
+ await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'transform animation on a collapsed element should run on the ' +
+ 'compositor');
+
+ const markers = await content.wrappedJSObject.observeStyling(5);
+ is(markers.length, 0,
+ 'transform animation on a collapsed element animation should not ' +
+ 'update styles on the main thread');
+ });
+
+ await ensureElementRemoval(iframe);
+ });
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..4912d05dd1
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ var div = addDiv(t, { style: 'transition: transform 50ms; ' +
+ 'transform: translateX(0px)' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+
+ var timeBeforeStart = window.performance.now();
+ return waitForPaints().then(() => {
+ // If it took over 50ms to paint the transition, we have no luck
+ // to test it. This situation will happen if GC runs while waiting for the
+ // paint.
+ if (window.performance.now() - timeBeforeStart >= 50) {
+ return;
+ }
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_not_equals(transform, '',
+ 'The transition style is applied on the compositor');
+
+ // Generate artificial busyness on the main thread for 100ms.
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {}
+
+ // Now the transition on the compositor should finish but stay at the final
+ // position because there was no chance to pull the transition back from
+ // the compositor.
+ transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The final transition style is still applied on the ' +
+ 'compositor');
+ });
+}, 'Transition on the compositor keeps the final style while the main thread ' +
+ 'is busy even if the transition finished on the compositor');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_cascade.html b/dom/animation/test/mozilla/test_cascade.html
new file mode 100644
index 0000000000..4bdb07530e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cascade.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes margin-left {
+ from { margin-left: 20px; }
+ to { margin-left: 80px; }
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transition: margin-left 100s; ' +
+ 'margin-left: 80px' });
+ var cs = getComputedStyle(div);
+
+ assert_equals(cs.marginLeft, '80px', 'initial margin-left');
+
+ div.style.marginLeft = "20px";
+ assert_equals(cs.marginLeft, '80px', 'margin-left transition at 0s');
+
+ div.style.animation = "margin-left 2s";
+ assert_equals(cs.marginLeft, '20px',
+ 'margin-left animation overrides transition at 0s');
+
+ div.style.animation = "none";
+ assert_equals(cs.marginLeft, '80px',
+ 'margin-left animation stops overriding transition at 0s');
+}, 'Animation overrides/stops overriding transition immediately');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
new file mode 100644
index 0000000000..bdbc78654f
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -0,0 +1,168 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<style>
+@keyframes anim {
+ to { margin-left: 100px; }
+}
+
+.transition-div {
+ margin-left: 100px;
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maxinum floating point value.
+const max_float = '3.40282e38';
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of upper boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for effect easing is out of upper boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of lower boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
+ 'y2 control point for effect easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of lower boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
+ 'y2 control point for keyframe easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
+
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of lower boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
+ 'y2 control point for CSS animation is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'transition-div'});
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getTiming().easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getTiming().easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getTiming().easing,
+ 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on lower boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getTiming().easing,
+ 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
+ 'y2 control point for CSS transition on lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
+
+ anim.pause();
+ // The positive steepest function on both edges.
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 1e+39)' });
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the highest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on upper edge for the highest value of y1 and y2 control points');
+
+ // The negative steepest function on both edges.
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, -1e+39)' });
+ anim.currentTime = 0;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+}, 'Calculated values on both edges');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html
new file mode 100644
index 0000000000..7d0a15b1f7
--- /dev/null
+++ b/dom/animation/test/mozilla/test_deferred_start.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.core.enabled", true],
+ ["dom.animations-api.getAnimations.enabled", true],
+ ["dom.animations-api.timelines.enabled", true],
+ ],
+ },
+ function() {
+ window.open("file_deferred_start.html");
+ }
+);
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html
new file mode 100644
index 0000000000..56e6362273
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({ explicit_done: true });
+SpecialPowers.pushPrefEnv(
+ { set: [['dom.animations-api.autoremove.enabled', false]] },
+ function() {
+ window.open('file_disable_animations_api_autoremove.html');
+ }
+);
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_compositing.html b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html
new file mode 100644
index 0000000000..94216ea62d
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.compositing.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_compositing.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html
new file mode 100644
index 0000000000..a7253439b7
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.getAnimations.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_get_animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html
new file mode 100644
index 0000000000..aaebf1f00a
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.implicit-keyframes.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_implicit_keyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_timelines.html b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html
new file mode 100644
index 0000000000..a20adf4ea2
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+setup({ explicit_done: true });
+SpecialPowers.pushPrefEnv(
+ { set: [['dom.animations-api.timelines.enabled', false]] },
+ function() {
+ window.open('file_disable_animations_api_timelines.html');
+ }
+);
+</script>
diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html
new file mode 100644
index 0000000000..2244143ceb
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disabled_properties.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function waitForSetPref(pref, value) {
+ return SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] });
+}
+
+/*
+ * These tests rely on the fact that the overflow-clip-box property is
+ * disabled by the layout.css.overflow-clip-box.enabled pref. If we ever remove
+ * that pref we will need to substitute some other pref:property combination.
+ */
+
+promise_test(function(t) {
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => {
+ var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]});
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'A property-indexed keyframe specifying only enabled'
+ + ' properties produces keyframes');
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', false);
+ }).then(() => {
+ var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]});
+ assert_equals(anim.effect.getKeyframes().length, 0,
+ 'A property-indexed keyframe specifying only disabled'
+ + ' properties produces no keyframes');
+ });
+}, 'Specifying a disabled property using a property-indexed keyframe');
+
+promise_test(function(t) {
+ var createAnim = () => {
+ var anim = addDiv(t).animate([ { overflowClipBoxBlock: 'padding-box' },
+ { overflowClipBoxBlock: 'content-box' } ]);
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'Animation specified using a keyframe sequence should'
+ + ' return the same number of keyframes regardless of'
+ + ' whether or not the specified properties are disabled');
+ return anim;
+ };
+
+ var assert_has_property = (anim, index, descr, property) => {
+ assert_true(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should have the '${property}' property`);
+ };
+ var assert_does_not_have_property = (anim, index, descr, property) => {
+ assert_false(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should NOT have the '${property}' property`);
+ };
+
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => {
+ var anim = createAnim();
+ assert_has_property(anim, 0, 'Initial keyframe', 'overflowClipBoxBlock');
+ assert_has_property(anim, 1, 'Final keyframe', 'overflowClipBoxBlock');
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', false);
+ }).then(() => {
+ var anim = createAnim();
+ assert_does_not_have_property(anim, 0, 'Initial keyframe',
+ 'overflowClipBoxBlock');
+ assert_does_not_have_property(anim, 1, 'Final keyframe',
+ 'overflowClipBoxBlock');
+ });
+}, 'Specifying a disabled property using a keyframe sequence');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_discrete_animations.html b/dom/animation/test/mozilla/test_discrete_animations.html
new file mode 100644
index 0000000000..d4826a74bd
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete_animations.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [
+ ["layout.css.osx-font-smoothing.enabled", true],
+ ] },
+ function() {
+ window.open("file_discrete_animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_distance_of_basic_shape.html b/dom/animation/test/mozilla/test_distance_of_basic_shape.html
new file mode 100644
index 0000000000..65e403bf06
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_basic_shape.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+// We don't have an official spec to define the distance between two basic
+// shapes, but we still need this for DevTools, so Gecko and Servo backends use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+// See https://github.com/w3c/csswg-drafts/issues/662.
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path', 'circle(10px)', 'circle(20px)');
+ assert_equals(dist, 10, 'circle(10px) and circle(20px)');
+}, 'circles');
+
+test(function(t) {
+ var target = addDiv(t);
+ var circle1 = 'circle(calc(10px + 10px) at 20px 10px)';
+ var circle2 = 'circle(30px at 10px 10px)';
+ var dist = getDistance(target, 'clip-path', circle1, circle2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 10 * 10),
+ circle1 + ' and ' + circle2);
+}, 'circles with positions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var ellipse1 = 'ellipse(20px calc(10px + 10px))';
+ var ellipse2 = 'ellipse(30px 30px)';
+ var dist = getDistance(target, 'clip-path', ellipse1, ellipse2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 10 * 10),
+ ellipse1 + ' and ' + ellipse2);
+}, 'ellipses');
+
+test(function(t) {
+ var target = addDiv(t);
+ var polygon1 = 'polygon(50px 0px, 100px 50px, 50px 100px, 0px 50px)';
+ var polygon2 = 'polygon(40px 0px, 100px 70px, 10px 100px, 0px 70px)';
+ var dist = getDistance(target, 'clip-path', polygon1, polygon2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 20 * 20 + 40 * 40 + 20 * 20),
+ polygon1 + ' and ' + polygon2);
+}, 'polygons');
+
+test(function(t) {
+ var target = addDiv(t);
+ var inset1 = 'inset(5px 5px 5px 5px round 40px 30px 20px 5px)';
+ var inset2 = 'inset(10px 5px round 50px 60px)';
+ var dist = getDistance(target, 'clip-path', inset1, inset2);
+
+ // if we have only two parameter in inset(), the first one means
+ // top and bottom edges, and the second one means left and right edges.
+ // and the definitions of inset is inset(top, right, bottom, left). Besides,
+ // the "round" part uses the shorthand of border radius for each corner, so
+ // each corner is a pair of (x, y). We are computing the distance between:
+ // 1. inset(5px 5px 5px 5px
+ // round (40px 40px) (30px 30px) (20px 20px) (5px 5px))
+ // 2. inset(10px 5px 10px 5px
+ // round (50px 50px) (60px 60px) (50px 50px) (60px 60px))
+ // That is why we need to multiply 2 for each border-radius corner.
+ assert_equals(dist,
+ Math.sqrt(5 * 5 + 5 * 5 +
+ (50 - 40) * (50 - 40) * 2 +
+ (60 - 30) * (60 - 30) * 2 +
+ (50 - 20) * (50 - 20) * 2 +
+ (60 - 5) * (60 - 5) * 2),
+ inset1 + ' and ' + inset2);
+}, 'insets');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path',
+ 'circle(20px)', 'ellipse(10px 20px)');
+ assert_equals(dist, 0, 'circle(20px) and ellipse(10px 20px)');
+}, 'Mismatched basic shapes');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_filter.html b/dom/animation/test/mozilla/test_distance_of_filter.html
new file mode 100644
index 0000000000..33f772d983
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_filter.html
@@ -0,0 +1,248 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+const EPSILON = 1e-6;
+
+// We don't have an official spec to define the distance between two filter
+// lists, but we still need this for DevTools, so Gecko and Servo backends use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+// See https://github.com/w3c/fxtf-drafts/issues/91.
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'blur(10px)', 'none');
+ // The default value of blur is 0px.
+ assert_equals(dist, 10, 'blur(10px) and none');
+}, 'blur and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'blur(10px)', 'blur(1px)');
+ assert_equals(dist, 9, 'blur(10px) and blur(1px)');
+}, 'blurs');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'brightness(75%)', 'none');
+ // The default value of brightness is 100%.
+ assert_equals(dist, (1 - 0.75), 'brightness(75%) and none');
+}, 'brightness and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter',
+ 'brightness(50%)', 'brightness(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'brightness(50%) and brightness(175%)');
+}, 'brightnesses');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'contrast(75%)', 'none');
+ // The default value of contrast is 100%.
+ assert_equals(dist, (1 - 0.75), 'contrast(75%) and none');
+}, 'contrast and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'contrast(50%)', 'contrast(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'contrast(50%) and contrast(175%)');
+}, 'contrasts');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px blue)';
+ var filter2 = 'none';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // The rgba of Blue is rgba(0, 0, 255, 1.0) = rgba(0%, 0%, 100%, 100%).
+ // So we are try to compute the distance of
+ // 1. drop-shadow(10, 10, 10, rgba(0, 0, 1.0, 1.0)).
+ // 2. drop-shadow( 0, 0, 0, rgba(0, 0, 0, 0)).
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 3 + (1 * 1 + 1 * 1)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadow and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px blue)';
+ var filter2 = 'drop-shadow(5px 5px 1px yellow)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // Blue: rgba(0, 0, 255, 1.0) = rgba( 0%, 0%, 100%, 100%).
+ // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%).
+ assert_equals(dist,
+ Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadows');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px)';
+ var filter2 = 'drop-shadow(5px 5px 1px yellow)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%)
+ // Transparent: rgba(0, 0, 0, 0) = rgba( 0%, 0%, 0%, 0%)
+ // Distance involving `currentcolor` is calculated as distance
+ // from `transparent`
+ assert_equals(dist,
+ Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadows with color and non-color');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(25%)', 'none');
+ // The default value of grayscale is 0%.
+ assert_equals(dist, 0.25, 'grayscale(25%) and none');
+}, 'grayscale and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(50%)', 'grayscale(75%)');
+ assert_equals(dist, 0.25, 'grayscale(50%) and grayscale(75%)');
+}, 'grayscales');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(75%)', 'grayscale(175%)');
+ assert_equals(dist, 0.25, 'distance of grayscale(75%) and grayscale(175%)');
+}, 'grayscales where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'hue-rotate(180deg)', 'none');
+ // The default value of hue-rotate is 0deg.
+ assert_approx_equals(dist, Math.PI, EPSILON, 'hue-rotate(180deg) and none');
+}, 'hue-rotate and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter',
+ 'hue-rotate(720deg)', 'hue-rotate(-180deg)');
+ assert_approx_equals(dist, 5 * Math.PI, EPSILON,
+ 'hue-rotate(720deg) and hue-rotate(-180deg)');
+}, 'hue-rotates');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(25%)', 'none');
+ // The default value of invert is 0%.
+ assert_equals(dist, 0.25, 'invert(25%) and none');
+}, 'invert and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(50%)', 'invert(75%)');
+ assert_equals(dist, 0.25, 'invert(50%) and invert(75%)');
+}, 'inverts');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(75%)', 'invert(175%)');
+ assert_equals(dist, 0.25, 'invert(75%) and invert(175%)');
+}, 'inverts where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(75%)', 'none');
+ // The default value of opacity is 100%.
+ assert_equals(dist, (1 - 0.75), 'opacity(75%) and none');
+}, 'opacity and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(50%)', 'opacity(75%)');
+ assert_equals(dist, 0.25, 'opacity(50%) and opacity(75%)');
+}, 'opacities');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(75%)', 'opacity(175%)');
+ assert_equals(dist, 0.25, 'opacity(75%) and opacity(175%)');
+}, 'opacities where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'saturate(75%)', 'none');
+ // The default value of saturate is 100%.
+ assert_equals(dist, (1 - 0.75), 'saturate(75%) and none');
+}, 'saturate and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'saturate(50%)', 'saturate(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'saturate(50%) and saturate(175%)');
+}, 'saturates');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(25%)', 'none');
+ // The default value of sepia is 0%.
+ assert_equals(dist, 0.25, 'sepia(25%) and none');
+}, 'sepia and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(50%)', 'sepia(75%)');
+ assert_equals(dist, 0.25, 'sepia(50%) and sepia(75%)');
+}, 'sepias');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(75%)', 'sepia(175%)');
+ assert_equals(dist, 0.25, 'sepia(75%) and sepia(175%)');
+}, 'sepias where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ // none filter: 'grayscale(0) opacity(1) blur(0px)'
+ var filter2 = 'none';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 5 * 5),
+ filter1 + ' and ' + filter2);
+}, 'Filter list and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ var filter2 = 'grayscale(100%) opacity(50%) blur(1px)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4),
+ filter1 + ' and ' + filter2);
+}, 'Filter lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ var filter2 = 'grayscale(100%) opacity(50%) blur(1px) sepia(50%)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4 + 0.5 * 0.5),
+ filter1 + ' and ' + filter2);
+}, 'Filter lists where one has extra functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%)';
+ var filter2 = 'opacity(100%) grayscale(50%)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist, 0, filter1 + ' and ' + filter2);
+}, 'Mismatched filter lists');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_path_function.html b/dom/animation/test/mozilla/test_distance_of_path_function.html
new file mode 100644
index 0000000000..af6592c892
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_path_function.html
@@ -0,0 +1,140 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id="log"></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path', 'path("M 10 10")', 'none');
+ assert_equals(dist, 0, 'path("M 10 10") and none');
+}, 'Path and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 10 10 H 10")',
+ 'path("M 10 10 H 10 H 10")');
+ assert_equals(dist, 0, 'path("M 10 10 H 10") and ' +
+ 'path("M 10 10 H 10 H 10")');
+}, 'Mismatched path functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 10 10")',
+ 'path("M 20 20")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2),
+ 'path("M 10 10") and path("M 30 30")');
+}, 'The moveto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 L 10 10")',
+ 'path("M 0 0 L 20 20")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2),
+ 'path("M 0 0 L 10 10") and path("M 0 0 L 20 20")');
+}, 'The lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 H 10")',
+ 'path("M 0 0 H 20")');
+ assert_equals(dist, 10, 'path("M 0 0 H 10") and path("M 0 0 H 20")');
+}, 'The horizontal lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 V 10")',
+ 'path("M 0 0 V 20")');
+ assert_equals(dist, 10, 'path("M 0 0 V 10") and path("M 0 0 V 20")');
+}, 'The vertical lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 C 10 10 20 20 30 30")',
+ 'path("M 0 0 C 20 20 40 40 0 0")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2 + 20 * 20 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 C 10 10 20 20 30 30") and ' +
+ 'path("M 0 0 C 20 20 40 40 0 0")');
+}, 'The cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 S 20 20 30 30")',
+ 'path("M 0 0 S 40 40 0 0")');
+ assert_equals(dist,
+ Math.sqrt(20 * 20 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 S 20 20 30 30") and ' +
+ 'path("M 0 0 S 40 40 0 0")');
+}, 'The smooth cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 Q 10 10 30 30")',
+ 'path("M 0 0 Q 20 20 0 0")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 Q 10 10 30 30") and ' +
+ 'path("M 0 0 Q 20 20 0 0")');
+}, 'The quadratic cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 T 30 30")',
+ 'path("M 0 0 T 0 0")');
+ assert_equals(dist,
+ Math.sqrt(30 * 30 * 2),
+ 'path("M 0 0 T 30 30") and ' +
+ 'path("M 0 0 T 0 0")');
+}, 'The smooth quadratic cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 A 5 5 10 0 1 30 30")',
+ 'path("M 0 0 A 4 4 5 0 0 20 20")');
+ assert_equals(dist,
+ Math.sqrt(1 * 1 * 2 + // radii
+ 5 * 5 + // angle
+ 1 * 1 + // flag
+ 10 * 10 * 2),
+ 'path("M 0 0 A 5 5 10 0 1 30 30") and ' +
+ 'path("M 0 0 A 4 4 5 0 0 20 20")');
+}, 'The elliptical arc curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60")',
+ // == 'path("M 10 20 H 40 V 80 H 50 V 70 L 160 130")'
+ 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")');
+ assert_equals(dist,
+ Math.sqrt(120 * 120 * 2 +
+ 80 * 80 * 4 +
+ 40 * 40 * 2),
+ 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60") and ' +
+ 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")');
+}, 'The distance of paths with absolute and relative coordinates');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_transform.html b/dom/animation/test/mozilla/test_distance_of_transform.html
new file mode 100644
index 0000000000..96ff1eb66d
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_transform.html
@@ -0,0 +1,404 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+// We don't have an official spec to define the distance between two transform
+// lists, but we still need this for DevTools, so Gecko and Servo backend use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+
+const EPSILON = 0.00001;
+
+// |v| should be a unit vector (i.e. having length 1)
+function getQuaternion(v, angle) {
+ return [
+ v[0] * Math.sin(angle / 2.0),
+ v[1] * Math.sin(angle / 2.0),
+ v[2] * Math.sin(angle / 2.0),
+ Math.cos(angle / 2.0)
+ ];
+}
+
+function computeRotateDistance(q1, q2) {
+ const dot = q1.reduce((sum, e, i) => sum + e * q2[i], 0);
+ return Math.acos(Math.min(Math.max(dot, -1.0), 1.0)) * 2.0;
+}
+
+function createMatrixFromArray(array) {
+ return (array.length === 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`;
+}
+
+function rotate3dToMatrix(x, y, z, radian) {
+ var sc = Math.sin(radian / 2) * Math.cos(radian / 2);
+ var sq = Math.sin(radian / 2) * Math.sin(radian / 2);
+
+ // Normalize the vector.
+ var length = Math.sqrt(x*x + y*y + z*z);
+ x /= length;
+ y /= length;
+ z /= length;
+
+ return [
+ 1 - 2 * (y*y + z*z) * sq,
+ 2 * (x * y * sq + z * sc),
+ 2 * (x * z * sq - y * sc),
+ 0,
+ 2 * (x * y * sq - z * sc),
+ 1 - 2 * (x*x + z*z) * sq,
+ 2 * (y * z * sq + x * sc),
+ 0,
+ 2 * (x * z * sq + y * sc),
+ 2 * (y * z * sq - x * sc),
+ 1 - 2 * (x*x + y*y) * sq,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1
+ ];
+}
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'none', 'none');
+ assert_equals(dist, 0, 'distance of translate');
+}, 'Test distance of none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'translate(100px)', 'none');
+ assert_equals(dist, 100, 'distance of translate');
+}, 'Test distance of translate function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'translate(100px)', 'translate(200px)');
+ assert_equals(dist, 200 - 100, 'distance of translate');
+}, 'Test distance of translate functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'translate3d(100px, 0, 50px)', 'none');
+ assert_equals(dist, Math.sqrt(100 * 100 + 50 * 50),
+ 'distance of translate3d');
+}, 'Test distance of translate3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform',
+ 'translate3d(100px, 0, 50px)',
+ 'translate3d(200px, 80px, 0)');
+ assert_equals(dist, Math.sqrt(100 * 100 + 80 * 80 + 50 * 50),
+ 'distance of translate');
+}, 'Test distance of translate3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'scale(1.5)', 'none');
+ assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
+}, 'Test distance of scale function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'scale(1.5)', 'scale(2.0)');
+ assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
+}, 'Test distance of scale functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'scale3d(1.5, 1.5, 1.5)',
+ 'none');
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
+ 'distance of scale3d');
+}, 'Test distance of scale3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'scale3d(1.5, 1.5, 1.5)',
+ 'scale3d(2.0, 2.0, 1.0)');
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
+ 'distance of scale3d');
+}, 'Test distance of scale3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'rotate(45deg)', 'rotate(90deg)');
+ assert_approx_equals(dist, Math.PI / 2.0 - Math.PI / 4.0, EPSILON, 'distance of rotate');
+}, 'Test distance of rotate functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'rotate(45deg)', 'none');
+ assert_approx_equals(dist, Math.PI / 4.0, EPSILON, 'distance of rotate');
+}, 'Test distance of rotate function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 1, 0, 90deg)',
+ 'none');
+ assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 0, 1, 90deg)',
+ 'rotate3d(1, 0, 0, 90deg)');
+ let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
+ let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
+ assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 0, 1, 90deg)',
+ 'rotate3d(0, 0, 0, 90deg)');
+ assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d functions whose direction vector cannot be ' +
+ 'normalized');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'skew(1rad, 0.5rad)', 'none');
+ assert_approx_equals(dist, Math.sqrt(1 * 1 + 0.5 * 0.5), EPSILON, 'distance of skew');
+}, 'Test distance of skew function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'skew(1rad, 0.5rad)',
+ 'skew(-1rad, 0)');
+ assert_approx_equals(dist, Math.sqrt(2 * 2 + 0.5 * 0.5), EPSILON, 'distance of skew');
+}, 'Test distance of skew functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'none');
+ assert_equals(dist, Infinity, 'distance of perspective');
+}, 'Test distance of perspective function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ // perspective(0) is treated as perspective(inf) because perspective length
+ // should be greater than or equal to zero.
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'perspective(0)');
+ assert_equals(dist, 128, 'distance of perspective');
+}, 'Test distance of perspective function and an invalid perspective');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'perspective(1024px)');
+ assert_equals(dist, 1024 - 128, 'distance of perspective');
+}, 'Test distance of perspective functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var sin_30 = Math.sin(Math.PI / 6);
+ var cos_30 = Math.cos(Math.PI / 6);
+ // matrix => translate(100, 0) rotate(30deg).
+ var matrix = createMatrixFromArray([ cos_30, sin_30,
+ -sin_30, cos_30,
+ 100, 0 ]);
+ var dist = getDistance(target, 'transform', matrix, 'none');
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + (Math.PI / 6) * (Math.PI / 6)),
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var sin_30 = Math.sin(Math.PI / 6);
+ var cos_30 = Math.cos(Math.PI / 6);
+ // matrix1 => translate(100, 0) rotate(30deg).
+ var matrix1 = createMatrixFromArray([ cos_30, sin_30,
+ -sin_30, cos_30,
+ 100, 0 ]);
+ // matrix2 => translate(0, 100) scale(0.5).
+ var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0.5, 0, 100 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + 100 * 100 + // translate
+ (Math.PI / 6) * (Math.PI / 6) + // rotate
+ 0.5 * 0.5 + 0.5 * 0.5), // scale
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var matrix = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
+ var dist = getDistance(target, 'transform', matrix, 'none');
+ assert_approx_equals(dist, Math.PI / 6, EPSILON, 'distance of matrix3d');
+}, 'Test distance of matrix3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ // matrix1 => rotate3d(0, 1, 0, 30deg).
+ var matrix1 = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
+ // matrix1 => translate3d(100, 0, 0) scale3d(0.5, 0.5, 0.5).
+ var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0,
+ 0, 0.5, 0, 0,
+ 0, 0, 0.5, 0,
+ 100, 0, 0, 1 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + // translate
+ 0.5 * 0.5 * 3 + // scale
+ (Math.PI / 6) * (Math.PI / 6)), // rotate
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var cos_180 = Math.cos(Math.PI);
+ var sin_180 = Math.sin(Math.PI);
+ // matrix1 => translate3d(100px, 50px, -10px) skew(45deg).
+ var matrix1 = createMatrixFromArray([ 1, 0, 0, 0,
+ Math.tan(Math.PI/4.0), 1, 0, 0,
+ 0, 0, 1, 0,
+ 100, 50, -10, 1]);
+ // matrix2 => translate3d(1000px, 0, 0) rotate3d(1, 0, 0, 180deg).
+ var matrix2 = createMatrixFromArray([ 1, 0, 0, 0,
+ 0, cos_180, sin_180, 0,
+ 0, -sin_180, cos_180, 0,
+ 1000, 0, 0, 1 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(900 * 900 + 50 * 50 + 10 * 10 + // translate
+ Math.PI * Math.PI + // rotate
+ (Math.PI / 4) * (Math.PI / 4)), // skew angle
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix3d functions with skew factors');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform',
+ 'rotate(180deg) translate(1000px)',
+ 'rotate(360deg) translate(0px)');
+ assert_approx_equals(dist, Math.sqrt(1000 * 1000 + Math.PI * Math.PI), EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of transform lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(100px) rotate(180deg)',
+ 'translate(50px) rotate(90deg) scale(5) skew(1rad)');
+ assert_approx_equals(dist,
+ Math.sqrt(50 * 50 +
+ Math.PI / 2 * Math.PI / 2 +
+ 4 * 4 * 2 +
+ 1 * 1),
+ EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of transform lists where one has extra items');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(1000px) rotate3d(1, 0, 0, 180deg)',
+ 'translate(1000px) scale3d(2.5, 0.5, 1)');
+ assert_equals(dist, Math.sqrt(Math.PI * Math.PI + 1.5 * 1.5 + 0.5 * 0.5),
+ 'distance of transform lists');
+}, 'Test distance of mismatched transform lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(100px) skew(1rad)',
+ 'translate(1000px) rotate3d(0, 1, 0, -2rad)');
+ assert_approx_equals(dist,
+ Math.sqrt(900 * 900 + 1 * 1 + 2 * 2),
+ EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of mismatched transform lists with skew function');
+
+
+// Individual transforms
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '50px', 'none');
+ assert_equals(dist, Math.sqrt(50 * 50), 'distance of 2D translate and none');
+}, 'Test distance of 2D translate property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '10px 30px', '50px');
+ assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30), 'distance of 2D translate');
+}, 'Test distance of 2D translate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '10px 30px 50px', '50px');
+ assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30 + 50 * 50),
+ 'distance of 3D translate');
+}, 'Test distance of 3D translate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '2', 'none');
+ assert_equals(dist, Math.sqrt(1 + 1), 'distance of 2D scale and none');
+}, 'Test distance of 2D scale property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '3', '1 1');
+ assert_equals(dist, Math.sqrt(2 * 2 + 2 * 2), 'distance of 2D scale');
+}, 'Test distance of 2D scale property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '3 2 2', '1 1');
+ assert_equals(dist, Math.sqrt(2 * 2 + 1 * 1 + 1 * 1),
+ 'distance of 3D scale');
+}, 'Test distance of 3D scale property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', '180deg', 'none');
+ assert_equals(dist, Math.PI, 'distance of 2D rotate and none');
+}, 'Test distance of 2D rotate property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', '180deg', '90deg');
+ assert_equals(dist, Math.PI / 2.0, 'distance of 2D rotate');
+}, 'Test distance of 2D rotate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', 'z 90deg', 'x 90deg');
+ let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
+ let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
+ assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON,
+ 'distance of 3D rotate');
+}, 'Test distance of 3D rotate property');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
new file mode 100644
index 0000000000..b2aeef8a77
--- /dev/null
+++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// If the originTime parameter passed to the DocumentTimeline exceeds
+// the range of the internal storage type (a signed 64-bit integer number
+// of ticks--a platform-dependent unit) then we should throw.
+// Infinity isn't allowed as an origin time value and clamping to just
+// inside the allowed range will just mean we overflow elsewhere.
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is positive infinity');
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is negative infinity');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_event_listener_leaks.html b/dom/animation/test/mozilla/test_event_listener_leaks.html
new file mode 100644
index 0000000000..bcfadaf9e9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_event_listener_leaks.html
@@ -0,0 +1,43 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450271 - Test Animation event listener leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+// Manipulate Animation. Its important here that we create a
+// listener callback from the DOM objects back to the frame's global
+// in order to exercise the leak condition.
+async function useAnimation(contentWindow) {
+ let div = contentWindow.document.createElement("div");
+ contentWindow.document.body.appendChild(div);
+ let animation = div.animate({}, 100 * 1000);
+ is(animation.playState, "running", "animation should be running");
+ animation.onfinish = _ => {
+ contentWindow.finishCount += 1;
+ };
+}
+
+async function runTest() {
+ try {
+ await checkForEventListenerLeaks("Animation", useAnimation);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html
new file mode 100644
index 0000000000..7d20e5b70b
--- /dev/null
+++ b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() which doesn't return scroll animations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+ @keyframes animWidth {
+ from { width: 100px; }
+ to { width: 200px }
+ }
+ @keyframes animTop {
+ to { top: 100px }
+ }
+ .fill-vh {
+ width: 100px;
+ height: 100vh;
+ }
+</style>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = addDiv(t,
+ { style: "width: 10px; height: 100px; " +
+ "animation: animWidth 100s scroll(), animTop 200s;" });
+
+ // Sanity check to make sure the scroll animation is there.
+ addDiv(t, { class: "fill-vh" });
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ assert_equals(getComputedStyle(div).width, "200px",
+ "The scroll animation is there");
+
+ const animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() should include scroll animations');
+ assert_equals(animations[0].animationName, "animWidth",
+ 'getAmimations() should return scroll animations');
+ // FIXME: Bug 1676794. Support ScrollTimeline interface.
+ assert_equals(animations[0].timeline, null,
+ 'scroll animation should not return scroll timeline');
+}, 'Element.getAnimation() should include scroll animations');
+
+test(function(t) {
+ const div = addDiv(t,
+ { style: "width: 10px; height: 100px; " +
+ "animation: animWidth 100s scroll(), animTop 100s;" });
+
+ // Sanity check to make sure the scroll animation is there.
+ addDiv(t, { class: "fill-vh" });
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ assert_equals(getComputedStyle(div).width, "200px",
+ "The scroll animation is there");
+
+ const animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() should include scroll animations');
+ assert_equals(animations[0].animationName, "animWidth",
+ 'getAmimations() should return scroll animations');
+ // FIXME: Bug 1676794. Support ScrollTimeline interface.
+ assert_equals(animations[0].timeline, null,
+ 'scroll animation should not return scroll timeline');
+}, 'Document.getAnimation() should include scroll animations');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html
new file mode 100644
index 0000000000..f36543bb1e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_hide_and_show.html
@@ -0,0 +1,198 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes move {
+ 100% {
+ transform: translateX(100px);
+ }
+}
+
+div.pseudo::before {
+ animation: move 0.01s;
+ content: 'content';
+}
+
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+}, 'Animation stops playing when the element style display is set to "none"');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+}, 'Animation stops playing when its parent element style display is set ' +
+ 'to "none"');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+}, 'Animation starts playing when the element gets shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+}, 'Animation starts playing when its parent element is shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ div.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation which has already finished starts playing when the element ' +
+ 'gets shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation with fill:forwards which has already finished starts playing ' +
+ 'when its parent element is shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 0,
+ 'Element does not have finished animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'CSS Animation which has already finished starts playing when its parent ' +
+ 'element is shown from "display:none" state');
+
+promise_test(function(t) {
+ var div = addDiv(t, { 'class': 'pseudo' });
+ var eventWatcher = new EventWatcher(t, div, 'animationend');
+
+ assert_equals(document.getAnimations().length, 1,
+ 'CSS animation on pseudo element');
+
+ return eventWatcher.wait_for('animationend').then(function() {
+ assert_equals(document.getAnimations().length, 0,
+ 'No CSS animation on pseudo element after the animation ' +
+ 'finished');
+
+ // Remove the class which generated this pseudo element.
+ div.classList.remove('pseudo');
+
+ // We need to wait for two frames to process re-framing.
+ // The callback of 'animationend' is processed just before rAF callbacks,
+ // and rAF callbacks are processed before re-framing process, so waiting for
+ // one rAF callback is not sufficient.
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ // Add the class again to re-generate pseudo element.
+ div.classList.add('pseudo');
+ assert_equals(document.getAnimations().length, 1,
+ 'A new CSS animation on pseudo element');
+ });
+}, 'CSS animation on pseudo element restarts after the pseudo element that ' +
+ 'had a finished CSS animation is re-generated');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html
new file mode 100644
index 0000000000..3653fd9536
--- /dev/null
+++ b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+.compositable {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ 'dom.animations.mainthread-synchronization-with-geometric-animations', false ]]
+ });
+
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_true(SpecialPowers.wrap(animA).isRunningOnCompositor,
+ 'Transform animation should not synchronize with margin-left animation ' +
+ 'created within the same tick with disabling the corresponding pref');
+}, 'Transform animation should not synchronize with margin-left animation '
+ + 'created within the same tick with disabling the corresponding pref');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_moz_prefixed_properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html
new file mode 100644
index 0000000000..f65d05134d
--- /dev/null
+++ b/dom/animation/test/mozilla/test_moz_prefixed_properties.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test animations of all properties that have -moz prefix</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <script src="../property_database.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+const testcases = [
+ {
+ property: "-moz-box-align"
+ },
+ {
+ property: "-moz-box-direction"
+ },
+ {
+ property: "-moz-box-ordinal-group"
+ },
+ {
+ property: "-moz-box-orient",
+ },
+ {
+ property: "-moz-box-pack"
+ },
+ {
+ property: "-moz-float-edge"
+ },
+ {
+ property: "-moz-force-broken-image-icon"
+ },
+ {
+ property: "-moz-orient"
+ },
+ {
+ property: "-moz-osx-font-smoothing",
+ pref: "layout.css.osx-font-smoothing.enabled"
+ },
+ {
+ property: "-moz-text-size-adjust"
+ },
+ {
+ property: "-moz-user-focus"
+ },
+ {
+ property: "-moz-user-input"
+ },
+ {
+ property: "-moz-user-modify"
+ },
+ {
+ property: "user-select"
+ },
+ {
+ property: "-moz-window-dragging"
+ },
+];
+
+testcases.forEach(testcase => {
+ if (testcase.pref && !IsCSSPropertyPrefEnabled(testcase.pref)) {
+ return;
+ }
+
+ const property = gCSSProperties[testcase.property];
+ const values = property.initial_values.concat(property.other_values);
+ values.forEach(value => {
+ test(function(t) {
+ const container = addDiv(t);
+ const target = document.createElement("div");
+ container.appendChild(target);
+
+ container.style[property.domProp] = value;
+
+ const animation =
+ target.animate({ [property.domProp]: [value, "inherit"] },
+ { duration: 1000, delay: -500 } );
+
+ const expectedValue = getComputedStyle(container)[property.domProp];
+ assert_equals(getComputedStyle(target)[property.domProp], expectedValue,
+ `Computed style shoud be "${ expectedValue }"`);
+ }, `Test inherit value for "${ testcase.property }" `
+ + `(Parent element style is "${ value }")`);
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/animation/test/mozilla/test_pending_animation_tracker.html b/dom/animation/test/mozilla/test_pending_animation_tracker.html
new file mode 100644
index 0000000000..022efa7bcf
--- /dev/null
+++ b/dom/animation/test/mozilla/test_pending_animation_tracker.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test animations in PendingAnimationTracker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(function waitForLoad() {
+ return new Promise(resolve => {
+ window.addEventListener("load", resolve, { once: true });
+ });
+});
+
+promise_test(async t => {
+ // See below, but we should ensure we are in a rAF callback before proceeding
+ // or else we will get inconsistent results.
+ await waitForNextFrame();
+
+ const target = addDiv(t);
+ const anim = target.animate(null, 100 * MS_PER_SEC);
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be tracked by tracker');
+
+ anim.effect = null;
+ await waitForNextFrame();
+
+ assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should NOT be tracked by the tracker');
+}, 'An animation whose effect is made null while pending is subsequently'
+ + ' removed from the tracker');
+
+test(t => {
+ const target = addDiv(t);
+ const anim = target.animate(null, 100 * MS_PER_SEC);
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be tracked by tracker');
+
+ const newEffect = new KeyframeEffect(target, null);
+ anim.effect = newEffect;
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be still tracked by tracker');
+}, 'Setting another effect keeps the pending animation in the tracker');
+
+test(t => {
+ const effect = new KeyframeEffect(null, null);
+ const anim = new Animation(effect);
+ anim.play();
+ assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The orphaned animation should NOT be tracked by tracker');
+
+ const target = addDiv(t);
+ const newEffect = new KeyframeEffect(target, null);
+ anim.effect = newEffect;
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be now tracked by tracker');
+}, 'Setting effect having target element starts being tracked by the ' +
+ 'tracker');
+
+test(t => {
+ const target = addDiv(t);
+ const anim = target.animate(null, 100 * MS_PER_SEC);
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be tracked by tracker');
+
+ anim.cancel();
+
+ assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should NOT be tracked by the tracker');
+}, 'Calling cancel() removes the animation from the tracker');
+
+promise_test(async t => {
+ // Before proceeding this test, make sure following code is _NOT_ processed
+ // between paint and refresh driver's tick. Otherwise, waitForNextFrame below
+ // doesn't ensure that a paint process happens which means that there is
+ // no chance to call TriggerPendingAnimationsOnNextTick to discard the
+ // animation from the pending animation tracker.
+ await waitForNextFrame();
+
+ const target = addDiv(t);
+ const anim = target.animate(null, 100 * MS_PER_SEC);
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be tracked by tracker');
+
+ target.remove();
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation is still being tracked by the tracker');
+
+ await waitForNextFrame();
+ assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should NOT be tracked by the tracker in the ' +
+ 'next frame');
+}, 'Removing target element from the document removes the animation from ' +
+ 'the tracker in the next tick');
+
+test(t => {
+ const target = addDiv(t);
+ const anotherTarget = addDiv(t);
+ const anim = target.animate(null, 100 * MS_PER_SEC);
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be tracked by tracker');
+
+ anim.effect.target = anotherTarget;
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be still tracked by tracker');
+}, 'Setting another target keeps the pending animation in the tracker');
+
+test(t => {
+ const effect = new KeyframeEffect(null, null);
+ const anim = new Animation(effect);
+ anim.play();
+ assert_false(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The orphaned animation should NOT be tracked by tracker');
+
+ const target = addDiv(t);
+ anim.effect.target = target;
+
+ assert_true(SpecialPowers.DOMWindowUtils.isAnimationInPendingTracker(anim),
+ 'The animation should be now tracked by tracker');
+}, 'Setting target element to the orphaned animation starts being tracked ' +
+ 'by the tracker');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html
new file mode 100644
index 0000000000..bc1ab70c74
--- /dev/null
+++ b/dom/animation/test/mozilla/test_restyles.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<div id='log'></div>
+<script>
+'use strict';
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1); // bug 1332970
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ['layout.reflow.synthMouseMove', false],
+ ['privacy.reduceTimerPrecision', false],
+ ],
+ },
+ function() {
+ window.open('file_restyles.html');
+ }
+);
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
new file mode 100644
index 0000000000..67b6ac8845
--- /dev/null
+++ b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This test supplements the web-platform-tests in:
+//
+// web-animations/interfaces/Animatable/animate-no-browsing-context.html
+//
+// Specifically, it covers the case where we have a running animation
+// targetting an element in a document without a browsing context.
+//
+// Currently the behavior in this case is not well-defined. For example,
+// if we were to simply take an element from such a document, and do:
+//
+// const xdoc = xhr.responseXML;
+// const div = xdoc.getElementById('test');
+// div.style.opacity = '0';
+// alert(getComputedStyle(div).opacity);
+//
+// We'd get '0' in Firefox and Edge, but an empty string in Chrome.
+//
+// However, if instead of using the style attribute, we set style in a <style>
+// element in *either* the document we're calling from *or* the XHR doc and
+// do the same we get '1' in Firefox and Edge, but an empty string in Chrome.
+//
+// That is, no browser appears to apply styles to elements in a document without
+// a browsing context unless the styles are defined using the style attribute,
+// and even then Chrome does not.
+//
+// There is some prose in CSSOM which says,
+//
+// Note: This means that even if obj is in a different document (e.g. one
+// fetched via XMLHttpRequest) it will still use the style rules associated
+// with the document that is associated with the global object on which
+// getComputedStyle() was invoked to compute the CSS declaration block.[1]
+//
+// However, this text has been around since at least 2013 and does not appear
+// to be implemented.
+//
+// As a result, it's not really possible to write a cross-browser test for the
+// behavior for animations in this context since it's not clear what the result
+// should be. That said, we still want to exercise this particular code path so
+// we make this case a Mozilla-specific test. The other similar tests cases for
+// which the behavior is well-defined are covered by web-platform-tests.
+//
+// [1] https://drafts.csswg.org/cssom/#extensions-to-the-window-interface
+
+function getXHRDoc(t) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'xhr_doc.html');
+ xhr.responseType = 'document';
+ xhr.onload = t.step_func(() => {
+ assert_equals(xhr.readyState, xhr.DONE,
+ 'Request should complete successfully');
+ assert_equals(xhr.status, 200,
+ 'Response should be OK');
+ resolve(xhr.responseXML);
+ });
+ xhr.send();
+ });
+}
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ anim = div.animate({ opacity: [ 0, 1 ] }, 1000);
+ // Give the animation an active timeline and kick-start it.
+ anim.timeline = document.timeline;
+ anim.startTime = document.timeline.currentTime;
+ assert_equals(anim.playState, 'running',
+ 'The animation should be running');
+ // Gecko currently skips applying animation styles to elements in documents
+ // without browsing contexts.
+ assert_not_equals(getComputedStyle(div).opacity, '0',
+ 'Style should NOT be updated');
+ });
+}, 'Forcing an animation targetting an element in a document without a'
+ + ' browsing context to play does not cause style to update');
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = addDiv(t);
+ anim = div.animate({ opacity: [ 0, 1 ] }, 1000);
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'Style should be updated');
+ // Trigger an animation restyle to be queued
+ anim.currentTime = 0.1;
+ // Adopt node into XHR doc
+ xhrdoc.body.appendChild(div);
+ // We should skip applying animation styles to elements in documents
+ // without a pres shell.
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'Style should NOT be updated');
+ });
+}, 'Moving an element with a pending animation restyle to a document without'
+ + ' a browsing context resets animation style');
+
+</script>
diff --git a/dom/animation/test/mozilla/test_set_easing.html b/dom/animation/test/mozilla/test_set_easing.html
new file mode 100644
index 0000000000..55c77f0e8f
--- /dev/null
+++ b/dom/animation/test/mozilla/test_set_easing.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test setting easing in sandbox</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ div.animate({ opacity: [0, 1] }, 100000 );
+
+ const contentScript = function() {
+ try {
+ document.getAnimations()[0].effect.updateTiming({ easing: 'linear' });
+ assert_true(true, 'Setting easing should not throw in sandbox');
+ } catch (e) {
+ assert_unreached('Setting easing threw ' + e);
+ }
+ };
+
+ const sandbox = new SpecialPowers.Cu.Sandbox(window);
+ sandbox.importFunction(document, "document");
+ sandbox.importFunction(assert_true, "assert_true");
+ sandbox.importFunction(assert_unreached, "assert_unreached");
+ SpecialPowers.Cu.evalInSandbox(`(${contentScript.toString()})()`, sandbox);
+}, 'Setting easing should not throw any exceptions in sandbox');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html
new file mode 100644
index 0000000000..bccae9e0d5
--- /dev/null
+++ b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html
@@ -0,0 +1,138 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test for styles after finished on the compositor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+.compositor {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: green;
+}
+</style>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { delay: 10,
+ duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation with positive delay is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 1 },
+ { offset: 0.9, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation initially opacity: 1 is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 0 },
+ { offset: 0.5, opacity: 1 },
+ { offset: 0.51, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { delay: 10, duration: 100 });
+
+ await waitForAnimationFrames(2);
+
+ // Setting the current time at the offset generating opacity: 1.
+ anim.currentTime = 60;
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation is removed from compositor even when it only visits ' +
+ 'exactly the point where the opacity: 1 value was set');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { delay: 10,
+ duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation with positive delay is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'none' },
+ { offset: 0.9, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation initially transform: none is removed from compositor ' +
+ 'when finished');
+
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'translateX(100px)' },
+ { offset: 0.5, transform: 'none' },
+ { offset: 0.9, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { delay: 10, duration: 100 });
+
+ await waitForAnimationFrames(2);
+
+ // Setting the current time at the offset generating transform: none.
+ anim.currentTime = 60;
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation is removed from compositor even when it only visits ' +
+ 'exactly the point where the transform: none value was set');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html
new file mode 100644
index 0000000000..92d1b7e1ec
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transform_limits.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maximum floating point value.
+const MAX_FLOAT = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'scale(1)' },
+ { transform: 'scale(3.5e+38)'},
+ { transform: 'scale(3)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(' + MAX_FLOAT + ', 0, 0, ' + MAX_FLOAT + ', 0, 0)');
+}, 'Test that the parameter of transform scale is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'translate(1px)' },
+ { transform: 'translate(3.5e+38px)'},
+ { transform: 'translate(3px)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, ' + MAX_FLOAT + ', 0)');
+}, 'Test that the parameter of transform translate is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' },
+ { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'},
+ { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ],
+ 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2, 0, 0, 2, ' + MAX_FLOAT + ', 0)');
+}, 'Test that the parameter of transform matrix is clamped' );
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..46a154b9af
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+// This test appears like it might get racey and cause a timeout with too low of a
+// precision, so we hardcode it to something reasonable.
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ['privacy.reduceTimerPrecision', true],
+ ['privacy.resistFingerprinting.reduceTimerPrecision.microseconds', 2000],
+ ],
+ },
+ function() {
+ window.open('file_transition_finish_on_compositor.html');
+ }
+);
+</script>
diff --git a/dom/animation/test/mozilla/test_underlying_discrete_value.html b/dom/animation/test/mozilla/test_underlying_discrete_value.html
new file mode 100644
index 0000000000..3961305df3
--- /dev/null
+++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html
@@ -0,0 +1,188 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+// Tests that we correctly extract the underlying value when the animation
+// type is 'discrete'.
+const discreteTests = [
+ {
+ stylesheet: {
+ "@keyframes keyframes":
+ "from { align-content: flex-start; } to { align-content: flex-end; } "
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for fully-specified keyframes"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "from { align-content: flex-start; }"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "normal" }
+ ],
+ explanation: "Test for 0% keyframe only",
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "to { align-content: flex-end; }"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "normal" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for 100% keyframe only",
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }"
+ },
+ attributes: {
+ style: "align-content: space-between"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using style attribute"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: inherit;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "normal" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "normal" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and 'inherit' specified on target element",
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ ".target": "align-content: space-between;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using type selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-end" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using ID selector that overrides class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between !important;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using important type selector that overrides other rules"
+ },
+];
+
+discreteTests.forEach(testcase => {
+ test(t => {
+ if (testcase.skip) {
+ return;
+ }
+ addStyle(t, testcase.stylesheet);
+
+ const div = addDiv(t, { "id": "target" });
+ if (testcase.attributes) {
+ for (let attributeName in testcase.attributes) {
+ div.setAttribute(attributeName, testcase.attributes[attributeName]);
+ }
+ }
+ div.style.animation = "keyframes 100s";
+
+ const keyframes = div.getAnimations()[0].effect.getKeyframes();
+ const expectedKeyframes = testcase.expectedKeyframes;
+ assert_equals(keyframes.length, expectedKeyframes.length,
+ `keyframes.length should be ${ expectedKeyframes.length }`);
+
+ keyframes.forEach((keyframe, index) => {
+ const expectedKeyframe = expectedKeyframes[index];
+ assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset,
+ `computedOffset of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.computedOffset }`);
+ assert_equals(keyframe.alignContent, expectedKeyframe.alignContent,
+ `alignContent of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.alignContent }`);
+ });
+ }, testcase.explanation);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_unstyled.html b/dom/animation/test/mozilla/test_unstyled.html
new file mode 100644
index 0000000000..4724979c11
--- /dev/null
+++ b/dom/animation/test/mozilla/test_unstyled.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+div.pseudo::before {
+ animation: animation 1s;
+ content: 'content';
+}
+@keyframes animation {
+ to { opacity: 0 }
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// Tests for cases where we may not have style data for an element
+
+promise_test(async t => {
+ // Get a CSSPseudoElement
+ const div = addDiv(t, { class: 'pseudo' });
+ const cssAnim = document.getAnimations()[0];
+ const pseudoElem = cssAnim.effect.target;
+
+ // Drop pseudo from styles and flush styles
+ div.classList.remove('pseudo');
+ getComputedStyle(div, '::before').content;
+
+ // Try animating the pseudo's content attribute
+ const contentAnim = pseudoElem.animate(
+ { content: ['none', '"content"'] },
+ { duration: 100 * MS_PER_SEC, fill: 'both' }
+ );
+
+ // Check that the initial value is as expected
+ await contentAnim.ready;
+ assert_equals(getComputedStyle(div, '::before').content, 'none');
+
+ contentAnim.finish();
+
+ // Animating an obsolete pseudo element should NOT cause the pseudo element
+ // to be re-generated. That behavior might change in which case this test
+ // will need to be updated. The most important part of this test, however,
+ // is simply checking that nothing explodes if we try to animate such a
+ // pseudo element.
+
+ assert_equals(getComputedStyle(div, '::before').content, 'none');
+}, 'Animation on an obsolete pseudo element produces expected results');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/xhr_doc.html b/dom/animation/test/mozilla/xhr_doc.html
new file mode 100644
index 0000000000..b9fa57e3f5
--- /dev/null
+++ b/dom/animation/test/mozilla/xhr_doc.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<div id=test></div>
diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html
new file mode 100644
index 0000000000..265de8f0f5
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html
@@ -0,0 +1,123 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.currentTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 90 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.currentTime = 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to after phase
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not effected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html
new file mode 100644
index 0000000000..e56db5f23d
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html
@@ -0,0 +1,123 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.startTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.startTime = animation.timeline.currentTime - 90 * MS_PER_SEC
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.startTime = animation.timeline.currentTime - 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to after phase
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html
new file mode 100644
index 0000000000..8712072a51
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-effect.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for setting effects by using Animation.effect</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src='../testcommon.js'></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script type='text/javascript'>
+
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var anim = new Animation();
+ anim.effect = new KeyframeEffect(target,
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+}, 'After setting target effect on an animation with null effect, the ' +
+ 'animation still works');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginLeft = '10px';
+ var anim = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+
+ anim.effect = null;
+ assert_equals(getComputedStyle(target).marginLeft, '10px');
+}, 'After setting null target effect, the computed style of the target ' +
+ 'element becomes the initial value');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = new Animation();
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 20 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original computed style of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '20px',
+ 'new computed style of the target element');
+}, 'After setting the target effect from an existing animation, the computed ' +
+ 'style of the target effect should reflect the time of the updated ' +
+ 'animation.');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginTop = '-10px';
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '20px',
+ 'original margin-top of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '10px',
+ 'new margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '-10px',
+ 'new margin-top of the target element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and both animation effects target to the same ' +
+ 'element, the computed style of this element should reflect the time and ' +
+ 'effect of the animation that was set');
+
+test(function(t) {
+ var targetA = addDiv(t);
+ var targetB = addDiv(t);
+ targetB.style.marginLeft = '-10px';
+ var animA = targetA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = targetB.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(targetA).marginLeft, '50px',
+ 'original margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '20px',
+ 'original margin-left of the second element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(targetA).marginLeft, '10px',
+ 'new margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '-10px',
+ 'new margin-left of the second element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and these animation effects target to ' +
+ 'different elements, the computed styles of the two elements should ' +
+ 'reflect the time and effect of the animation that was set');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '50px' ] },
+ 100 * MS_PER_SEC);
+ animA.currentTime = 20 * MS_PER_SEC;
+ animB.currentTime = 30 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '40px');
+ assert_equals(getComputedStyle(target).marginTop, '15px');
+
+ var effectA = animA.effect;
+ animA.effect = animB.effect;
+ animB.effect = effectA;
+ assert_equals(getComputedStyle(target).marginLeft, '60px');
+ assert_equals(getComputedStyle(target).marginTop, '10px');
+}, 'After swapping effects of two playing animations, both animations are ' +
+ 'still running with the same current time');
+
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/test_composite.html b/dom/animation/test/style/test_composite.html
new file mode 100644
index 0000000000..1383b1b1e6
--- /dev/null
+++ b/dom/animation/test/style/test_composite.html
@@ -0,0 +1,142 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 20px;
+ height: 20px;
+ background-color: white;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+ !SpecialPowers.getBoolPref(
+ 'layers.offmainthreadcomposition.async-animations')) {
+ // If OMTA is disabled, nothing to run.
+ done();
+}
+
+function waitForPaintsFlushed() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // Without this, the first test case fails on Android.
+ return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: ['translateX(0px)', 'translateX(200px)'],
+ composite: 'accumulate' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Accumulate onto the base value');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ div.animate({ transform: ['translateX(100px)', 'translateX(200px)'],
+ composite: 'replace' },
+ 100 * MS_PER_SEC);
+ div.animate({ transform: ['translateX(0px)', 'translateX(100px)'],
+ composite: 'accumulate' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Accumulate onto an underlying animation value');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
+ { transform: 'translateX(300px)', composite: 'replace' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Transform value at 50s');
+ });
+}, 'Composite when mixing accumulate and replace');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ transform: 'translateX(100px)', composite: 'replace' },
+ { transform: 'translateX(300px)' }],
+ { duration: 100 * MS_PER_SEC, composite: 'accumulate' });
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Composite specified on a keyframe overrides the composite mode of the ' +
+ 'effect');
+
+promise_test(t => {
+ var div;
+ var anim;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ div.animate({ transform: [ 'scale(2)', 'scale(2)' ] }, 100 * MS_PER_SEC);
+ anim = div.animate({ transform: [ 'scale(4)', 'scale(4)' ] },
+ { duration: 100 * MS_PER_SEC, composite: 'add' });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(8, 0, 0, 8, 0, 0)',
+ 'The additive scale value should be scale(8)'); // scale(2) scale(4)
+
+ anim.effect.composite = 'accumulate';
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(1);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(5, 0, 0, 5, 0, 0)',
+ // (scale(2 - 1) + scale(4 - 1) + scale(1))
+ 'The accumulate scale value should be scale(5)');
+ });
+}, 'Composite operation change');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html
new file mode 100644
index 0000000000..1da95392eb
--- /dev/null
+++ b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.transform = 'translateX(100px)';
+ target.style.transition = 'all 10s linear -5s';
+ getComputedStyle(target).transform;
+
+ target.style.transform = 'rotate(90deg)';
+ var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' +
+ Math.sin(Math.PI / 4) + ',' +
+ -Math.sin(Math.PI / 4) + ',' +
+ Math.cos(Math.PI / 4) + ',' +
+ '50, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform,
+ interpolated_matrix,
+ 'the equivalent matrix of ' + 'interpolatematrix(' +
+ 'translateX(100px), rotate(90deg), 0.5)');
+
+ // Trigger a new transition from
+ // interpolatematrix(translateX(100px), rotate(90deg), 0.5) to none
+ // with 'all 10s linear -5s'.
+ target.style.transform = 'none';
+ interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 8) + ',' +
+ Math.sin(Math.PI / 8) + ',' +
+ -Math.sin(Math.PI / 8) + ',' +
+ Math.cos(Math.PI / 8) + ',' +
+ '25, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform,
+ interpolated_matrix,
+ 'the expected matrix from interpolatematrix(' +
+ 'translateX(100px), rotate(90deg), 0.5) to none at 50%');
+}, 'Test interpolation from interpolatematrix to none at 50%');
+
+</script>
+</html>
diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
new file mode 100644
index 0000000000..8b92a89168
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
@@ -0,0 +1,577 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+ !SpecialPowers.getBoolPref(
+ 'layers.offmainthreadcomposition.async-animations')) {
+ // If OMTA is disabled, nothing to run.
+ done();
+}
+
+function waitForPaintsFlushed() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+// Note that promise tests run in sequence so this ensures the document is
+// loaded before any of the other tests run.
+promise_test(t => {
+ // Without this, the first test case fails on Android.
+ return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1' });
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.1',
+ 'The initial opacity value should be the base value');
+ });
+}, 'Initial opacity value for animation with no no keyframe at offset 0');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1' });
+ div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.5',
+ 'The initial opacity value should be the value of ' +
+ 'lower-priority animation value');
+ });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1; transition: opacity 100s linear' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = '0.5';
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.1',
+ 'The initial opacity value should be the initial value of ' +
+ 'the transition');
+ });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+ 'there is a transition on the same property');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0' });
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.5',
+ 'Opacity value at 50% should be composed onto the base ' +
+ 'value');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% ');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0' });
+ div.animate({ opacity: [ 0.5, 0.5 ] }, 100 * MS_PER_SEC);
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.75', // (0.5 + 1) * 0.5
+ 'Opacity value at 50% should be composed onto the value ' +
+ 'of middle of lower-priority animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0; transition: opacity 100s linear' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = '0.5';
+ getComputedStyle(div).opacity;
+
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.625', // ((0 + 0.5) * 0.5 + 1) * 0.5
+ 'Opacity value at 50% should be composed onto the value ' +
+ 'of middle of transition');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a transition on the same property');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.pause();
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority paused animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a paused underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.playbackRate = 0;
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority zero playback rate animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.effect.updateTiming({
+ duration: 0,
+ fill: 'forwards',
+ });
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is filling forwards state of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority zero active duration animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero active duration underlying animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: 'translateX(200px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The initial transform value should be the base value');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: [ 'translateX(200px)', 'translateX(300px)' ] },
+ 100 * MS_PER_SEC);
+ div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'The initial transform value should be lower-priority animation value');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px);' +
+ 'transition: transform 100s linear' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(200px)';
+ getComputedStyle(div).transform;
+
+ div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The initial transform value should be the initial value of the ' +
+ 'transition');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+ 'there is a transition');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ offset: 0, transform: 'translateX(200pX)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+ 'Transform value at 50% should be the base value');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50%');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: [ 'translateX(200px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'The final transform value should be the base value');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px);' +
+ 'transition: transform 100s linear' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(200px)';
+ getComputedStyle(div).transform;
+
+ div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * 0.5
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'The final transform value should be the final value of the transition');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a transition');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.pause();
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 100px.
+ // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority paused animation');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a paused underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.playbackRate = 0;
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 100px.
+ // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority zero playback rate animation');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ // We need to wait for a paint so that we can send the state of the lower
+ // animation that is actually finished at this point.
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (200px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 250px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ endDelay: -5 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ // We need to wait for a paint just like the above test.
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards and negative endDelay');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards and negative ' +
+ 'endDelay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ endDelay: 100 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (200px + 300px) * 0.5
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards during positive endDelay');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards during positive ' +
+ 'endDelay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: 'translateX(200px)' },
+ { duration: 100 * MS_PER_SEC, delay: 50 * MS_PER_SEC });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(100 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+ 'Transform value for animation with positive delay should be composed ' +
+ 'onto the base style');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 and with ' +
+ 'positive delay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+
+ div.animate([{ offset: 0, transform: 'translateX(200px)'}],
+ { duration: 100 * MS_PER_SEC,
+ iterationStart: 1,
+ iterationComposite: 'accumulate' });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 300, 0)',
+ 'Transform value for animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ lowerAnimation.timeline = null;
+ // Set current time at 50% duration.
+ lowerAnimation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation without timeline');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto an animation without timeline');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe.html
new file mode 100644
index 0000000000..4047e62408
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '100px',
+ 'The initial margin-left value should be the base value');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ div.animate([{ offset: 0, marginLeft: '200px' },
+ { offset: 1, marginLeft: '300px' }],
+ 100 * MS_PER_SEC);
+ div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '200px',
+ 'The initial margin-left value should be the initial value ' +
+ 'of lower-priority animation');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+ 'is that of lower-priority animations');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px;' +
+ 'transition: margin-left 100s -50s linear'});
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '200px';
+ flushComputedStyle(div);
+
+ div.animate([{ marginLeft: '300px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'The initial margin-left value should be the initial value ' +
+ 'of the transition');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+ 'is that of transition');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ var animation = div.animate([{ offset: 0, marginLeft: '200px' }],
+ 100 * MS_PER_SEC);
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'The margin-left value at 50% should be the base value');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ var lowerAnimation = div.animate([{ offset: 0, marginLeft: '200px' },
+ { offset: 1, marginLeft: '300px' }],
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate([{ offset: 0, marginLeft: '400px' }],
+ 100 * MS_PER_SEC);
+
+ lowerAnimation.currentTime = 50 * MS_PER_SEC;
+ higherAnimation.currentTime = 50 * MS_PER_SEC;
+ // (250px + 400px) * 0.5
+ assert_equals(getComputedStyle(div).marginLeft, '325px',
+ 'The margin-left value at 50% should be additive value of ' +
+ 'lower-priority animation and higher-priority animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+ 'is that of lower-priority animations');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px;' +
+ 'transition: margin-left 100s linear' });
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '300px';
+ flushComputedStyle(div);
+
+ div.animate([{ offset: 0, marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ div.getAnimations().forEach(animation => {
+ animation.currentTime = 50 * MS_PER_SEC;
+ });
+ // (200px + 200px) * 0.5
+ assert_equals(getComputedStyle(div).marginLeft, '200px',
+ 'The margin-left value at 50% should be additive value of ' +
+ 'the transition and animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+ 'is that of transition');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+
+ var animation = div.animate([{ offset: 0, marginLeft: '200px' }],
+ { duration: 100 * MS_PER_SEC,
+ iterationStart: 1,
+ iterationComposite: 'accumulate' });
+
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'The margin-left value should be additive value of the ' +
+ 'accumulation of the initial value onto the base value ');
+}, 'margin-left value for an animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html
new file mode 100644
index 0000000000..ad2584ac40
--- /dev/null
+++ b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.transform = 'rotate3d(0, 0, 1, 90deg)';
+ target.style.transition = 'all 10s linear -5s';
+ getComputedStyle(target).transform;
+
+ target.style.transform = 'rotate3d(0, 0, 0, 270deg)';
+ var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' +
+ Math.sin(Math.PI / 4) + ',' +
+ -Math.sin(Math.PI / 4) + ',' +
+ Math.cos(Math.PI / 4) + ',' +
+ '0, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform, interpolated_matrix,
+ 'transition from a normal rotate3d to a ' +
+ 'non-normalizable rotate3d');
+}, 'Test interpolation on non-normalizable rotate3d function');
+
+</script>
+</html>
diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
new file mode 100644
index 0000000000..e1645d4a19
--- /dev/null
+++ b/dom/animation/test/testcommon.js
@@ -0,0 +1,527 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Use this variable if you specify duration or some other properties
+ * for script animation.
+ * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ *
+ * NOTE: Creating animations with short duration may cause intermittent
+ * failures in asynchronous test. For example, the short duration animation
+ * might be finished when animation.ready has been fulfilled because of slow
+ * platforms or busyness of the main thread.
+ * Setting short duration to cancel its animation does not matter but
+ * if you don't want to cancel the animation, consider using longer duration.
+ */
+const MS_PER_SEC = 1000;
+
+/* The recommended minimum precision to use for time values[1].
+ *
+ * [1] https://drafts.csswg.org/web-animations/#precision-of-time-values
+ */
+var TIME_PRECISION = 0.0005; // ms
+
+/*
+ * Allow implementations to substitute an alternative method for comparing
+ * times based on their precision requirements.
+ */
+function assert_times_equal(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
+}
+
+/*
+ * Compare a time value based on its precision requirements with a fixed value.
+ */
+function assert_time_equals_literal(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+}
+
+/*
+ * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'.
+ * This function allows error, 0.01, because on Android when we are scaling down
+ * the document, it results in some errors.
+ */
+function assert_matrix_equals(actual, expected, description) {
+ var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/;
+ assert_regexp_match(actual, matrixRegExp, "Actual value should be a matrix");
+ assert_regexp_match(
+ expected,
+ matrixRegExp,
+ "Expected value should be a matrix"
+ );
+
+ var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number);
+ var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number);
+
+ assert_equals(
+ actualMatrixArray.length,
+ expectedMatrixArray.length,
+ "Array lengths should be equal (got '" +
+ expected +
+ "' and '" +
+ actual +
+ "'): " +
+ description
+ );
+ for (var i = 0; i < actualMatrixArray.length; i++) {
+ assert_approx_equals(
+ actualMatrixArray[i],
+ expectedMatrixArray[i],
+ 0.01,
+ "Matrix array should be equal (got '" +
+ expected +
+ "' and '" +
+ actual +
+ "'): " +
+ description
+ );
+ }
+}
+
+/**
+ * Compare given values which are same format of
+ * KeyframeEffectReadonly::GetProperties.
+ */
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ const compareProperties = (a, b) =>
+ a.property == b.property ? 0 : a.property < b.property ? -1 : 1;
+
+ const sortedActual = actual.sort(compareProperties);
+ const sortedExpected = expected.sort(compareProperties);
+
+ const serializeValues = values =>
+ values
+ .map(
+ value =>
+ "{ " +
+ ["offset", "value", "easing", "composite"]
+ .map(member => `${member}: ${value[member]}`)
+ .join(", ") +
+ " }"
+ )
+ .join(", ");
+
+ for (let i = 0; i < sortedActual.length; i++) {
+ assert_equals(
+ sortedActual[i].property,
+ sortedExpected[i].property,
+ "CSS property name should match"
+ );
+ assert_equals(
+ serializeValues(sortedActual[i].values),
+ serializeValues(sortedExpected[i].values),
+ `Values arrays do not match for ` + `${sortedActual[i].property} property`
+ );
+ }
+}
+
+/**
+ * Construct a object which is same to a value of
+ * KeyframeEffectReadonly::GetProperties().
+ * The method returns undefined as a value in case of missing keyframe.
+ * Therefor, we can use undefined for |value| and |easing| parameter.
+ * @param offset - keyframe offset. e.g. 0.1
+ * @param value - any keyframe value. e.g. undefined '1px', 'center', 0.5
+ * @param composite - 'replace', 'add', 'accumulate'
+ * @param easing - e.g. undefined, 'linear', 'ease' and so on
+ * @return Object -
+ * e.g. { offset: 0.1, value: '1px', composite: 'replace', easing: 'ease'}
+ */
+function valueFormat(offset, value, composite, easing) {
+ return { offset, value, easing, composite };
+}
+
+/**
+ * Appends a div to the document body and creates an animation on the div.
+ * NOTE: This function asserts when trying to create animations with durations
+ * shorter than 100s because the shorter duration may cause intermittent
+ * failures. If you are not sure how long it is suitable, use 100s; it's
+ * long enough but shorter than our test framework timeout (330s).
+ * If you really need to use shorter durations, use animate() function directly.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ * @param frames The keyframes passed to Element.animate().
+ * @param options The options passed to Element.animate().
+ */
+function addDivAndAnimate(t, attrs, frames, options) {
+ let animDur = typeof options === "object" ? options.duration : options;
+ assert_greater_than_equal(
+ animDur,
+ 100 * MS_PER_SEC,
+ "Clients of this addDivAndAnimate API must request a duration " +
+ "of at least 100s, to avoid intermittent failures from e.g." +
+ "the main thread being busy for an extended period"
+ );
+
+ return addDiv(t, attrs).animate(frames, options);
+}
+
+/**
+ * Appends a div to the document body.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ *
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ */
+function addDiv(t, attrs) {
+ var div = document.createElement("div");
+ if (attrs) {
+ for (var attrName in attrs) {
+ div.setAttribute(attrName, attrs[attrName]);
+ }
+ }
+ document.body.appendChild(div);
+ if (t && typeof t.add_cleanup === "function") {
+ t.add_cleanup(function () {
+ if (div.parentNode) {
+ div.remove();
+ }
+ });
+ }
+ return div;
+}
+
+/**
+ * Appends a style div to the document head.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the style element
+ * when the test finishes.
+ *
+ * @param rules A dictionary object with selector names and rules to set on
+ * the style sheet.
+ */
+function addStyle(t, rules) {
+ var extraStyle = document.createElement("style");
+ document.head.appendChild(extraStyle);
+ if (rules) {
+ var sheet = extraStyle.sheet;
+ for (var selector in rules) {
+ sheet.insertRule(
+ selector + "{" + rules[selector] + "}",
+ sheet.cssRules.length
+ );
+ }
+ }
+
+ if (t && typeof t.add_cleanup === "function") {
+ t.add_cleanup(function () {
+ extraStyle.remove();
+ });
+ }
+}
+
+/**
+ * Takes a CSS property (e.g. margin-left) and returns the equivalent IDL
+ * name (e.g. marginLeft).
+ */
+function propertyToIDL(property) {
+ var prefixMatch = property.match(/^-(\w+)-/);
+ if (prefixMatch) {
+ var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
+ property = prefix + property.substring(prefixMatch[0].length - 1);
+ }
+ // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+ return property.replace(/-([a-z])/gi, function (str, group) {
+ return group.toUpperCase();
+ });
+}
+
+/**
+ * Promise wrapper for requestAnimationFrame.
+ */
+function waitForFrame() {
+ return new Promise(function (resolve, reject) {
+ window.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Waits for a requestAnimationFrame callback in the next refresh driver tick.
+ * Note that the 'dom.animations-api.core.enabled' and
+ * 'dom.animations-api.timelines.enabled' prefs should be true to use this
+ * function.
+ */
+function waitForNextFrame(aWindow = window) {
+ const timeAtStart = aWindow.document.timeline.currentTime;
+ return new Promise(resolve => {
+ aWindow.requestAnimationFrame(() => {
+ if (timeAtStart === aWindow.document.timeline.currentTime) {
+ aWindow.requestAnimationFrame(resolve);
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Returns a Promise that is resolved after the given number of consecutive
+ * animation frames have occured (using requestAnimationFrame callbacks).
+ *
+ * @param aFrameCount The number of animation frames.
+ * @param aOnFrame An optional function to be processed in each animation frame.
+ * @param aWindow An optional window object to be used for requestAnimationFrame.
+ */
+function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) {
+ const timeAtStart = aWindow.document.timeline.currentTime;
+ return new Promise(function (resolve, reject) {
+ function handleFrame() {
+ if (aOnFrame && typeof aOnFrame === "function") {
+ aOnFrame();
+ }
+ if (
+ timeAtStart != aWindow.document.timeline.currentTime &&
+ --aFrameCount <= 0
+ ) {
+ resolve();
+ } else {
+ aWindow.requestAnimationFrame(handleFrame); // wait another frame
+ }
+ }
+ aWindow.requestAnimationFrame(handleFrame);
+ });
+}
+
+/**
+ * Promise wrapper for requestIdleCallback.
+ */
+function waitForIdle() {
+ return new Promise(resolve => {
+ requestIdleCallback(resolve);
+ });
+}
+
+/**
+ * Wrapper that takes a sequence of N animations and returns:
+ *
+ * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]);
+ */
+function waitForAllAnimations(animations) {
+ return Promise.all(
+ animations.map(function (animation) {
+ return animation.ready;
+ })
+ );
+}
+
+/**
+ * Flush the computed style for the given element. This is useful, for example,
+ * when we are testing a transition and need the initial value of a property
+ * to be computed so that when we synchronouslyet set it to a different value
+ * we actually get a transition instead of that being the initial value.
+ */
+function flushComputedStyle(elem) {
+ var cs = getComputedStyle(elem);
+ cs.marginLeft;
+}
+
+if (opener) {
+ for (var funcName of [
+ "async_test",
+ "assert_not_equals",
+ "assert_equals",
+ "assert_approx_equals",
+ "assert_less_than",
+ "assert_less_than_equal",
+ "assert_greater_than",
+ "assert_between_inclusive",
+ "assert_true",
+ "assert_false",
+ "assert_class_string",
+ "assert_throws",
+ "assert_unreached",
+ "assert_regexp_match",
+ "promise_test",
+ "test",
+ ]) {
+ if (opener[funcName]) {
+ window[funcName] = opener[funcName].bind(opener);
+ }
+ }
+
+ window.EventWatcher = opener.EventWatcher;
+
+ function done() {
+ opener.add_completion_callback(function () {
+ self.close();
+ });
+ opener.done();
+ }
+}
+
+/*
+ * Returns a promise that is resolved when the document has finished loading.
+ */
+function waitForDocumentLoad() {
+ return new Promise(function (resolve, reject) {
+ if (document.readyState === "complete") {
+ resolve();
+ } else {
+ window.addEventListener("load", resolve);
+ }
+ });
+}
+
+/*
+ * Enters test refresh mode, and restores the mode when |t| finishes.
+ */
+function useTestRefreshMode(t) {
+ function ensureNoSuppressedPaints() {
+ return new Promise(resolve => {
+ function checkSuppressedPaints() {
+ if (!SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(checkSuppressedPaints);
+ }
+ }
+ checkSuppressedPaints();
+ });
+ }
+
+ return ensureNoSuppressedPaints().then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+ t.add_cleanup(() => {
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+ });
+ });
+}
+
+/**
+ * Returns true if off-main-thread animations.
+ */
+function isOMTAEnabled() {
+ const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations";
+ return (
+ SpecialPowers.DOMWindowUtils.layerManagerRemote &&
+ SpecialPowers.getBoolPref(OMTAPrefKey)
+ );
+}
+
+/**
+ * Append an SVG element to the target element.
+ *
+ * @param target The element which want to append.
+ * @param attrs A array object with attribute name and values to set on
+ * the SVG element.
+ * @return An SVG outer element.
+ */
+function addSVGElement(target, tag, attrs) {
+ if (!target) {
+ return null;
+ }
+ var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
+ if (attrs) {
+ for (var attrName in attrs) {
+ element.setAttributeNS(null, attrName, attrs[attrName]);
+ }
+ }
+ target.appendChild(element);
+ return element;
+}
+
+/*
+ * Get Animation distance between two specified values for a specific property.
+ *
+ * @param target The target element.
+ * @param prop The CSS property.
+ * @param v1 The first property value.
+ * @param v2 The Second property value.
+ *
+ * @return The distance between |v1| and |v2| for |prop| on |target|.
+ */
+function getDistance(target, prop, v1, v2) {
+ if (!target) {
+ return 0.0;
+ }
+ return SpecialPowers.DOMWindowUtils.computeAnimationDistance(
+ target,
+ prop,
+ v1,
+ v2
+ );
+}
+
+/*
+ * A promise wrapper for waiting MozAfterPaint.
+ */
+function waitForPaints() {
+ // FIXME: Bug 1415065. Instead waiting for two requestAnimationFrames, we
+ // should wait for MozAfterPaint once after MozAfterPaint is fired properly
+ // (bug 1341294).
+ return waitForAnimationFrames(2);
+}
+
+// Returns true if |aAnimation| begins at the current timeline time. We
+// sometimes need to detect this case because if we started an animation
+// asynchronously (e.g. using play()) and then ended up running the next frame
+// at precisely the time the animation started (due to aligning with vsync
+// refresh rate) then we won't end up restyling in that frame.
+function animationStartsRightNow(aAnimation) {
+ return (
+ aAnimation.startTime === aAnimation.timeline.currentTime &&
+ aAnimation.currentTime === 0
+ );
+}
+
+// Waits for a given animation being ready to restyle.
+async function waitForAnimationReadyToRestyle(aAnimation) {
+ await aAnimation.ready;
+ // If |aAnimation| begins at the current timeline time, we will not process
+ // restyling in the initial frame because of aligning with the refresh driver,
+ // the animation frame in which the ready promise is resolved happens to
+ // coincide perfectly with the start time of the animation. In this case no
+ // restyling is needed in the frame so we have to wait one more frame.
+ if (animationStartsRightNow(aAnimation)) {
+ await waitForNextFrame(aAnimation.ownerGlobal);
+ }
+}
+
+function getDocShellForObservingRestylesForWindow(aWindow) {
+ const docShell = SpecialPowers.wrap(aWindow).docShell;
+
+ docShell.recordProfileTimelineMarkers = true;
+ docShell.popProfileTimelineMarkers();
+
+ return docShell;
+}
+
+// Returns the animation restyle markers observed during |frameCount| refresh
+// driver ticks in this `window`. This function is typically used to count the
+// number of restyles that take place as part of the style update that happens
+// on each refresh driver tick, as opposed to synchronous restyles triggered by
+// script.
+//
+// For the latter observeAnimSyncStyling (below) should be used.
+function observeStyling(frameCount, onFrame) {
+ return observeStylingInTargetWindow(window, frameCount, onFrame);
+}
+
+// As with observeStyling but applied to target window |aWindow|.
+function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) {
+ const docShell = getDocShellForObservingRestylesForWindow(aWindow);
+
+ return new Promise(resolve => {
+ return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => {
+ const markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ const stylingMarkers = Array.prototype.filter.call(
+ markers,
+ (marker, index) => {
+ return marker.name == "Styles" && marker.isAnimationOnly;
+ }
+ );
+ resolve(stylingMarkers);
+ });
+ });
+}