From cbefc77b36ed1f487f7071c808802181c5359259 Mon Sep 17 00:00:00 2001 From: roytam1 Date: Fri, 31 Dec 2021 09:22:44 +0800 Subject: [PATCH] import changes from `dev' branch of rmottola/Arctic-Fox: - Bug 1178664 - Part 1 - Make Animation interface EventTarget inheritance. r=smaug (0ea50ab93) - Bug 1178664 - Part 2 - webidl for AnimationPlaybackEvent. r=smaug (3a5d86f40) - Bug 1150808 - Implement Animation.reverse(). r=smaug r=birtles (9ce9fc4db) - Bug 1178664 - Part 3 - Implement Animation.onfinish event. r=bbirtles, r=smaug (9139c4227) - Bug 1178664 - Part 4 -Implement Animation.oncancel event. r=bbirtles, r=smaug (415b068e6) - Bug 1175751 - Apply playback rate to compositor animations. r=bbirtles (0ffd9cb90) - Bug 1181905 - Animation::IsPlaying should check playbackRate != 0 to stop playing on compositor animation. r=bbirtles (e3a2fd2f4) - Bug 1208385 part 0 - Fix up some references to Web Animations spec; r=heycam (fca9626d4) - Bug 1203009 part 1 - Rename sequence number to animation index; r=heycam (cd7dc513f) - Bug 1203009 part 2 - Remove {CSSAnimation,CSSTransition}::OwningElement() getter; r=heycam (4f49c63b1) - Bug 1203009 part 3 - Add mNeedsNewAnimationIndexWhenRun flag to CSSAnimation and CSSTransition; r=heycam (73a45b5a7) - Bug 1195523: Use type-safe LinkedList instead of PRCList to manage AnimationCollection objects. r=birtles (35b233981) - Bug 1194037 part 1 - Make nsAutoAnimationMutationBatch batch multiple elements at once; r=smaug (83f808043) - Bug 1194037 part 2 - Make WillRefresh no longer call FlushAnimations; r=dholbert (449b0fbd5) - Bug 1194037 part 3 - Add Animation::HasEndEventToQueue(); r=dholbert (b1ddc33d3) - ug 1203009 part 4 - Implement new composite ordering; r=heycam (4c571e608) - Bug 1203009 part 5 - Remove IsUsingCustomCompositeOrder; r=heycam (242d0142c) - Bug 1203009 part 6 - Add tests for new composite order; r=heycam (5f8711177) --- dom/animation/Animation.cpp | 141 ++++++++++----- dom/animation/Animation.h | 65 ++++--- dom/animation/KeyframeEffect.cpp | 5 +- .../file_animation-oncancel.html | 35 ++++ .../file_animation-onfinish.html | 142 +++++++++++++++ .../file_animation-reverse.html | 169 ++++++++++++++++++ .../file_timeline-get-animations.html | 50 ++++++ .../test_animation-oncancel.html | 15 ++ .../test_animation-onfinish.html | 15 ++ .../test_animation-reverse.html | 15 ++ dom/animation/test/mochitest.ini | 6 + dom/animation/test/testcommon.js | 12 ++ dom/base/nsDOMMutationObserver.cpp | 59 +++--- dom/base/nsDOMMutationObserver.h | 75 ++++---- dom/base/nsGkAtomList.h | 2 + .../test/test_all_synthetic_events.html | 4 + .../mochitest/general/test_interfaces.html | 2 + dom/webidl/Animation.webidl | 7 +- dom/webidl/AnimationPlaybackEvent.webidl | 24 +++ dom/webidl/moz.build | 1 + .../composite/AsyncCompositionManager.cpp | 3 +- gfx/layers/ipc/LayersMessages.ipdlh | 1 + layout/base/nsDisplayList.cpp | 1 + layout/style/AnimationCommon.cpp | 47 +++-- layout/style/AnimationCommon.h | 9 +- layout/style/nsAnimationManager.cpp | 49 +++-- layout/style/nsAnimationManager.h | 79 ++++---- layout/style/nsTransitionManager.cpp | 42 +++-- layout/style/nsTransitionManager.h | 86 +++++---- .../test/file_animations_playbackrate.html | 72 ++++++++ layout/style/test/mochitest.ini | 2 + .../test/test_animations_playbackrate.html | 28 +++ 32 files changed, 1013 insertions(+), 250 deletions(-) create mode 100644 dom/animation/test/css-animations/file_animation-oncancel.html create mode 100644 dom/animation/test/css-animations/file_animation-onfinish.html create mode 100644 dom/animation/test/css-animations/file_animation-reverse.html create mode 100644 dom/animation/test/css-animations/test_animation-oncancel.html create mode 100644 dom/animation/test/css-animations/test_animation-onfinish.html create mode 100644 dom/animation/test/css-animations/test_animation-reverse.html create mode 100644 dom/webidl/AnimationPlaybackEvent.webidl create mode 100644 layout/style/test/file_animations_playbackrate.html create mode 100644 layout/style/test/test_animations_playbackrate.html diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index 3b44290f9f..26acdfe27b 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -7,7 +7,9 @@ #include "Animation.h" #include "AnimationUtils.h" #include "mozilla/dom/AnimationBinding.h" +#include "mozilla/dom/AnimationPlaybackEvent.h" #include "mozilla/AutoRestore.h" +#include "mozilla/AsyncEventDispatcher.h" //For AsyncEventDispatcher #include "AnimationCommon.h" // For AnimationCollection, // CommonAnimationManager #include "nsIDocument.h" // For nsIDocument @@ -20,16 +22,19 @@ namespace mozilla { namespace dom { // Static members -uint64_t Animation::sNextSequenceNum = 0; +uint64_t Animation::sNextAnimationIndex = 0; -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Animation, mGlobal, mTimeline, - mEffect, mReady, mFinished) -NS_IMPL_CYCLE_COLLECTING_ADDREF(Animation) -NS_IMPL_CYCLE_COLLECTING_RELEASE(Animation) -NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Animation) - NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY - NS_INTERFACE_MAP_ENTRY(nsISupports) -NS_INTERFACE_MAP_END +NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper, + mTimeline, + mEffect, + mReady, + mFinished) + +NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Animation) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) JSObject* Animation::WrapObject(JSContext* aCx, JS::Handle aGivenProto) @@ -81,6 +86,7 @@ Animation::SetTimeline(AnimationTimeline* aTimeline) // (but *not* when this method gets called from style). } +// https://w3c.github.io/web-animations/#set-the-animation-start-time void Animation::SetStartTime(const Nullable& aNewStartTime) { @@ -116,7 +122,7 @@ Animation::SetStartTime(const Nullable& aNewStartTime) PostUpdate(); } -// http://w3c.github.io/web-animations/#current-time +// https://w3c.github.io/web-animations/#current-time Nullable Animation::GetCurrentTime() const { @@ -136,7 +142,7 @@ Animation::GetCurrentTime() const return result; } -// Implements http://w3c.github.io/web-animations/#set-the-current-time +// https://w3c.github.io/web-animations/#set-the-current-time void Animation::SetCurrentTime(const TimeDuration& aSeekTime) { @@ -157,6 +163,7 @@ Animation::SetCurrentTime(const TimeDuration& aSeekTime) PostUpdate(); } +// https://w3c.github.io/web-animations/#set-the-animation-playback-rate void Animation::SetPlaybackRate(double aPlaybackRate) { @@ -169,6 +176,7 @@ Animation::SetPlaybackRate(double aPlaybackRate) } } +// https://w3c.github.io/web-animations/#play-state AnimationPlayState Animation::PlayState() const { @@ -196,8 +204,9 @@ Animation::PlayState() const Promise* Animation::GetReady(ErrorResult& aRv) { - if (!mReady && mGlobal) { - mReady = Promise::Create(mGlobal, aRv); // Lazily create on demand + nsCOMPtr global = GetOwnerGlobal(); + if (!mReady && global) { + mReady = Promise::Create(global, aRv); // Lazily create on demand } if (!mReady) { aRv.Throw(NS_ERROR_FAILURE); @@ -210,8 +219,9 @@ Animation::GetReady(ErrorResult& aRv) Promise* Animation::GetFinished(ErrorResult& aRv) { - if (!mFinished && mGlobal) { - mFinished = Promise::Create(mGlobal, aRv); // Lazily create on demand + nsCOMPtr global = GetOwnerGlobal(); + if (!mFinished && global) { + mFinished = Promise::Create(global, aRv); // Lazily create on demand } if (!mFinished) { aRv.Throw(NS_ERROR_FAILURE); @@ -290,6 +300,23 @@ Animation::Pause(ErrorResult& aRv) PostUpdate(); } +// https://w3c.github.io/web-animations/#reverse-an-animation +void +Animation::Reverse(ErrorResult& aRv) +{ + if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mPlaybackRate == 0.0) { + return; + } + + SilentlySetPlaybackRate(-mPlaybackRate); + Play(aRv, LimitBehavior::AutoRewind); +} + // --------------------------------------------------------------------------- // // JS wrappers for Animation interface: @@ -425,7 +452,7 @@ Animation::GetCurrentOrPendingStartTime() const return result; } -// http://w3c.github.io/web-animations/#silently-set-the-current-time +// https://w3c.github.io/web-animations/#silently-set-the-current-time void Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) { @@ -458,6 +485,7 @@ Animation::SilentlySetPlaybackRate(double aPlaybackRate) } } +// https://w3c.github.io/web-animations/#cancel-an-animation void Animation::DoCancel() { @@ -473,6 +501,8 @@ Animation::DoCancel() } ResetFinishedPromise(); + DispatchPlaybackEvent(NS_LITERAL_STRING("cancel")); + mHoldTime.SetNull(); mStartTime.SetNull(); @@ -496,15 +526,15 @@ Animation::UpdateRelevance() bool Animation::HasLowerCompositeOrderThan(const Animation& aOther) const { - // We only ever sort non-idle animations so we don't ever expect - // mSequenceNum to be set to kUnsequenced - MOZ_ASSERT(mSequenceNum != kUnsequenced && - aOther.mSequenceNum != kUnsequenced, - "Animations to compare should not be idle"); - MOZ_ASSERT(mSequenceNum != aOther.mSequenceNum || &aOther == this, - "Sequence numbers should be unique"); + // Due to the way subclasses of this repurpose the mAnimationIndex to + // implement their own brand of composite ordering it is possible for + // two animations to have an identical mAnimationIndex member. + // However, these subclasses override this method so we shouldn't see + // identical animation indices here. + MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex || &aOther == this, + "Animation indices should be unique"); - return mSequenceNum < aOther.mSequenceNum; + return mAnimationIndex < aOther.mAnimationIndex; } bool @@ -553,7 +583,8 @@ Animation::ComposeStyle(nsRefPtr& aStyleRule, AnimationPlayState playState = PlayState(); if (playState == AnimationPlayState::Running || - playState == AnimationPlayState::Pending) { + playState == AnimationPlayState::Pending || + HasEndEventToQueue()) { aNeedsRefreshes = true; } @@ -639,7 +670,7 @@ Animation::NotifyEffectTimingUpdated() Animation::SyncNotifyFlag::Async); } -// http://w3c.github.io/web-animations/#play-an-animation +// https://w3c.github.io/web-animations/#play-an-animation void Animation::DoPlay(ErrorResult& aRv, LimitBehavior aLimitBehavior) { @@ -706,7 +737,7 @@ Animation::DoPlay(ErrorResult& aRv, LimitBehavior aLimitBehavior) UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); } -// http://w3c.github.io/web-animations/#pause-an-animation +// https://w3c.github.io/web-animations/#pause-an-animation void Animation::DoPause(ErrorResult& aRv) { @@ -812,16 +843,6 @@ Animation::PauseAt(const TimeDuration& aReadyTime) void Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) { - // Update the sequence number each time we transition in or out of the - // idle state - if (!IsUsingCustomCompositeOrder()) { - if (PlayState() == AnimationPlayState::Idle) { - mSequenceNum = kUnsequenced; - } else if (mSequenceNum == kUnsequenced) { - mSequenceNum = sNextSequenceNum++; - } - } - // We call UpdateFinishedState before UpdateEffect because the former // can change the current time, which is used by the latter. UpdateFinishedState(aSeekFlag, aSyncNotifyFlag); @@ -863,6 +884,7 @@ Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) } } +// https://w3c.github.io/web-animations/#update-an-animations-finished-state void Animation::UpdateFinishedState(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) @@ -1082,10 +1104,10 @@ void Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) { if (aSyncNotifyFlag == SyncNotifyFlag::Sync) { - MaybeResolveFinishedPromise(); + DoFinishNotificationImmediately(); } else if (!mFinishNotificationTask.IsPending()) { nsRefPtr> runnable = - NS_NewRunnableMethod(this, &Animation::MaybeResolveFinishedPromise); + NS_NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately); Promise::DispatchToMicroTask(runnable); mFinishNotificationTask = runnable; } @@ -1101,17 +1123,46 @@ Animation::ResetFinishedPromise() void Animation::MaybeResolveFinishedPromise() { - mFinishNotificationTask.Revoke(); - - if (PlayState() != AnimationPlayState::Finished) { - return; - } - if (mFinished) { mFinished->MaybeResolve(this); } mFinishedIsResolved = true; } +void +Animation::DoFinishNotificationImmediately() +{ + mFinishNotificationTask.Revoke(); + + if (PlayState() != AnimationPlayState::Finished) { + return; + } + + MaybeResolveFinishedPromise(); + + DispatchPlaybackEvent(NS_LITERAL_STRING("finish")); +} + +void +Animation::DispatchPlaybackEvent(const nsAString& aName) +{ + AnimationPlaybackEventInit init; + + if (aName.EqualsLiteral("finish")) { + init.mCurrentTime = GetCurrentTimeAsDouble(); + } + if (mTimeline) { + init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble(); + } + + nsRefPtr event = + AnimationPlaybackEvent::Constructor(this, aName, init); + event->SetTrusted(true); + + nsRefPtr asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->PostDOMEvent(); +} + } // namespace dom } // namespace mozilla diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h index a4525f5f36..d70c6e35da 100644 --- a/dom/animation/Animation.h +++ b/dom/animation/Animation.h @@ -13,6 +13,7 @@ #include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration #include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState #include "mozilla/dom/AnimationTimeline.h" // for AnimationTimeline +#include "mozilla/DOMEventTargetHelper.h" // for DOMEventTargetHelper #include "mozilla/dom/KeyframeEffect.h" // for KeyframeEffectReadOnly #include "mozilla/dom/Promise.h" // for Promise #include "nsCSSProperty.h" // for nsCSSProperty @@ -46,18 +47,17 @@ class CSSAnimation; class CSSTransition; class Animation - : public nsISupports - , public nsWrapperCache + : public DOMEventTargetHelper { protected: virtual ~Animation() {} public: explicit Animation(nsIGlobalObject* aGlobal) - : mGlobal(aGlobal) + : DOMEventTargetHelper(aGlobal) , mPlaybackRate(1.0) , mPendingState(PendingState::NotPending) - , mSequenceNum(kUnsequenced) + , mAnimationIndex(sNextAnimationIndex++) , mIsRunningOnCompositor(false) , mFinishedAtLastComposeStyle(false) , mIsRelevant(false) @@ -65,8 +65,9 @@ public: { } - NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Animation) + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation, + DOMEventTargetHelper) AnimationTimeline* GetParentObject() const { return mTimeline; } virtual JSObject* WrapObject(JSContext* aCx, @@ -107,7 +108,10 @@ public: virtual void Finish(ErrorResult& aRv); virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior); virtual void Pause(ErrorResult& aRv); + virtual void Reverse(ErrorResult& aRv); bool IsRunningOnCompositor() const { return mIsRunningOnCompositor; } + IMPL_EVENT_HANDLER(finish); + IMPL_EVENT_HANDLER(cancel); // Wrapper functions for Animation DOM methods when called // from script. @@ -245,13 +249,15 @@ public: * still running but we only consider it playing when it is in its active * interval. This definition is used for fetching the animations that are * candidates for running on the compositor (since we don't ship animations - * to the compositor when they are in their delay phase or paused). + * to the compositor when they are in their delay phase or paused including + * being effectively paused due to having a zero playback rate). */ bool IsPlaying() const { // We need to have an effect in its active interval, and - // be either running or waiting to run. + // be either running or waiting to run with a non-zero playback rate. return HasInPlayEffect() && + mPlaybackRate != 0.0 && (PlayState() == AnimationPlayState::Running || mPendingState == PendingState::PlayPending); } @@ -262,14 +268,6 @@ public: * Returns true if this Animation has a lower composite order than aOther. */ virtual bool HasLowerCompositeOrderThan(const Animation& aOther) const; - /** - * Returns true if this Animation is involved in some sort of - * custom composite ordering (such as the ordering defined for CSS - * animations or CSS transitions). - * - * When this is true, this class will not update the sequence number. - */ - virtual bool IsUsingCustomCompositeOrder() const { return false; } void SetIsRunningOnCompositor() { mIsRunningOnCompositor = true; } void ClearIsRunningOnCompositor() { mIsRunningOnCompositor = false; } @@ -292,6 +290,19 @@ public: nsCSSPropertySet& aSetProperties, bool& aNeedsRefreshes); + + // FIXME: Because we currently determine if we need refresh driver ticks + // during restyling (specifically ComposeStyle above) and not necessarily + // during a refresh driver tick, we can arrive at a situation where we + // have finished running an animation but are waiting until the next tick + // to queue the final end event. This method tells us when we are in that + // situation so we can avoid unregistering from the refresh driver until + // we've finished dispatching events. + // + // This is a temporary measure until bug 1195180 is done and we can do all + // our registering and unregistering within a tick callback. + virtual bool HasEndEventToQueue() const { return false; } + void NotifyEffectTimingUpdated(); protected: @@ -327,8 +338,8 @@ protected: Async }; - void UpdateTiming(SeekFlag aSeekFlag, - SyncNotifyFlag aSyncNotifyFlag); + virtual void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag); void UpdateFinishedState(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag); void UpdateEffect(); @@ -337,6 +348,8 @@ protected: void ResetFinishedPromise(); void MaybeResolveFinishedPromise(); void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag); + void DoFinishNotificationImmediately(); + void DispatchPlaybackEvent(const nsAString& aName); /** * Remove this animation from the pending animation tracker and reset @@ -353,7 +366,6 @@ protected: virtual CommonAnimationManager* GetAnimationManager() const = 0; AnimationCollection* GetCollection() const; - nsCOMPtr mGlobal; nsRefPtr mTimeline; nsRefPtr mEffect; // The beginning of the delay period. @@ -385,13 +397,16 @@ protected: enum class PendingState { NotPending, PlayPending, PausePending }; PendingState mPendingState; - static uint64_t sNextSequenceNum; - static const uint64_t kUnsequenced = UINT64_MAX; + static uint64_t sNextAnimationIndex; - // The sequence number assigned to this animation. This is kUnsequenced - // while the animation is in the idle state and is updated each time - // the animation transitions out of the idle state. - uint64_t mSequenceNum; + // The relative position of this animation within the global animation list. + // This is kNoIndex while the animation is in the idle state and is updated + // each time the animation transitions out of the idle state. + // + // 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; bool mIsRunningOnCompositor; bool mFinishedAtLastComposeStyle; diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp index 07107ecef0..1fde8838a8 100644 --- a/dom/animation/KeyframeEffect.cpp +++ b/dom/animation/KeyframeEffect.cpp @@ -243,7 +243,7 @@ KeyframeEffectReadOnly::ActiveDuration(const AnimationTiming& aTiming) aTiming.mIterationDuration.MultDouble(aTiming.mIterationCount)); } -// http://w3c.github.io/web-animations/#in-play +// https://w3c.github.io/web-animations/#in-play bool KeyframeEffectReadOnly::IsInPlay(const Animation& aAnimation) const { @@ -254,7 +254,7 @@ KeyframeEffectReadOnly::IsInPlay(const Animation& aAnimation) const return GetComputedTiming().mPhase == ComputedTiming::AnimationPhase_Active; } -// http://w3c.github.io/web-animations/#current +// https://w3c.github.io/web-animations/#current bool KeyframeEffectReadOnly::IsCurrent(const Animation& aAnimation) const { @@ -267,6 +267,7 @@ KeyframeEffectReadOnly::IsCurrent(const Animation& aAnimation) const computedTiming.mPhase == ComputedTiming::AnimationPhase_Active; } +// https://w3c.github.io/web-animations/#in-effect bool KeyframeEffectReadOnly::IsInEffect() const { diff --git a/dom/animation/test/css-animations/file_animation-oncancel.html b/dom/animation/test/css-animations/file_animation-oncancel.html new file mode 100644 index 0000000000..563ef00613 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-oncancel.html @@ -0,0 +1,35 @@ + + + + + + + diff --git a/dom/animation/test/css-animations/file_animation-onfinish.html b/dom/animation/test/css-animations/file_animation-onfinish.html new file mode 100644 index 0000000000..e69aa72a09 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-onfinish.html @@ -0,0 +1,142 @@ + + + + + + + diff --git a/dom/animation/test/css-animations/file_animation-reverse.html b/dom/animation/test/css-animations/file_animation-reverse.html new file mode 100644 index 0000000000..c217471cb0 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-reverse.html @@ -0,0 +1,169 @@ + + + + + + + diff --git a/dom/animation/test/css-animations/file_timeline-get-animations.html b/dom/animation/test/css-animations/file_timeline-get-animations.html index 33ef73d79a..a0357d0e0d 100644 --- a/dom/animation/test/css-animations/file_timeline-get-animations.html +++ b/dom/animation/test/css-animations/file_timeline-get-animations.html @@ -144,6 +144,54 @@ test(function(t) { }); }, 'Order of CSS Animations - across and within elements'); +test(function(t) { + cancelAllAnimationsOnEnd(t); + + var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' }); + var animLeft = document.timeline.getAnimations()[0]; + assert_equals(animLeft.animationName, 'animLeft', + 'Originally, animLeft animation comes first'); + + // Disassociate animLeft from markup and restart + div.style.animation = 'animTop 100s'; + animLeft.play(); + + var animations = document.timeline.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns markup-bound and free animations'); + assert_equals(animations[0].animationName, 'animTop', + 'Markup-bound animations come first'); + assert_equals(animations[1], animLeft, 'Free animations come last'); +}, 'Order of CSS Animations - markup-bound vs free animations'); + +test(function(t) { + cancelAllAnimationsOnEnd(t); + + var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' }); + var animLeft = document.timeline.getAnimations()[0]; + var animTop = document.timeline.getAnimations()[1]; + + // Disassociate both animations from markup and restart in opposite order + div.style.animation = ''; + animTop.play(); + animLeft.play(); + + var animations = document.timeline.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns free animations'); + assert_equals(animations[0], animTop, + 'Free animations are returned in the order they are started'); + assert_equals(animations[1], animLeft, + 'Animations started later are returned later'); + + // Restarting an animation should have no effect + animTop.cancel(); + animTop.play(); + assert_equals(document.timeline.getAnimations()[0], animTop, + 'After restarting, the ordering of free animations' + + ' does not change'); +}, 'Order of CSS Animations - free animations'); + test(function(t) { // Add an animation first var div = addDiv(t, { style: 'animation: animLeft 100s' }); @@ -191,6 +239,8 @@ test(function(t) { }, 'CSS Animations cancelled via the API are not returned'); test(function(t) { + cancelAllAnimationsOnEnd(t); + var div = addDiv(t, { style: 'animation: animLeft 100s' }); var anim = div.getAnimations()[0]; anim.cancel(); diff --git a/dom/animation/test/css-animations/test_animation-oncancel.html b/dom/animation/test/css-animations/test_animation-oncancel.html new file mode 100644 index 0000000000..94b5f92fb0 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-oncancel.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/css-animations/test_animation-onfinish.html b/dom/animation/test/css-animations/test_animation-onfinish.html new file mode 100644 index 0000000000..2e7d70e6fc --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-onfinish.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/css-animations/test_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse.html new file mode 100644 index 0000000000..673b1e0d3a --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-reverse.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini index 8c0afff5df..35c9b45963 100644 --- a/dom/animation/test/mochitest.ini +++ b/dom/animation/test/mochitest.ini @@ -12,6 +12,10 @@ support-files = css-animations/file_animation-currenttime.html support-files = css-animations/file_animation-finish.html [css-animations/test_animation-finished.html] support-files = css-animations/file_animation-finished.html +[css-animations/test_animation-oncancel.html] +support-files = css-animations/file_animation-oncancel.html +[css-animations/test_animation-onfinish.html] +support-files = css-animations/file_animation-onfinish.html [css-animations/test_animation-pausing.html] support-files = css-animations/file_animation-pausing.html [css-animations/test_animation-play.html] @@ -20,6 +24,8 @@ support-files = css-animations/file_animation-play.html support-files = css-animations/file_animation-playstate.html [css-animations/test_animation-ready.html] support-files = css-animations/file_animation-ready.html +[css-animations/test_animation-reverse.html] +support-files = css-animations/file_animation-reverse.html [css-animations/test_animation-starttime.html] support-files = css-animations/file_animation-starttime.html [css-animations/test_cssanimation-animationname.html] diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js index f3ba89edbc..289e039a24 100644 --- a/dom/animation/test/testcommon.js +++ b/dom/animation/test/testcommon.js @@ -17,6 +17,18 @@ function addDiv(t) { return div; } +/** + * Some tests cause animations to continue to exist even after their target + * element has been removed from the document tree. To ensure that these + * animations do not impact other tests we should cancel them when the test + * is complete. + */ +function cancelAllAnimationsOnEnd(t) { + t.add_cleanup(function() { + document.timeline.getAnimations().forEach(animation => animation.cancel()); + }); +} + /** * Promise wrapper for requestAnimationFrame. */ diff --git a/dom/base/nsDOMMutationObserver.cpp b/dom/base/nsDOMMutationObserver.cpp index 8df55c38a4..ef9e36c13a 100644 --- a/dom/base/nsDOMMutationObserver.cpp +++ b/dom/base/nsDOMMutationObserver.cpp @@ -362,19 +362,18 @@ nsAnimationReceiver::RecordAnimationMutation(Animation* aAnimation, } if (nsAutoAnimationMutationBatch::IsBatching()) { - if (nsAutoAnimationMutationBatch::GetBatchTarget() != animationTarget) { - return; - } - switch (aMutationType) { case eAnimationMutation_Added: - nsAutoAnimationMutationBatch::AnimationAdded(aAnimation); + nsAutoAnimationMutationBatch::AnimationAdded(aAnimation, + animationTarget); break; case eAnimationMutation_Changed: - nsAutoAnimationMutationBatch::AnimationChanged(aAnimation); + nsAutoAnimationMutationBatch::AnimationChanged(aAnimation, + animationTarget); break; case eAnimationMutation_Removed: - nsAutoAnimationMutationBatch::AnimationRemoved(aAnimation); + nsAutoAnimationMutationBatch::AnimationRemoved(aAnimation, + animationTarget); break; } @@ -1031,32 +1030,46 @@ nsAutoAnimationMutationBatch::Done() return; } - sCurrentBatch = mPreviousBatch; + sCurrentBatch = nullptr; if (mObservers.IsEmpty()) { nsDOMMutationObserver::LeaveMutationHandling(); // Nothing to do. return; } - for (nsDOMMutationObserver* ob : mObservers) { - nsRefPtr m = - new nsDOMMutationRecord(nsGkAtoms::animations, ob->GetParentObject()); - m->mTarget = mBatchTarget; + mBatchTargets.Sort(TreeOrderComparator()); - for (const Entry& e : mEntries) { - if (e.mState == eState_Added) { - m->mAddedAnimations.AppendElement(e.mAnimation); - } else if (e.mState == eState_Removed) { - m->mRemovedAnimations.AppendElement(e.mAnimation); - } else if (e.mState == eState_RemainedPresent && e.mChanged) { - m->mChangedAnimations.AppendElement(e.mAnimation); + for (nsDOMMutationObserver* ob : mObservers) { + bool didAddRecords = false; + + for (nsINode* target : mBatchTargets) { + EntryArray* entries = mEntryTable.Get(target); + MOZ_ASSERT(entries, + "Targets in entry table and targets list should match"); + + nsRefPtr m = + new nsDOMMutationRecord(nsGkAtoms::animations, ob->GetParentObject()); + m->mTarget = target; + + for (const Entry& e : *entries) { + if (e.mState == eState_Added) { + m->mAddedAnimations.AppendElement(e.mAnimation); + } else if (e.mState == eState_Removed) { + m->mRemovedAnimations.AppendElement(e.mAnimation); + } else if (e.mState == eState_RemainedPresent && e.mChanged) { + m->mChangedAnimations.AppendElement(e.mAnimation); + } + } + + if (!m->mAddedAnimations.IsEmpty() || + !m->mChangedAnimations.IsEmpty() || + !m->mRemovedAnimations.IsEmpty()) { + ob->AppendMutationRecord(m.forget()); + didAddRecords = true; } } - if (!m->mAddedAnimations.IsEmpty() || - !m->mChangedAnimations.IsEmpty() || - !m->mRemovedAnimations.IsEmpty()) { - ob->AppendMutationRecord(m.forget()); + if (didAddRecords) { ob->ScheduleForRun(); } } diff --git a/dom/base/nsDOMMutationObserver.h b/dom/base/nsDOMMutationObserver.h index 292823a378..5fe300cd85 100644 --- a/dom/base/nsDOMMutationObserver.h +++ b/dom/base/nsDOMMutationObserver.h @@ -724,20 +724,21 @@ class nsAutoAnimationMutationBatch struct Entry; public: - explicit nsAutoAnimationMutationBatch(nsINode* aTarget) - : mBatchTarget(nullptr) + explicit nsAutoAnimationMutationBatch(nsIDocument* aDocument) { - Init(aTarget); + Init(aDocument); } - void Init(nsINode* aTarget) + void Init(nsIDocument* aDocument) { - if (aTarget && aTarget->OwnerDoc()->MayHaveDOMMutationObservers()) { - mBatchTarget = aTarget; - mPreviousBatch = sCurrentBatch; - sCurrentBatch = this; - nsDOMMutationObserver::EnterMutationHandling(); + if (!aDocument || + !aDocument->MayHaveDOMMutationObservers() || + sCurrentBatch) { + return; } + + sCurrentBatch = this; + nsDOMMutationObserver::EnterMutationHandling(); } ~nsAutoAnimationMutationBatch() @@ -765,18 +766,14 @@ public: sCurrentBatch->mObservers.AppendElement(aObserver); } - static nsINode* GetBatchTarget() - { - return sCurrentBatch->mBatchTarget; - } - - static void AnimationAdded(mozilla::dom::Animation* aAnimation) + static void AnimationAdded(mozilla::dom::Animation* aAnimation, + nsINode* aTarget) { if (!IsBatching()) { return; } - Entry* entry = sCurrentBatch->FindEntry(aAnimation); + Entry* entry = sCurrentBatch->FindEntry(aAnimation, aTarget); if (entry) { switch (entry->mState) { case eState_RemainedAbsent: @@ -790,16 +787,16 @@ public: "twice"); } } else { - entry = sCurrentBatch->mEntries.AppendElement(); - entry->mAnimation = aAnimation; + entry = sCurrentBatch->AddEntry(aAnimation, aTarget); entry->mState = eState_Added; entry->mChanged = false; } } - static void AnimationChanged(mozilla::dom::Animation* aAnimation) + static void AnimationChanged(mozilla::dom::Animation* aAnimation, + nsINode* aTarget) { - Entry* entry = sCurrentBatch->FindEntry(aAnimation); + Entry* entry = sCurrentBatch->FindEntry(aAnimation, aTarget); if (entry) { NS_ASSERTION(entry->mState == eState_RemainedPresent || entry->mState == eState_Added, @@ -807,16 +804,16 @@ public: "being removed"); entry->mChanged = true; } else { - entry = sCurrentBatch->mEntries.AppendElement(); - entry->mAnimation = aAnimation; + entry = sCurrentBatch->AddEntry(aAnimation, aTarget); entry->mState = eState_RemainedPresent; entry->mChanged = true; } } - static void AnimationRemoved(mozilla::dom::Animation* aAnimation) + static void AnimationRemoved(mozilla::dom::Animation* aAnimation, + nsINode* aTarget) { - Entry* entry = sCurrentBatch->FindEntry(aAnimation); + Entry* entry = sCurrentBatch->FindEntry(aAnimation, aTarget); if (entry) { switch (entry->mState) { case eState_RemainedPresent: @@ -830,17 +827,21 @@ public: "twice"); } } else { - entry = sCurrentBatch->mEntries.AppendElement(); - entry->mAnimation = aAnimation; + entry = sCurrentBatch->AddEntry(aAnimation, aTarget); entry->mState = eState_Removed; entry->mChanged = false; } } private: - Entry* FindEntry(mozilla::dom::Animation* aAnimation) + Entry* FindEntry(mozilla::dom::Animation* aAnimation, nsINode* aTarget) { - for (Entry& e : mEntries) { + EntryArray* entries = mEntryTable.Get(aTarget); + if (!entries) { + return nullptr; + } + + for (Entry& e : *entries) { if (e.mAnimation == aAnimation) { return &e; } @@ -848,6 +849,17 @@ private: return nullptr; } + Entry* AddEntry(mozilla::dom::Animation* aAnimation, nsINode* aTarget) + { + EntryArray* entries = sCurrentBatch->mEntryTable.LookupOrAdd(aTarget); + if (entries->IsEmpty()) { + sCurrentBatch->mBatchTargets.AppendElement(aTarget); + } + Entry* entry = entries->AppendElement(); + entry->mAnimation = aAnimation; + return entry; + } + enum State { eState_RemainedPresent, eState_RemainedAbsent, @@ -863,10 +875,11 @@ private: }; static nsAutoAnimationMutationBatch* sCurrentBatch; - nsAutoAnimationMutationBatch* mPreviousBatch; nsAutoTArray mObservers; - nsTArray mEntries; - nsINode* mBatchTarget; + typedef nsTArray EntryArray; + nsClassHashtable, EntryArray> mEntryTable; + // List of nodes referred to by mEntryTable so we can sort them + nsTArray mBatchTargets; }; inline diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 50511bd504..e8821a0071 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -699,6 +699,7 @@ GK_ATOM(onbroadcast, "onbroadcast") GK_ATOM(onbusy, "onbusy") GK_ATOM(oncached, "oncached") GK_ATOM(oncallschanged, "oncallschanged") +GK_ATOM(oncancel, "oncancel") GK_ATOM(oncardstatechange, "oncardstatechange") GK_ATOM(oncfstatechange, "oncfstatechange") GK_ATOM(onchange, "onchange") @@ -773,6 +774,7 @@ GK_ATOM(onevicted, "onevicted") GK_ATOM(onfacesdetected, "onfacesdetected") GK_ATOM(onfailed, "onfailed") GK_ATOM(onfetch, "onfetch") +GK_ATOM(onfinish, "onfinish") GK_ATOM(onfocus, "onfocus") GK_ATOM(onfrequencychange, "onfrequencychange") GK_ATOM(onspeakerforcedchange, "onspeakerforcedchange") diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html index 9cf5fc6ac2..5d63c64e01 100644 --- a/dom/events/test/test_all_synthetic_events.html +++ b/dom/events/test/test_all_synthetic_events.html @@ -32,6 +32,10 @@ const kEventConstructors = { return new AnimationEvent(aName, aProps); }, }, + AnimationPlaybackEvent: { create: function (aName, aProps) { + return new AnimationPlaybackEvent(aName, aProps); + }, + }, AudioProcessingEvent: { create: null, // Cannot create untrusted event from JS. }, BeforeAfterKeyboardEvent: { create: function (aName, aProps) { diff --git a/dom/tests/mochitest/general/test_interfaces.html b/dom/tests/mochitest/general/test_interfaces.html index a0b617b306..e693cbcb4f 100644 --- a/dom/tests/mochitest/general/test_interfaces.html +++ b/dom/tests/mochitest/general/test_interfaces.html @@ -139,6 +139,8 @@ var interfaceNamesInGlobalScope = {name: "AnimationEffectReadOnly", release: false}, // IMPORTANT: Do not change this list without review from a DOM peer! "AnimationEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "AnimationPlaybackEvent", // IMPORTANT: Do not change this list without review from a DOM peer! {name: "AnimationTimeline", release: false}, // IMPORTANT: Do not change this list without review from a DOM peer! diff --git a/dom/webidl/Animation.webidl b/dom/webidl/Animation.webidl index 9c5bc13308..6ca9d42535 100644 --- a/dom/webidl/Animation.webidl +++ b/dom/webidl/Animation.webidl @@ -13,7 +13,7 @@ enum AnimationPlayState { "idle", "pending", "running", "paused", "finished" }; [Func="nsDocument::IsWebAnimationsEnabled"] -interface Animation { +interface Animation : EventTarget { // Bug 1049975: Make 'effect' writeable [Pure] readonly attribute AnimationEffectReadOnly? effect; @@ -30,6 +30,8 @@ interface Animation { readonly attribute Promise ready; [Throws] readonly attribute Promise finished; + attribute EventHandler onfinish; + attribute EventHandler oncancel; void cancel (); [Throws] void finish (); @@ -37,9 +39,8 @@ interface Animation { void play (); [Throws, BinaryName="pauseFromJS"] void pause (); - /* + [Throws] void reverse (); - */ }; // Non-standard extensions diff --git a/dom/webidl/AnimationPlaybackEvent.webidl b/dom/webidl/AnimationPlaybackEvent.webidl new file mode 100644 index 0000000000..52f5f501a4 --- /dev/null +++ b/dom/webidl/AnimationPlaybackEvent.webidl @@ -0,0 +1,24 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. + * + * The origin of this IDL file is + * http://w3c.github.io/web-animations/#the-animationplaybackevent-interface + * + * Copyright © 2015 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C + * liability, trademark and document use rules apply. + */ + +[Func="nsDocument::IsWebAnimationsEnabled", + Constructor(DOMString type, + optional AnimationPlaybackEventInit eventInitDict)] +interface AnimationPlaybackEvent : Event { + readonly attribute double? currentTime; + readonly attribute double? timelineTime; +}; + +dictionary AnimationPlaybackEventInit : EventInit { + double? currentTime = null; + double? timelineTime = null; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 67a4043f4f..66e8168e92 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -738,6 +738,7 @@ if CONFIG['MOZ_B2G_FM']: ] GENERATED_EVENTS_WEBIDL_FILES = [ + 'AnimationPlaybackEvent.webidl', 'AutocompleteErrorEvent.webidl', 'BlobEvent.webidl', 'CallEvent.webidl', diff --git a/gfx/layers/composite/AsyncCompositionManager.cpp b/gfx/layers/composite/AsyncCompositionManager.cpp index cff7045835..0b5f2f011d 100644 --- a/gfx/layers/composite/AsyncCompositionManager.cpp +++ b/gfx/layers/composite/AsyncCompositionManager.cpp @@ -460,7 +460,8 @@ SampleAnimations(Layer* aLayer, TimeStamp aPoint) MOZ_ASSERT(!animation.startTime().IsNull(), "Failed to resolve start time of pending animations"); - TimeDuration elapsedDuration = aPoint - animation.startTime(); + TimeDuration elapsedDuration = + (aPoint - animation.startTime()).MultDouble(animation.playbackRate()); // Skip animations that are yet to start. // // Currently, this should only happen when the refresh driver is under test diff --git a/gfx/layers/ipc/LayersMessages.ipdlh b/gfx/layers/ipc/LayersMessages.ipdlh index cdae4364d6..824743e6bf 100644 --- a/gfx/layers/ipc/LayersMessages.ipdlh +++ b/gfx/layers/ipc/LayersMessages.ipdlh @@ -198,6 +198,7 @@ struct Animation { int32_t direction; nsCSSProperty property; AnimationData data; + float playbackRate; }; // Change a layer's attributes diff --git a/layout/base/nsDisplayList.cpp b/layout/base/nsDisplayList.cpp index e9945d2562..fd4de6aae2 100644 --- a/layout/base/nsDisplayList.cpp +++ b/layout/base/nsDisplayList.cpp @@ -389,6 +389,7 @@ AddAnimationForProperty(nsIFrame* aFrame, const AnimationProperty& aProperty, animation->iterationCount() = timing.mIterationCount; animation->direction() = timing.mDirection; animation->property() = aProperty.mProperty; + animation->playbackRate() = aAnimation->PlaybackRate(); animation->data() = aData; for (uint32_t segIdx = 0; segIdx < aProperty.mSegments.Length(); segIdx++) { diff --git a/layout/style/AnimationCommon.cpp b/layout/style/AnimationCommon.cpp index 75e026a4c6..f0d4a199e3 100644 --- a/layout/style/AnimationCommon.cpp +++ b/layout/style/AnimationCommon.cpp @@ -54,7 +54,6 @@ CommonAnimationManager::CommonAnimationManager(nsPresContext *aPresContext) : mPresContext(aPresContext) , mIsObservingRefreshDriver(false) { - PR_INIT_CLIST(&mElementCollections); } CommonAnimationManager::~CommonAnimationManager() @@ -82,17 +81,15 @@ CommonAnimationManager::AddElementCollection(AnimationCollection* aCollection) mIsObservingRefreshDriver = true; } - PR_INSERT_BEFORE(aCollection, &mElementCollections); + mElementCollections.insertBack(aCollection); } void CommonAnimationManager::RemoveAllElementCollections() { - while (!PR_CLIST_IS_EMPTY(&mElementCollections)) { - AnimationCollection* head = - static_cast(PR_LIST_HEAD(&mElementCollections)); - head->Destroy(); - } + while (AnimationCollection* head = mElementCollections.getFirst()) { + head->Destroy(); // Note: this removes 'head' from mElementCollections. + } } void @@ -121,10 +118,9 @@ CommonAnimationManager::MaybeStartOrStopObservingRefreshDriver() bool CommonAnimationManager::NeedsRefresh() const { - for (PRCList *l = PR_LIST_HEAD(&mElementCollections); - l != &mElementCollections; - l = PR_NEXT_LINK(l)) { - if (static_cast(l)->mNeedsRefreshes) { + for (const AnimationCollection* collection = mElementCollections.getFirst(); + collection; collection = collection->getNext()) { + if (collection->mNeedsRefreshes) { return true; } } @@ -280,11 +276,8 @@ CommonAnimationManager::AddStyleUpdatesTo(RestyleTracker& aTracker) { TimeStamp now = mPresContext->RefreshDriver()->MostRecentRefresh(); - PRCList* next = PR_LIST_HEAD(&mElementCollections); - while (next != &mElementCollections) { - AnimationCollection* collection = static_cast(next); - next = PR_NEXT_LINK(next); - + for (AnimationCollection* collection = mElementCollections.getFirst(); + collection; collection = collection->getNext()) { collection->EnsureStyleRuleFor(now); dom::Element* elementToRestyle = collection->GetElementToRestyle(); @@ -320,7 +313,7 @@ CommonAnimationManager::GetAnimations(dom::Element *aElement, nsCSSPseudoElements::Type aPseudoType, bool aCreateIfNeeded) { - if (!aCreateIfNeeded && PR_CLIST_IS_EMPTY(&mElementCollections)) { + if (!aCreateIfNeeded && mElementCollections.isEmpty()) { // Early return for the most common case. return nullptr; } @@ -367,11 +360,8 @@ void CommonAnimationManager::FlushAnimations(FlushFlags aFlags) { TimeStamp now = mPresContext->RefreshDriver()->MostRecentRefresh(); - for (PRCList *l = PR_LIST_HEAD(&mElementCollections); - l != &mElementCollections; - l = PR_NEXT_LINK(l)) { - AnimationCollection* collection = static_cast(l); - + for (AnimationCollection* collection = mElementCollections.getFirst(); + collection; collection = collection->getNext()) { if (collection->mStyleRuleRefreshTime == now) { continue; } @@ -380,7 +370,7 @@ CommonAnimationManager::FlushAnimations(FlushFlags aFlags) collection->RequestRestyle(AnimationCollection::RestyleType::Standard); } - nsAutoAnimationMutationBatch mb(collection->mElement); + nsAutoAnimationMutationBatch mb(collection->mElement->OwnerDoc()); collection->Tick(); } @@ -458,7 +448,14 @@ CommonAnimationManager::WillRefresh(TimeStamp aTime) return; } - FlushAnimations(Can_Throttle); + nsAutoAnimationMutationBatch mb(mPresContext->Document()); + + for (AnimationCollection* collection = mElementCollections.getFirst(); + collection; collection = collection->getNext()) { + collection->Tick(); + } + + MaybeStartOrStopObservingRefreshDriver(); } #ifdef DEBUG @@ -784,7 +781,7 @@ AnimationCollection::PropertyDtor(void *aObject, nsIAtom *aPropertyName, collection->mCalledPropertyDtor = true; #endif { - nsAutoAnimationMutationBatch mb(collection->mElement); + nsAutoAnimationMutationBatch mb(collection->mElement->OwnerDoc()); for (size_t animIdx = collection->mAnimations.Length(); animIdx-- != 0; ) { collection->mAnimations[animIdx]->CancelFromStyle(); diff --git a/layout/style/AnimationCommon.h b/layout/style/AnimationCommon.h index 7bff7471ed..460ea9f377 100644 --- a/layout/style/AnimationCommon.h +++ b/layout/style/AnimationCommon.h @@ -9,11 +9,11 @@ #include "nsIStyleRuleProcessor.h" #include "nsIStyleRule.h" #include "nsRefreshDriver.h" -#include "prclist.h" #include "nsChangeHint.h" #include "nsCSSProperty.h" #include "nsDisplayList.h" // For nsDisplayItem::Type #include "mozilla/EventDispatcher.h" +#include "mozilla/LinkedList.h" #include "mozilla/MemoryReporting.h" #include "mozilla/StyleAnimationValue.h" #include "mozilla/dom/Animation.h" @@ -178,7 +178,7 @@ public: GetAnimationCollection(const nsIFrame* aFrame); protected: - PRCList mElementCollections; + LinkedList mElementCollections; nsPresContext *mPresContext; // weak (non-null from ctor to Disconnect) bool mIsObservingRefreshDriver; }; @@ -233,7 +233,7 @@ private: typedef InfallibleTArray> AnimationPtrArray; -struct AnimationCollection : public PRCList +struct AnimationCollection : public LinkedListElement { AnimationCollection(dom::Element *aElement, nsIAtom *aElementProperty, CommonAnimationManager *aManager) @@ -249,14 +249,13 @@ struct AnimationCollection : public PRCList #endif { MOZ_COUNT_CTOR(AnimationCollection); - PR_INIT_CLIST(this); } ~AnimationCollection() { MOZ_ASSERT(mCalledPropertyDtor, "must call destructor through element property dtor"); MOZ_COUNT_DTOR(AnimationCollection); - PR_REMOVE_LINK(this); + remove(); mManager->ElementCollectionRemoved(); } diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp index 199dfc5510..13078e5db9 100644 --- a/layout/style/nsAnimationManager.cpp +++ b/layout/style/nsAnimationManager.cpp @@ -141,28 +141,24 @@ CSSAnimation::HasLowerCompositeOrderThan(const Animation& aOther) const return false; } - // 2. CSS animations using custom composite ordering (i.e. those that - // correspond to an animation-name property) sort lower than other CSS - // animations (e.g. those created or kept-alive by script). - if (!IsUsingCustomCompositeOrder()) { - return !aOther.IsUsingCustomCompositeOrder() ? + // 2. CSS animations that correspond to an animation-name property sort lower + // than other CSS animations (e.g. those created or kept-alive by script). + if (!IsTiedToMarkup()) { + return !otherAnimation->IsTiedToMarkup() ? Animation::HasLowerCompositeOrderThan(aOther) : false; } - if (!aOther.IsUsingCustomCompositeOrder()) { + if (!otherAnimation->IsTiedToMarkup()) { return true; } // 3. Sort by document order - MOZ_ASSERT(mOwningElement.IsSet() && otherAnimation->OwningElement().IsSet(), - "Animations using custom composite order should have an " - "owning element"); - if (!mOwningElement.Equals(otherAnimation->OwningElement())) { - return mOwningElement.LessThan(otherAnimation->OwningElement()); + if (!mOwningElement.Equals(otherAnimation->mOwningElement)) { + return mOwningElement.LessThan(otherAnimation->mOwningElement); } // 4. (Same element and pseudo): Sort by position in animation-name - return mSequenceNum < otherAnimation->mSequenceNum; + return mAnimationIndex < otherAnimation->mAnimationIndex; } void @@ -277,6 +273,21 @@ CSSAnimation::QueueEvents() owningPseudoType)); } +bool +CSSAnimation::HasEndEventToQueue() const +{ + if (!mEffect) { + return false; + } + + bool wasActive = mPreviousPhaseOrIteration != PREVIOUS_PHASE_BEFORE && + mPreviousPhaseOrIteration != PREVIOUS_PHASE_AFTER; + bool isActive = mEffect->GetComputedTiming().mPhase == + ComputedTiming::AnimationPhase_Active; + + return wasActive && !isActive; +} + CommonAnimationManager* CSSAnimation::GetAnimationManager() const { @@ -288,6 +299,18 @@ CSSAnimation::GetAnimationManager() const return context->AnimationManager(); } +void +CSSAnimation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) +{ + if (mNeedsNewAnimationIndexWhenRun && + PlayState() != AnimationPlayState::Idle) { + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = false; + } + + Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); +} + ////////////////////////// nsAnimationManager //////////////////////////// NS_IMPL_CYCLE_COLLECTION(nsAnimationManager, mEventDispatcher) @@ -363,7 +386,7 @@ nsAnimationManager::CheckAnimationRule(nsStyleContext* aStyleContext, return nullptr; } - nsAutoAnimationMutationBatch mb(aElement); + nsAutoAnimationMutationBatch mb(aElement->OwnerDoc()); // build the animations list dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); diff --git a/layout/style/nsAnimationManager.h b/layout/style/nsAnimationManager.h index 1dcd35eb54..8029279b08 100644 --- a/layout/style/nsAnimationManager.h +++ b/layout/style/nsAnimationManager.h @@ -61,6 +61,7 @@ public: , mAnimationName(aAnimationName) , mIsStylePaused(false) , mPauseShouldStick(false) + , mNeedsNewAnimationIndexWhenRun(false) , mPreviousPhaseOrIteration(PREVIOUS_PHASE_BEFORE) { // We might need to drop this assertion once we add a script-accessible @@ -95,59 +96,53 @@ public: void CancelFromStyle() override { mOwningElement = OwningElementRef(); + + // 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::CancelFromStyle(); - MOZ_ASSERT(mSequenceNum == kUnsequenced); } void Tick() override; void QueueEvents(); + bool HasEndEventToQueue() const override; bool IsStylePaused() const { return mIsStylePaused; } bool HasLowerCompositeOrderThan(const Animation& aOther) const override; - bool IsUsingCustomCompositeOrder() const override - { - return mOwningElement.IsSet(); - } void SetAnimationIndex(uint64_t aIndex) { - MOZ_ASSERT(IsUsingCustomCompositeOrder()); - mSequenceNum = aIndex; + MOZ_ASSERT(IsTiedToMarkup()); + mAnimationIndex = aIndex; } void CopyAnimationIndex(const CSSAnimation& aOther) { - MOZ_ASSERT(IsUsingCustomCompositeOrder() && - aOther.IsUsingCustomCompositeOrder()); - mSequenceNum = aOther.mSequenceNum; + MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup()); + mAnimationIndex = aOther.mAnimationIndex; } - // Returns the element or pseudo-element whose animation-name property - // this CSSAnimation corresponds to (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 returned owning element will return !IsSet(). - const OwningElementRef& OwningElement() const { return mOwningElement; } - // Sets the owning element which is used for determining the composite // order of CSSAnimation objects generated from CSS markup. // - // @see OwningElement() + // @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(); } // Is this animation currently in effect for the purposes of computing // mWinsInCascade. (In general, this can be computed from the timing @@ -162,12 +157,32 @@ protected: MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared " "before a CSS animation is destroyed"); } - virtual CommonAnimationManager* GetAnimationManager() const override; + + // Animation overrides + CommonAnimationManager* GetAnimationManager() const override; + void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) override; nsString 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 combining animation-play-state with play() / pause() the following @@ -222,6 +237,10 @@ protected: bool mIsStylePaused; bool mPauseShouldStick; + // When true, indicates that when this animation next leaves the idle state, + // its animation index should be updated. + bool mNeedsNewAnimationIndexWhenRun; + enum { PREVIOUS_PHASE_BEFORE = uint64_t(-1), PREVIOUS_PHASE_AFTER = uint64_t(-2) diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 70c1445e5c..cfc20feda7 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -112,6 +112,18 @@ CSSTransition::GetAnimationManager() const return context->TransitionManager(); } +void +CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) +{ + if (mNeedsNewAnimationIndexWhenRun && + PlayState() != AnimationPlayState::Idle) { + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = false; + } + + Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); +} + void CSSTransition::QueueEvents() { @@ -141,6 +153,17 @@ CSSTransition::QueueEvents() owningPseudoType)); } +bool +CSSTransition::HasEndEventToQueue() const +{ + if (!mEffect) { + return false; + } + + return !mWasFinishedOnLastTick && + PlayState() == AnimationPlayState::Finished; +} + void CSSTransition::Tick() { @@ -176,26 +199,23 @@ CSSTransition::HasLowerCompositeOrderThan(const Animation& aOther) const // 2. CSS transitions that correspond to a transition-property property sort // lower than CSS transitions owned by script. - if (!IsUsingCustomCompositeOrder()) { - return !aOther.IsUsingCustomCompositeOrder() ? + if (!IsTiedToMarkup()) { + return !otherTransition->IsTiedToMarkup() ? Animation::HasLowerCompositeOrderThan(aOther) : false; } - if (!aOther.IsUsingCustomCompositeOrder()) { + if (!otherTransition->IsTiedToMarkup()) { return true; } // 3. Sort by document order - MOZ_ASSERT(mOwningElement.IsSet() && otherTransition->OwningElement().IsSet(), - "Transitions using custom composite order should have an owning " - "element"); - if (!mOwningElement.Equals(otherTransition->OwningElement())) { - return mOwningElement.LessThan(otherTransition->OwningElement()); + if (!mOwningElement.Equals(otherTransition->mOwningElement)) { + return mOwningElement.LessThan(otherTransition->mOwningElement); } // 4. (Same element and pseudo): Sort by transition generation - if (mSequenceNum != otherTransition->mSequenceNum) { - return mSequenceNum < otherTransition->mSequenceNum; + if (mAnimationIndex != otherTransition->mAnimationIndex) { + return mAnimationIndex < otherTransition->mAnimationIndex; } // 5. (Same transition generation): Sort by transition property @@ -329,7 +349,7 @@ nsTransitionManager::StyleContextChanged(dom::Element *aElement, afterChangeStyle = newStyleContext; } - nsAutoAnimationMutationBatch mb(aElement); + nsAutoAnimationMutationBatch mb(aElement->OwnerDoc()); // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html // I'll consider only the transitions from the number of items in diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 5c3b9910b9..b77b387095 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -85,6 +85,7 @@ public: explicit CSSTransition(nsIGlobalObject* aGlobal) : dom::Animation(aGlobal) , mWasFinishedOnLastTick(false) + , mNeedsNewAnimationIndexWhenRun(false) { } @@ -114,8 +115,17 @@ public: void CancelFromStyle() override { mOwningElement = OwningElementRef(); + + // 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::CancelFromStyle(); - MOZ_ASSERT(mSequenceNum == kUnsequenced); } void Tick() override; @@ -123,20 +133,44 @@ public: nsCSSProperty TransitionProperty() const; bool HasLowerCompositeOrderThan(const Animation& aOther) const override; - bool IsUsingCustomCompositeOrder() const override - { - return mOwningElement.IsSet(); - } - void SetCreationSequence(uint64_t aIndex) { - MOZ_ASSERT(IsUsingCustomCompositeOrder()); - mSequenceNum = aIndex; + MOZ_ASSERT(IsTiedToMarkup()); + mAnimationIndex = aIndex; } - // Returns the element or pseudo-element whose transition-property property - // this CSSTransition corresponds to (if any). This is used for determining - // the relative composite order of transitions generated from CSS markup. + // 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(); } + +protected: + virtual ~CSSTransition() + { + MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared " + "before a CSS transition is destroyed"); + } + + // Animation overrides + CommonAnimationManager* GetAnimationManager() const override; + void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) override; + + void QueueEvents(); + bool HasEndEventToQueue() const override; + + // 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 @@ -148,34 +182,14 @@ public: // c) If this object is generated from script using the CSSTransition // constructor. // - // For (b) and (c) the returned owning element will return !IsSet(). - const OwningElementRef& OwningElement() const { return mOwningElement; } - - // Sets the owning element which is used for determining the composite - // oder of CSSTransition objects generated from CSS markup. - // - // @see OwningElement() - void SetOwningElement(const OwningElementRef& aElement) - { - mOwningElement = aElement; - } - -protected: - virtual ~CSSTransition() - { - MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared " - "before a CSS transition is destroyed"); - } - - virtual CommonAnimationManager* GetAnimationManager() const override; - - void QueueEvents(); - - // The (pseudo-)element whose computed transition-property refers to this - // transition (if any). + // For (b) and (c) the owning element will return !IsSet(). OwningElementRef mOwningElement; bool mWasFinishedOnLastTick; + + // When true, indicates that when this transition next leaves the idle state, + // its animation index should be updated. + bool mNeedsNewAnimationIndexWhenRun; }; } // namespace dom diff --git a/layout/style/test/file_animations_playbackrate.html b/layout/style/test/file_animations_playbackrate.html new file mode 100644 index 0000000000..951ef5eeec --- /dev/null +++ b/layout/style/test/file_animations_playbackrate.html @@ -0,0 +1,72 @@ + + + + + + + + + +
+ + + diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index 535d42c9ad..9563b2a99e 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -42,6 +42,8 @@ skip-if = toolkit == 'android' skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # bug 1041017 [test_animations_pausing.html] support-files = file_animations_pausing.html +[test_animations_playbackrate.html] +support-files = file_animations_playbackrate.html [test_any_dynamic.html] [test_at_rule_parse_serialize.html] [test_bug73586.html] diff --git a/layout/style/test/test_animations_playbackrate.html b/layout/style/test/test_animations_playbackrate.html new file mode 100644 index 0000000000..16deca02c1 --- /dev/null +++ b/layout/style/test/test_animations_playbackrate.html @@ -0,0 +1,28 @@ + + + + + Test for Animation.playbackRate on compositor animations (Bug 1175751) + + + + +Mozilla Bug 1175751 +
+
+
+
+ +