# HG changeset patch # User Brian Birtles # Date 1518501858 -32400 # Node ID 3fc608432c52da556e179d5a36ce15f1a8a031a7 # Parent 97ae9bf0af3f6180300b321179971f8328b52320 Bug 1436659 - Implement pending playback rate mechanism; r=hiro This reflects the following changes to the Web Animations specification: 1. https://github.com/w3c/csswg-drafts/commit/5af5e276badf4df0271bcfa0b8e7837fff24133a 2. https://github.com/w3c/csswg-drafts/commit/673f6fc1269829743c707c53dcb04092f958de35 which can be viewed as a merged diff at: https://gist.github.com/birtles/d147eb2e0e2d4d37fadf217abd709411 MozReview-Commit-ID: 3DoaWUkxBTo diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -268,17 +268,20 @@ Animation::SetStartTime(const NullableGetCurrentTime(); } if (timelineTime.IsNull() && !aNewStartTime.IsNull()) { mHoldTime.SetNull(); } Nullable previousCurrentTime = GetCurrentTime(); + + ApplyPendingPlaybackRate(); mStartTime = aNewStartTime; + if (!aNewStartTime.IsNull()) { if (mPlaybackRate != 0.0) { mHoldTime.SetNull(); } } else { mHoldTime = previousCurrentTime; } @@ -332,35 +335,39 @@ Animation::SetCurrentTime(const TimeDura AutoMutationBatchForAnimation mb(*this); SilentlySetCurrentTime(aSeekTime); if (mPendingState == PendingState::PausePending) { // Finish the pause operation mHoldTime.SetValue(aSeekTime); + + ApplyPendingPlaybackRate(); mStartTime.SetNull(); if (mReady) { mReady->MaybeResolve(this); } CancelPendingTasks(); } UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); if (IsRelevant()) { nsNodeUtils::AnimationChanged(this); } PostUpdate(); } -// https://drafts.csswg.org/web-animations/#set-the-animation-playback-rate +// https://drafts.csswg.org/web-animations/#set-the-playback-rate void Animation::SetPlaybackRate(double aPlaybackRate) { + mPendingPlaybackRate.reset(); + if (aPlaybackRate == mPlaybackRate) { return; } AutoMutationBatchForAnimation mb(*this); Nullable previousTime = GetCurrentTime(); mPlaybackRate = aPlaybackRate; @@ -378,16 +385,93 @@ Animation::SetPlaybackRate(double aPlayb // - update the playback rate on animations on layers. UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); if (IsRelevant()) { nsNodeUtils::AnimationChanged(this); } PostUpdate(); } +// https://drafts.csswg.org/web-animations/#seamlessly-update-the-playback-rate +void +Animation::UpdatePlaybackRate(double aPlaybackRate) +{ + if (mPendingPlaybackRate && mPendingPlaybackRate.value() == aPlaybackRate) { + return; + } + + mPendingPlaybackRate = Some(aPlaybackRate); + + // If we already have a pending task, there is nothing more to do since the + // playback rate will be applied then. + if (Pending()) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + AnimationPlayState playState = PlayState(); + if (playState == AnimationPlayState::Idle || + playState == AnimationPlayState::Paused) { + // We are either idle or paused. In either case we can apply the pending + // playback rate immediately. + ApplyPendingPlaybackRate(); + + // We don't need to update timing or post an update here because: + // + // * the current time hasn't changed -- it's either unresolved or fixed + // with a hold time -- so the output won't have changed + // * the finished state won't have changed even if the sign of the + // playback rate changed since we're not finished (we're paused or idle) + // * the playback rate on layers doesn't need to be updated since we're not + // moving. Once we get a start time etc. we'll update the playback rate + // then. + // + // All we need to do is update observers so that, e.g. DevTools, report the + // right information. + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + } else if (playState == AnimationPlayState::Finished) { + MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(), + "If we have no active timeline, we should be idle or paused"); + if (aPlaybackRate != 0) { + // The unconstrained current time can only be unresolved if either we + // don't have an active timeline (and we already asserted that is not + // true) or we have an unresolved start time (in which case we should be + // paused). + MOZ_ASSERT(!GetUnconstrainedCurrentTime().IsNull(), + "Unconstrained current time should be resolved"); + TimeDuration unconstrainedCurrentTime = + GetUnconstrainedCurrentTime().Value(); + TimeDuration timelineTime = mTimeline->GetCurrentTime().Value(); + mStartTime = StartTimeFromTimelineTime( + timelineTime, unconstrainedCurrentTime, aPlaybackRate); + } else { + mStartTime = mTimeline->GetCurrentTime(); + } + + ApplyPendingPlaybackRate(); + + // Even though we preserve the current time, we might now leave the finished + // state (e.g. if the playback rate changes sign) so we need to update + // timing. + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + PostUpdate(); + } else { + ErrorResult rv; + Play(rv, LimitBehavior::Continue); + MOZ_ASSERT(!rv.Failed(), + "We should only fail to play when using auto-rewind behavior"); + } +} + // https://drafts.csswg.org/web-animations/#play-state AnimationPlayState Animation::PlayState() const { if (!nsContentUtils::AnimationsAPIPendingMemberEnabled() && Pending()) { return AnimationPlayState::Pending; } @@ -450,24 +534,28 @@ Animation::Cancel() CancelNoUpdate(); PostUpdate(); } // https://drafts.csswg.org/web-animations/#finish-an-animation void Animation::Finish(ErrorResult& aRv) { - if (mPlaybackRate == 0 || - (mPlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) { + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + + if (effectivePlaybackRate == 0 || + (effectivePlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } AutoMutationBatchForAnimation mb(*this); + ApplyPendingPlaybackRate(); + // Seek to the end TimeDuration limit = mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0); bool didChange = GetCurrentTime() != Nullable(limit); SilentlySetCurrentTime(limit); // If we are paused or play-pending we need to fill in the start time in // order to transition to the finished state. @@ -525,35 +613,34 @@ Animation::Pause(ErrorResult& aRv) void Animation::Reverse(ErrorResult& aRv) { if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } - if (mPlaybackRate == 0.0) { + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + + if (effectivePlaybackRate == 0.0) { return; } - AutoMutationBatchForAnimation mb(*this); + Maybe originalPendingPlaybackRate = mPendingPlaybackRate; - SilentlySetPlaybackRate(-mPlaybackRate); + mPendingPlaybackRate = Some(-effectivePlaybackRate); + Play(aRv, LimitBehavior::AutoRewind); // If Play() threw, restore state and don't report anything to mutation // observers. if (aRv.Failed()) { - SilentlySetPlaybackRate(-mPlaybackRate); - return; + mPendingPlaybackRate = originalPendingPlaybackRate; } - if (IsRelevant()) { - nsNodeUtils::AnimationChanged(this); - } // Play(), above, unconditionally calls PostUpdate so we don't need to do // it here. } // --------------------------------------------------------------------------- // // JS wrappers for Animation interface: // @@ -673,16 +760,37 @@ Animation::TriggerNow() FinishPendingAt(mTimeline->GetCurrentTime().Value()); } Nullable Animation::GetCurrentOrPendingStartTime() const { Nullable result; + // If we have a pending playback rate, work out what start time we will use + // when we come to updating that playback rate. + // + // This logic roughly shadows that in ResumeAt but is just different enough + // that it is difficult to extract out the common functionality (and + // extracting that functionality out would make it harder to match ResumeAt up + // against the spec). + if (mPendingPlaybackRate && !mPendingReadyTime.IsNull() && + !mStartTime.IsNull()) { + // If we have a hold time, use it as the current time to match. + TimeDuration currentTimeToMatch = + !mHoldTime.IsNull() + ? mHoldTime.Value() + : CurrentTimeFromTimelineTime( + mPendingReadyTime.Value(), mStartTime.Value(), mPlaybackRate); + + result = StartTimeFromTimelineTime( + mPendingReadyTime.Value(), currentTimeToMatch, *mPendingPlaybackRate); + return result; + } + if (!mStartTime.IsNull()) { result = mStartTime; return result; } if (mPendingReadyTime.IsNull() || mHoldTime.IsNull()) { return result; } @@ -758,26 +866,16 @@ Animation::SilentlySetCurrentTime(const } else { mStartTime = StartTimeFromTimelineTime( mTimeline->GetCurrentTime().Value(), aSeekTime, mPlaybackRate); } mPreviousCurrentTime.SetNull(); } -void -Animation::SilentlySetPlaybackRate(double aPlaybackRate) -{ - Nullable previousTime = GetCurrentTime(); - mPlaybackRate = aPlaybackRate; - if (!previousTime.IsNull()) { - SilentlySetCurrentTime(previousTime.Value()); - } -} - // https://drafts.csswg.org/web-animations/#cancel-an-animation void Animation::CancelNoUpdate() { if (PlayState() != AnimationPlayState::Idle) { ResetPendingTasks(); if (mFinished) { @@ -1040,48 +1138,57 @@ Animation::NotifyGeometricAnimationsStar // https://drafts.csswg.org/web-animations/#play-an-animation void Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) { AutoMutationBatchForAnimation mb(*this); bool abortedPause = mPendingState == PendingState::PausePending; + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + Nullable currentTime = GetCurrentTime(); - if (mPlaybackRate > 0.0 && + if (effectivePlaybackRate > 0.0 && (currentTime.IsNull() || (aLimitBehavior == LimitBehavior::AutoRewind && (currentTime.Value() < TimeDuration() || currentTime.Value() >= EffectEnd())))) { mHoldTime.SetValue(TimeDuration(0)); - } else if (mPlaybackRate < 0.0 && + } else if (effectivePlaybackRate < 0.0 && (currentTime.IsNull() || (aLimitBehavior == LimitBehavior::AutoRewind && (currentTime.Value() <= TimeDuration() || currentTime.Value() > EffectEnd())))) { if (EffectEnd() == TimeDuration::Forever()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } mHoldTime.SetValue(TimeDuration(EffectEnd())); - } else if (mPlaybackRate == 0.0 && currentTime.IsNull()) { + } else if (effectivePlaybackRate == 0.0 && currentTime.IsNull()) { mHoldTime.SetValue(TimeDuration(0)); } bool reuseReadyPromise = false; if (mPendingState != PendingState::NotPending) { CancelPendingTasks(); reuseReadyPromise = true; } - // If the hold time is null then we're either already playing normally (and - // we can ignore this call) or we aborted a pending pause operation (in which - // case, for consistency, we need to go through the motions of doing an - // asynchronous start even though we already have a resolved start time). - if (mHoldTime.IsNull() && !abortedPause) { + // If the hold time is null then we're already playing normally and, + // typically, we can bail out here. + // + // However, there are two cases where we can't do that: + // + // (a) If we just aborted a pause. In this case, for consistency, we need to + // go through the motions of doing an asynchronous start. + // + // (b) If we have timing changes (specifically a change to the playbackRate) + // that should be applied asynchronously. + // + if (mHoldTime.IsNull() && !abortedPause && !mPendingPlaybackRate) { return; } // Clear the start time until we resolve a new one. We do this except // for the case where we are aborting a pause and don't have a hold time. // // If we're aborting a pause and *do* have a hold time (e.g. because // the animation is finished or we just applied the auto-rewind behavior @@ -1168,56 +1275,79 @@ Animation::PauseNoUpdate(ErrorResult& aR } UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); if (IsRelevant()) { nsNodeUtils::AnimationChanged(this); } } +// https://drafts.csswg.org/web-animations/#play-an-animation void Animation::ResumeAt(const TimeDuration& aReadyTime) { // This method is only expected to be called for an animation that is // waiting to play. We can easily adapt it to handle other states // but it's currently not necessary. MOZ_ASSERT(mPendingState == PendingState::PlayPending, "Expected to resume a play-pending animation"); MOZ_ASSERT(!mHoldTime.IsNull() || !mStartTime.IsNull(), "An animation in the play-pending state should have either a" " resolved hold time or resolved start time"); - // If we aborted a pending pause operation we will already have a start time - // we should use. In all other cases, we resolve it from the ready time. - if (mStartTime.IsNull()) { + AutoMutationBatchForAnimation mb(*this); + bool hadPendingPlaybackRate = mPendingPlaybackRate.isSome(); + + if (!mHoldTime.IsNull()) { + // The hold time is set, so we don't need any special handling to preserve + // the current time. + ApplyPendingPlaybackRate(); mStartTime = StartTimeFromTimelineTime(aReadyTime, mHoldTime.Value(), mPlaybackRate); if (mPlaybackRate != 0) { mHoldTime.SetNull(); } + } else if (!mStartTime.IsNull() && mPendingPlaybackRate) { + // Apply any pending playback rate, preserving the current time. + TimeDuration currentTimeToMatch = CurrentTimeFromTimelineTime( + aReadyTime, mStartTime.Value(), mPlaybackRate); + ApplyPendingPlaybackRate(); + mStartTime = + StartTimeFromTimelineTime(aReadyTime, currentTimeToMatch, mPlaybackRate); + if (mPlaybackRate == 0) { + mHoldTime.SetValue(currentTimeToMatch); + } } + mPendingState = PendingState::NotPending; UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + // If we had a pending playback rate, we will have now applied it so we need + // to notify observers. + if (hadPendingPlaybackRate && IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + if (mReady) { mReady->MaybeResolve(this); } } void Animation::PauseAt(const TimeDuration& aReadyTime) { MOZ_ASSERT(mPendingState == PendingState::PausePending, "Expected to pause a pause-pending animation"); if (!mStartTime.IsNull() && mHoldTime.IsNull()) { mHoldTime = CurrentTimeFromTimelineTime( aReadyTime, mStartTime.Value(), mPlaybackRate); } + ApplyPendingPlaybackRate(); mStartTime.SetNull(); mPendingState = PendingState::NotPending; UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); if (mReady) { mReady->MaybeResolve(this); } @@ -1355,16 +1485,18 @@ Animation::CancelPendingTasks() void Animation::ResetPendingTasks() { if (mPendingState == PendingState::NotPending) { return; } CancelPendingTasks(); + ApplyPendingPlaybackRate(); + if (mReady) { mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR); mReady = nullptr; } } bool Animation::IsPossiblyOrphanedPendingAnimation() const diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h --- a/dom/animation/Animation.h +++ b/dom/animation/Animation.h @@ -115,17 +115,17 @@ public: bool Pending() const { return mPendingState != PendingState::NotPending; } virtual Promise* GetReady(ErrorResult& aRv); Promise* GetFinished(ErrorResult& aRv); void Cancel(); void Finish(ErrorResult& aRv); virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior); virtual void Pause(ErrorResult& aRv); void Reverse(ErrorResult& aRv); - void UpdatePlaybackRate(double /*aPlaybackRate*/) {} + void UpdatePlaybackRate(double aPlaybackRate); bool IsRunningOnCompositor() const; IMPL_EVENT_HANDLER(finish); IMPL_EVENT_HANDLER(cancel); // Wrapper functions for Animation DOM methods when called // from script. // // We often use the same methods internally and from script but when called @@ -239,16 +239,25 @@ public: * animations on the next tick and apply the start time stored here. * * This method returns the start time, if resolved. Otherwise, if we have * a pending ready time, it returns the corresponding start time. If neither * of those are available, it returns null. */ Nullable GetCurrentOrPendingStartTime() const; + /** + * As with the start time, we should use the pending playback rate when + * producing layer animations. + */ + double CurrentOrPendingPlaybackRate() const + { + return mPendingPlaybackRate.valueOr(mPlaybackRate); + } + bool HasPendingPlaybackRate() const { return mPendingPlaybackRate.isSome(); } /** * The following relationship from the definition of the 'current time' is * re-used in many algorithms so we extract it here into a static method that * can be re-used: * * current time = (timeline time - start time) * playback rate * @@ -388,32 +397,38 @@ public: * We need to do this synchronously because after a CSS animation/transition * is canceled, it will be released by its owning element and may not still * exist when we would normally go to queue events on the next tick. */ virtual void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) {}; protected: void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime); - void SilentlySetPlaybackRate(double aPlaybackRate); void CancelNoUpdate(); void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior); void PauseNoUpdate(ErrorResult& aRv); void ResumeAt(const TimeDuration& aReadyTime); void PauseAt(const TimeDuration& aReadyTime); void FinishPendingAt(const TimeDuration& aReadyTime) { if (mPendingState == PendingState::PlayPending) { ResumeAt(aReadyTime); } else if (mPendingState == PendingState::PausePending) { PauseAt(aReadyTime); } else { NS_NOTREACHED("Can't finish pending if we're not in a pending state"); } } + void ApplyPendingPlaybackRate() + { + if (mPendingPlaybackRate) { + mPlaybackRate = *mPendingPlaybackRate; + mPendingPlaybackRate.reset(); + } + } /** * Finishing behavior depends on if changes to timing occurred due * to a seek or regular playback. */ enum class SeekFlag { NoSeek, DidSeek @@ -452,37 +467,47 @@ protected: /** * Returns true if this animation is not only play-pending, but has * yet to be given a pending ready time. This roughly corresponds to * animations that are waiting to be painted (since we set the pending * ready time at the end of painting). Identifying such animations is * useful because in some cases animations that are painted together * may need to be synchronized. + * + * We don't, however, want to include animations with a fixed start time such + * as animations that are simply having their playbackRate updated or which + * are resuming from an aborted pause. */ bool IsNewlyStarted() const { return mPendingState == PendingState::PlayPending && - mPendingReadyTime.IsNull(); + mPendingReadyTime.IsNull() && + mStartTime.IsNull(); } bool IsPossiblyOrphanedPendingAnimation() const; StickyTimeDuration EffectEnd() const; Nullable GetCurrentTimeForHoldTime( const Nullable& aHoldTime) const; + Nullable GetUnconstrainedCurrentTime() const + { + return GetCurrentTimeForHoldTime(Nullable()); + } nsIDocument* GetRenderedDocument() const; RefPtr mTimeline; RefPtr mEffect; // The beginning of the delay period. Nullable mStartTime; // Timeline timescale Nullable mHoldTime; // Animation timescale Nullable mPendingReadyTime; // Timeline timescale Nullable mPreviousCurrentTime; // Animation timescale double mPlaybackRate; + Maybe mPendingPlaybackRate; // A Promise that is replaced on each call to Play() // and fulfilled when Play() is successfully completed. // This object is lazily created by GetReady. // See http://drafts.csswg.org/web-animations/#current-ready-promise RefPtr mReady; // A Promise that is resolved when we reach the end of the effect, or