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)
This commit is contained in:
2021-12-31 09:22:44 +08:00
parent de52ef4061
commit cbefc77b36
32 changed files with 1013 additions and 250 deletions
+96 -45
View File
@@ -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<JSObject*> 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<TimeDuration>& aNewStartTime)
{
@@ -116,7 +122,7 @@ Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime)
PostUpdate();
}
// http://w3c.github.io/web-animations/#current-time
// https://w3c.github.io/web-animations/#current-time
Nullable<TimeDuration>
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<nsIGlobalObject> 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<nsIGlobalObject> 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<AnimValuesStyleRule>& 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<nsRunnableMethod<Animation>> 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<AnimationPlaybackEvent> event =
AnimationPlaybackEvent::Constructor(this, aName, init);
event->SetTrusted(true);
nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
new AsyncEventDispatcher(this, event);
asyncDispatcher->PostDOMEvent();
}
} // namespace dom
} // namespace mozilla
+40 -25
View File
@@ -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<nsIGlobalObject> mGlobal;
nsRefPtr<AnimationTimeline> mTimeline;
nsRefPtr<KeyframeEffectReadOnly> 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;
+3 -2
View File
@@ -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
{
@@ -0,0 +1,35 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes abc {
to { transform: translate(10px) }
}
</style>
<body>
<script>
'use strict';
async_test(function(t) {
var div = addDiv(t, {'style': 'animation: abc 100s'});
var animation = div.getAnimations()[0];
var finishedTimelineTime;
animation.finished.then().catch(function() {
finishedTimelineTime = animation.timeline.currentTime;
});
animation.oncancel = t.step_func_done(function(event) {
assert_equals(event.currentTime, null,
'event.currentTime should be null');
assert_equals(event.timelineTime, finishedTimelineTime,
'event.timelineTime should equal to the animation timeline ' +
'when finished promise is rejected');
});
animation.cancel();
}, 'oncancel event is fired when animation.cancel()');
done();
</script>
</body>
@@ -0,0 +1,142 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes abc {
to { transform: translate(10px) }
}
</style>
<body>
<script>
'use strict';
const ANIM_PROP_VAL = 'abc 100s';
const ANIM_DURATION = 100000; // ms
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var finishedTimelineTime;
animation.finished.then(function() {
finishedTimelineTime = animation.timeline.currentTime;
});
animation.onfinish = t.step_func_done(function(event) {
assert_equals(event.currentTime, 0,
'event.currentTime should be zero');
assert_equals(event.timelineTime, finishedTimelineTime,
'event.timelineTime should equal to the animation timeline ' +
'when finished promise is resolved');
});
animation.playbackRate = -1;
}, 'onfinish event is fired when the currentTime < 0 and ' +
'the playbackRate < 0');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var finishedTimelineTime;
animation.finished.then(function() {
finishedTimelineTime = animation.timeline.currentTime;
});
animation.onfinish = t.step_func_done(function(event) {
assert_equals(event.currentTime, ANIM_DURATION,
'event.currentTime should be the effect end');
assert_equals(event.timelineTime, finishedTimelineTime,
'event.timelineTime should equal to the animation timeline ' +
'when finished promise is resolved');
});
animation.currentTime = ANIM_DURATION;
}, 'onfinish event is fired when the currentTime > 0 and ' +
'the playbackRate > 0');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var finishedTimelineTime;
animation.finished.then(function() {
finishedTimelineTime = animation.timeline.currentTime;
});
animation.onfinish = t.step_func_done(function(event) {
assert_equals(event.currentTime, ANIM_DURATION,
'event.currentTime should be the effect end');
assert_equals(event.timelineTime, finishedTimelineTime,
'event.timelineTime should equal to the animation timeline ' +
'when finished promise is resolved');
});
animation.finish();
}, 'onfinish event is fired when animation.finish() is called');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.onfinish = t.step_func(function(event) {
assert_unreached('onfinish event should not be fired');
});
animation.currentTime = ANIM_DURATION / 2;
animation.pause();
animation.ready.then(t.step_func(function() {
animation.currentTime = ANIM_DURATION;
return waitForAnimationFrames(2);
})).then(t.step_func(function() {
t.done();
}));
}, 'onfinish event is not fired when paused');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.onfinish = t.step_func(function(event) {
assert_unreached('onfinish event should not be fired');
});
animation.ready.then(function() {
animation.playbackRate = 0;
animation.currentTime = ANIM_DURATION;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
t.done();
}));
}, 'onfinish event is not fired when the playbackRate is zero');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.onfinish = t.step_func(function(event) {
assert_unreached('onfinish event should not be fired');
});
animation.ready.then(function() {
animation.currentTime = ANIM_DURATION;
animation.currentTime = ANIM_DURATION / 2;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
t.done();
}));
}, 'onfinish event is not fired when the animation falls out ' +
'finished state immediately');
done();
</script>
</body>
@@ -0,0 +1,169 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim {
to { transform: translate(100px) }
}
</style>
<body>
<script>
'use strict';
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s infinite' });
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func_done(function() {
var previousPlaybackRate = animation.playbackRate;
animation.reverse();
assert_equals(animation.playbackRate, -previousPlaybackRate,
'playbackRate should be invetrted');
}));
}, 'reverse() inverts playbackRate');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s infinite' });
var animation = div.getAnimations()[0];
animation.currentTime = 50000;
animation.pause();
animation.ready.then(t.step_func(function() {
animation.reverse();
return animation.ready;
})).then(t.step_func_done(function() {
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" after reverse()');
}));
}, 'reverse() starts to play when pausing animation');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
div.style.animation = "";
flushComputedStyle(div);
assert_equals(animation.currentTime, null);
animation.reverse();
assert_equals(animation.currentTime, 100000,
'animation.currentTime should be its effect end');
t.done();
}, 'reverse() from idle state starts playing the animation');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.currentTime = 50000;
animation.reverse();
assert_equals(animation.currentTime, 50000,
'reverse() should not change the currentTime ' +
'if the currentTime is in the middle of animation duration');
t.done();
}, 'reverse() maintains the same currentTime');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.currentTime = 200000;
animation.reverse();
assert_equals(animation.currentTime, 100000,
'reverse() should start playing from the animation effect end ' +
'if the playbackRate > 0 and the currentTime > effect end');
t.done();
}, 'reverse() when playbackRate > 0 and currentTime > effect end');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.currentTime = -200000;
animation.reverse();
assert_equals(animation.currentTime, 100000,
'reverse() should start playing from the animation effect end ' +
'if the playbackRate > 0 and the currentTime < 0');
t.done();
}, 'reverse() when playbackRate > 0 and currentTime < 0');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.playbackRate = -1;
animation.currentTime = -200000;
animation.reverse();
assert_equals(animation.currentTime, 0,
'reverse() should start playing from the start of animation time ' +
'if the playbackRate < 0 and the currentTime < 0');
t.done();
}, 'reverse() when playbackRate < 0 and currentTime < 0');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.playbackRate = -1;
animation.currentTime = 200000;
animation.reverse();
assert_equals(animation.currentTime, 0,
'reverse() should start playing from the start of animation time ' +
'if the playbackRate < 0 and the currentTime > effect end');
t.done();
}, 'reverse() when playbackRate < 0 and currentTime > effect end');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s infinite' });
var animation = div.getAnimations()[0];
animation.currentTime = -200000;
assert_throws('InvalidStateError',
function () { animation.reverse(); },
'reverse() should throw InvalidStateError ' +
'if the playbackRate > 0 and the currentTime < 0 ' +
'and the target effect is positive infinity');
t.done();
}, 'reverse() when playbackRate > 0 and currentTime < 0 ' +
'and the target effect is positive infinity');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s infinite' });
var animation = div.getAnimations()[0];
animation.playbackRate = -1;
animation.currentTime = -200000;
animation.reverse();
assert_equals(animation.currentTime, 0,
'reverse() should start playing from the start of animation time ' +
'if the playbackRate < 0 and the currentTime < 0 ' +
'and the target effect is positive infinity');
t.done();
}, 'reverse() when playbackRate < 0 and currentTime < 0 ' +
'and the target effect is positive infinity');
async_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.playbackRate = 0;
animation.currentTime = 50000;
animation.reverse();
assert_equals(animation.playbackRate, 0,
'reverse() should preserve playbackRate if the playbackRate == 0');
assert_equals(animation.currentTime, 50000,
'reverse() should not affect the currentTime if the playbackRate == 0');
t.done();
}, 'reverse() when playbackRate == 0');
done();
</script>
</body>
@@ -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();
@@ -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.core.enabled", true]]},
function() {
window.open("file_animation-oncancel.html");
});
</script>
</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.core.enabled", true]]},
function() {
window.open("file_animation-onfinish.html");
});
</script>
</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.core.enabled", true]]},
function() {
window.open("file_animation-reverse.html");
});
</script>
</html>
+6
View File
@@ -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]
+12
View File
@@ -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.
*/
+36 -23
View File
@@ -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<nsDOMMutationRecord> 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<nsDOMMutationRecord> 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();
}
}
+44 -31
View File
@@ -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<nsDOMMutationObserver*, 2> mObservers;
nsTArray<Entry> mEntries;
nsINode* mBatchTarget;
typedef nsTArray<Entry> EntryArray;
nsClassHashtable<nsPtrHashKey<nsINode>, EntryArray> mEntryTable;
// List of nodes referred to by mEntryTable so we can sort them
nsTArray<nsINode*> mBatchTargets;
};
inline
+2
View File
@@ -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")
@@ -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) {
@@ -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!
+4 -3
View File
@@ -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<Animation> ready;
[Throws]
readonly attribute Promise<Animation> 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
+24
View File
@@ -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;
};
+1
View File
@@ -738,6 +738,7 @@ if CONFIG['MOZ_B2G_FM']:
]
GENERATED_EVENTS_WEBIDL_FILES = [
'AnimationPlaybackEvent.webidl',
'AutocompleteErrorEvent.webidl',
'BlobEvent.webidl',
'CallEvent.webidl',
@@ -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
+1
View File
@@ -198,6 +198,7 @@ struct Animation {
int32_t direction;
nsCSSProperty property;
AnimationData data;
float playbackRate;
};
// Change a layer's attributes
+1
View File
@@ -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++) {
+22 -25
View File
@@ -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<AnimationCollection*>(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<AnimationCollection*>(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<AnimationCollection*>(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<AnimationCollection*>(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();
+4 -5
View File
@@ -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<AnimationCollection> mElementCollections;
nsPresContext *mPresContext; // weak (non-null from ctor to Disconnect)
bool mIsObservingRefreshDriver;
};
@@ -233,7 +233,7 @@ private:
typedef InfallibleTArray<nsRefPtr<dom::Animation>> AnimationPtrArray;
struct AnimationCollection : public PRCList
struct AnimationCollection : public LinkedListElement<AnimationCollection>
{
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();
}
+36 -13
View File
@@ -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();
+49 -30
View File
@@ -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)
+31 -11
View File
@@ -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
+50 -36
View File
@@ -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
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript"
src="/tests/SimpleTest/paint_listener.js"></script>
<script type="application/javascript" src="animation_utils.js"></script>
<style type="text/css">
@keyframes anim {
0% { transform: translate(0px) }
100% { transform: translate(100px) }
}
.target {
/* The animation target needs geometry in order to qualify for OMTA */
width: 100px;
height: 100px;
background-color: white;
}
</style>
<script>
var ok = opener.ok.bind(opener);
var is = opener.is.bind(opener);
var todo = opener.todo.bind(opener);
function finish() {
var o = opener;
self.close();
o.SimpleTest.finish();
}
</script>
</head>
<body>
<div id="display"></div>
<script type="application/javascript">
"use strict";
runOMTATest(function() {
runAllAsyncAnimTests().then(function() {
finish();
});
}, finish, opener.SpecialPowers);
addAsyncAnimTest(function *() {
var [ div, cs ] = new_div("animation: anim 10s 1 linear forwards");
var animation = div.getAnimations()[0];
animation.playbackRate = 10;
advance_clock(300);
yield waitForPaints();
omta_is(div, "transform", { tx: 30 }, RunningOn.Compositor,
"at 300ms");
done_div();
});
addAsyncAnimTest(function *() {
var [ div, cs ] = new_div("animation: anim 10s 1 linear forwards");
var animation = div.getAnimations()[0];
advance_clock(300);
yield waitForPaints();
animation.playbackRate = 0;
yield waitForPaintsFlushed();
omta_is(div, "transform", { tx: 3 }, RunningOn.MainThread,
"animation with zero playback rate should stay in the " +
"same position and be running on the main thread");
done_div();
});
</script>
</body>
</html>
+2
View File
@@ -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]
@@ -0,0 +1,28 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1175751
-->
<head>
<title>Test for Animation.playbackRate on compositor animations (Bug 1175751)</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175751">Mozilla Bug 1175751</a>
<div id="display"></div>
<pre id="test">
<script type="application/javascript">
"use strict";
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv(
{ "set": [[ "dom.animations-api.core.enabled", true]] },
function() {
window.open("file_animations_playbackrate.html");
});
</script>
</pre>
</body>
</html>