From 78696876a238ebf0a49a6ea2cfd57a27561beeee Mon Sep 17 00:00:00 2001 From: Jonathan Watt Date: Wed, 18 Mar 2015 13:22:11 +0000 Subject: [PATCH] Bug 1074630, part 1 - Implement Web Animations finishing behavior. r=birtles, r=smaug --- dom/animation/AnimationPlayer.cpp | 167 ++++++++++++++++++++++------ dom/animation/AnimationPlayer.h | 30 ++++- dom/webidl/AnimationPlayer.webidl | 3 +- layout/style/nsAnimationManager.cpp | 6 +- layout/style/nsAnimationManager.h | 2 +- layout/style/nsTransitionManager.h | 2 +- 6 files changed, 161 insertions(+), 49 deletions(-) diff --git a/dom/animation/AnimationPlayer.cpp b/dom/animation/AnimationPlayer.cpp index 7a30ab9a331..fb64ecf503c 100644 --- a/dom/animation/AnimationPlayer.cpp +++ b/dom/animation/AnimationPlayer.cpp @@ -17,7 +17,7 @@ namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationPlayer, mTimeline, - mSource, mReady) + mSource, mReady, mFinished) NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationPlayer) NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationPlayer) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationPlayer) @@ -68,12 +68,8 @@ AnimationPlayer::SetStartTime(const Nullable& aNewStartTime) mReady->MaybeResolve(this); } - UpdateSourceContent(); + UpdateTiming(); PostUpdate(); - - // FIXME: Once bug 1074630 is fixed, run the procedure to update a player's - // finished state for player: - // http://w3c.github.io/web-animations/#update-a-players-finished-state } Nullable @@ -113,8 +109,7 @@ AnimationPlayer::SilentlySetCurrentTime(const TimeDuration& aSeekTime) (aSeekTime / mPlaybackRate)); } - // Once AnimationPlayers store a previous current time, set that to - // unresolved. + mPreviousCurrentTime.SetNull(); } // Implements http://w3c.github.io/web-animations/#set-the-current-time @@ -130,12 +125,9 @@ AnimationPlayer::SetCurrentTime(const TimeDuration& aSeekTime) } } + UpdateFinishedState(true); UpdateSourceContent(); PostUpdate(); - - // FIXME: Once bug 1074630 is fixed, run the procedure to update a player's - // finished state for player: - // http://w3c.github.io/web-animations/#update-a-players-finished-state } void @@ -186,36 +178,56 @@ AnimationPlayer::PlayState() const return AnimationPlayState::Running; } +static inline already_AddRefed +CreatePromise(AnimationTimeline* aTimeline, ErrorResult& aRv) +{ + nsIGlobalObject* global = aTimeline->GetParentObject(); + if (global) { + return Promise::Create(global, aRv); + } + return nullptr; +} + Promise* AnimationPlayer::GetReady(ErrorResult& aRv) { - // Lazily create the ready promise if it doesn't exist if (!mReady) { - nsIGlobalObject* global = mTimeline->GetParentObject(); - if (global) { - mReady = Promise::Create(global, aRv); - if (mReady && PlayState() != AnimationPlayState::Pending) { - mReady->MaybeResolve(this); - } - } + mReady = CreatePromise(mTimeline, aRv); // Lazily create on demand } if (!mReady) { aRv.Throw(NS_ERROR_FAILURE); + } else if (PlayState() != AnimationPlayState::Pending) { + mReady->MaybeResolve(this); } - return mReady; } -void -AnimationPlayer::Play() +Promise* +AnimationPlayer::GetFinished(ErrorResult& aRv) { - DoPlay(); + if (!mFinished) { + mFinished = CreatePromise(mTimeline, aRv); // Lazily create on demand + } + if (!mFinished) { + aRv.Throw(NS_ERROR_FAILURE); + } else if (IsFinished()) { + mFinished->MaybeResolve(this); + } + return mFinished; +} + +void +AnimationPlayer::Play(LimitBehavior aLimitBehavior) +{ + DoPlay(aLimitBehavior); PostUpdate(); } void AnimationPlayer::Pause() { + // TODO: The DoPause() call should not be synchronous (bug 1109390). See + // http://w3c.github.io/web-animations/#pausing-an-animation-section DoPause(); PostUpdate(); } @@ -285,7 +297,7 @@ AnimationPlayer::Tick() ResumeAt(mTimeline->GetCurrentTime().Value()); } - UpdateSourceContent(); + UpdateTiming(); } void @@ -345,6 +357,12 @@ AnimationPlayer::Cancel() } } + if (mFinished) { + mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + // Clear finished promise. We'll create a new one lazily. + mFinished = nullptr; + mHoldTime.SetNull(); mStartTime.SetNull(); @@ -407,16 +425,12 @@ AnimationPlayer::ComposeStyle(nsRefPtr& aStyleRule, mSource->ComposeStyle(aStyleRule, aSetProperties); - mIsPreviousStateFinished = (playState == AnimationPlayState::Finished); + //XXXjwatt mIsPreviousStateFinished = (playState == AnimationPlayState::Finished); } void -AnimationPlayer::DoPlay() +AnimationPlayer::DoPlay(LimitBehavior aLimitBehavior) { - // FIXME: When we implement finishing behavior (bug 1074630) we will - // need to pass a flag so that when we start playing due to a change in - // animation-play-state we *don't* trigger finishing behavior. - bool reuseReadyPromise = false; if (mPendingState != PendingState::NotPending) { CancelPendingTasks(); @@ -425,10 +439,16 @@ AnimationPlayer::DoPlay() Nullable currentTime = GetCurrentTime(); if (mPlaybackRate > 0.0 && - (currentTime.IsNull())) { + (currentTime.IsNull() || + (aLimitBehavior == LimitBehavior::AutoRewind && + (currentTime.Value().ToMilliseconds() < 0.0 || + currentTime.Value() >= SourceContentEnd())))) { mHoldTime.SetValue(TimeDuration(0)); } else if (mPlaybackRate < 0.0 && - (currentTime.IsNull())) { + (currentTime.IsNull() || + (aLimitBehavior == LimitBehavior::AutoRewind && + (currentTime.Value().ToMilliseconds() <= 0.0 || + currentTime.Value() > SourceContentEnd())))) { mHoldTime.SetValue(TimeDuration(SourceContentEnd())); } else if (mPlaybackRate == 0.0 && currentTime.IsNull()) { mHoldTime.SetValue(TimeDuration(0)); @@ -457,9 +477,8 @@ AnimationPlayer::DoPlay() PendingPlayerTracker* tracker = doc->GetOrCreatePendingPlayerTracker(); tracker->AddPlayPending(*this); - // We may have updated the current time when we set the hold time above - // so notify source content. - UpdateSourceContent(); + // We may have updated the current time when we set the hold time above. + UpdateTiming(); } void @@ -484,6 +503,8 @@ AnimationPlayer::DoPause() // Bug 1109390 - check for null result here and go to pending state mHoldTime = GetCurrentTime(); mStartTime.SetNull(); + + UpdateFinishedState(); } void @@ -506,13 +527,72 @@ AnimationPlayer::ResumeAt(const TimeDuration& aResumeTime) } mPendingState = PendingState::NotPending; - UpdateSourceContent(); + UpdateTiming(); if (mReady) { mReady->MaybeResolve(this); } } +void +AnimationPlayer::UpdateTiming() +{ + // We call UpdateFinishedState before UpdateSourceContent because the former + // can change the current time, which is used by the latter. + UpdateFinishedState(); + UpdateSourceContent(); +} + +void +AnimationPlayer::UpdateFinishedState(bool aSeekFlag) +{ + Nullable currentTime = GetCurrentTime(); + TimeDuration targetEffectEnd = TimeDuration(SourceContentEnd()); + + if (!mStartTime.IsNull() && + mPendingState == PendingState::NotPending) { + if (mPlaybackRate > 0.0 && + !currentTime.IsNull() && + currentTime.Value() >= targetEffectEnd) { + if (aSeekFlag) { + mHoldTime = currentTime; + } else if (!mPreviousCurrentTime.IsNull()) { + mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), + targetEffectEnd)); + } else { + mHoldTime.SetValue(targetEffectEnd); + } + } else if (mPlaybackRate < 0.0 && + !currentTime.IsNull() && + currentTime.Value().ToMilliseconds() <= 0.0) { + if (aSeekFlag) { + mHoldTime = currentTime; + } else { + mHoldTime.SetValue(0); + } + } else if (mPlaybackRate != 0.0 && + !currentTime.IsNull()) { + if (aSeekFlag && !mHoldTime.IsNull()) { + mStartTime.SetValue(mTimeline->GetCurrentTime().Value() - + (mHoldTime.Value() / mPlaybackRate)); + } + mHoldTime.SetNull(); + } + } + + bool currentFinishedState = IsFinished(); + if (currentFinishedState && !mIsPreviousStateFinished) { + if (mFinished) { + mFinished->MaybeResolve(this); + } + } else if (!currentFinishedState && mIsPreviousStateFinished) { + // Clear finished promise. We'll create a new one lazily. + mFinished = nullptr; + } + mIsPreviousStateFinished = currentFinishedState; + mPreviousCurrentTime = currentTime; +} + void AnimationPlayer::UpdateSourceContent() { @@ -563,6 +643,19 @@ AnimationPlayer::CancelPendingTasks() mPendingReadyTime.SetNull(); } +bool +AnimationPlayer::IsFinished() const +{ + // Unfortunately there's some weirdness in the spec at the moment where if + // you're finished and paused, the playState is paused. This prevents us + // from just checking |PlayState() == AnimationPlayState::Finished| here, + // and we need this much more messy check to see if we're finished. + Nullable currentTime = GetCurrentTime(); + return !currentTime.IsNull() && + ((mPlaybackRate > 0.0 && currentTime.Value() >= SourceContentEnd()) || + (mPlaybackRate < 0.0 && currentTime.Value().ToMilliseconds() <= 0.0)); +} + bool AnimationPlayer::IsPossiblyOrphanedPendingPlayer() const { diff --git a/dom/animation/AnimationPlayer.h b/dom/animation/AnimationPlayer.h index 19b2d475ff0..94088afe167 100644 --- a/dom/animation/AnimationPlayer.h +++ b/dom/animation/AnimationPlayer.h @@ -70,6 +70,13 @@ public: virtual CSSAnimationPlayer* AsCSSAnimationPlayer() { return nullptr; } virtual CSSTransitionPlayer* AsCSSTransitionPlayer() { return nullptr; } + // Flag to pass to DoPlay to indicate that it should not carry out finishing + // behavior (reset the current time to the beginning of the active duration). + enum LimitBehavior { + AutoRewind = 0, + Continue = 1 + }; + // AnimationPlayer methods Animation* GetSource() const { return mSource; } AnimationTimeline* Timeline() const { return mTimeline; } @@ -83,7 +90,8 @@ public: void SilentlySetPlaybackRate(double aPlaybackRate); AnimationPlayState PlayState() const; virtual Promise* GetReady(ErrorResult& aRv); - virtual void Play(); + virtual Promise* GetFinished(ErrorResult& aRv); + virtual void Play(LimitBehavior aLimitBehavior); virtual void Pause(); bool IsRunningOnCompositor() const { return mIsRunningOnCompositor; } @@ -97,7 +105,7 @@ public: void SetCurrentTimeAsDouble(const Nullable& aCurrentTime, ErrorResult& aRv); virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); } - virtual void PlayFromJS() { Play(); } + virtual void PlayFromJS() { Play(LimitBehavior::AutoRewind); } // PauseFromJS is currently only here for symmetry with PlayFromJS but // in future we will likely have to flush style in // CSSAnimationPlayer::PauseFromJS so we leave it for now. @@ -252,10 +260,12 @@ public: bool& aNeedsRefreshes); protected: - void DoPlay(); + void DoPlay(LimitBehavior aLimitBehavior); void DoPause(); void ResumeAt(const TimeDuration& aResumeTime); + void UpdateTiming(); + void UpdateFinishedState(bool aSeekFlag = false); void UpdateSourceContent(); void FlushStyle() const; void PostUpdate(); @@ -266,6 +276,8 @@ protected: */ void CancelPendingTasks(); + bool IsFinished() const; + bool IsPossiblyOrphanedPendingPlayer() const; StickyTimeDuration SourceContentEnd() const; @@ -280,13 +292,22 @@ protected: Nullable mStartTime; // Timeline timescale Nullable mHoldTime; // Player timescale Nullable mPendingReadyTime; // Timeline timescale + Nullable mPreviousCurrentTime; // Player timescale double mPlaybackRate; // A Promise that is replaced on each call to Play() (and in future Pause()) // and fulfilled when Play() is successfully completed. // This object is lazily created by GetReady. + // See http://w3c.github.io/web-animations/#current-ready-promise nsRefPtr mReady; + // A Promise that is resolved when we reach the end of the source content, or + // 0 when playing backwards. The Promise is replaced if the animation is + // finished but then a state change makes it not finished. + // This object is lazily created by GetFinished. + // See http://w3c.github.io/web-animations/#current-finished-promise + nsRefPtr mFinished; + // Indicates if the player is in the pending state (and what state it is // waiting to enter when it finished pending). We use this rather than // checking if this player is tracked by a PendingPlayerTracker because the @@ -299,9 +320,6 @@ protected: bool mIsRunningOnCompositor; // Indicates whether we were in the finished state during our // most recent unthrottled sample (our last ComposeStyle call). - // FIXME: When we implement the finished promise (bug 1074630) we can - // probably remove this and check if the promise has been settled yet - // or not instead. bool mIsPreviousStateFinished; // Spec calls this "previous finished state" // Indicates that the player should be exposed in an element's // getAnimationPlayers() list. diff --git a/dom/webidl/AnimationPlayer.webidl b/dom/webidl/AnimationPlayer.webidl index 2e00fbd1978..6186eb4e6f3 100644 --- a/dom/webidl/AnimationPlayer.webidl +++ b/dom/webidl/AnimationPlayer.webidl @@ -29,8 +29,9 @@ interface AnimationPlayer { readonly attribute AnimationPlayState playState; [Throws] readonly attribute Promise ready; - /* + [Throws] readonly attribute Promise finished; + /* void cancel (); void finish (); */ diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp index 58f4ba88188..50c3fb8010b 100644 --- a/layout/style/nsAnimationManager.cpp +++ b/layout/style/nsAnimationManager.cpp @@ -35,10 +35,10 @@ CSSAnimationPlayer::GetReady(ErrorResult& aRv) } void -CSSAnimationPlayer::Play() +CSSAnimationPlayer::Play(LimitBehavior aLimitBehavior) { mPauseShouldStick = false; - AnimationPlayer::Play(); + AnimationPlayer::Play(aLimitBehavior); } void @@ -71,7 +71,7 @@ CSSAnimationPlayer::PlayFromStyle() { mIsStylePaused = false; if (!mPauseShouldStick) { - DoPlay(); + DoPlay(AnimationPlayer::LimitBehavior::Continue); } } diff --git a/layout/style/nsAnimationManager.h b/layout/style/nsAnimationManager.h index 7f873973273..4f9c9e77051 100644 --- a/layout/style/nsAnimationManager.h +++ b/layout/style/nsAnimationManager.h @@ -67,7 +67,7 @@ public: AsCSSAnimationPlayer() override { return this; } virtual dom::Promise* GetReady(ErrorResult& aRv) override; - virtual void Play() override; + virtual void Play(LimitBehavior aLimitBehavior) override; virtual void Pause() override; virtual dom::AnimationPlayState PlayStateFromJS() const override; diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 53e77fb2749..2326cbf6355 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -81,7 +81,7 @@ public: // A variant of Play() that avoids posting style updates since this method // is expected to be called whilst already updating style. - void PlayFromStyle() { DoPlay(); } + void PlayFromStyle() { DoPlay(AnimationPlayer::LimitBehavior::Continue); } protected: virtual ~CSSTransitionPlayer() { }