Bug 1775327 - Part 1: Make sure the scroll animation is still in active phase at boundaries. r=firefox-animation-reviewers,birtles

We have to introduce "at progress timeline boundary" which is defined in
web-animations-2 [1]. We need this to make sure the scroll animations
do not go into before phase or after phase.

The test of fill-mode should be together with delay, so I'd like to add the
tests in the last patch.

[1] https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary

Differential Revision: https://phabricator.services.mozilla.com/D149684
This commit is contained in:
Boris Chiou 2022-07-07 18:33:41 +00:00
parent 309272c871
commit ee025d7686
10 changed files with 144 additions and 22 deletions

View File

@ -1741,6 +1741,53 @@ void Animation::ReschedulePendingTasks() {
}
}
// https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary
/* static*/ Animation::ProgressTimelinePosition
Animation::AtProgressTimelineBoundary(
const Nullable<TimeDuration>& aTimelineDuration,
const Nullable<TimeDuration>& aCurrentTime,
const TimeDuration& aEffectStartTime, const double aPlaybackRate) {
// Based on changed defined in: https://github.com/w3c/csswg-drafts/pull/6702
// 1. If any of the following conditions are true:
// * the associated animation's timeline is not a progress-based timeline,
// or
// * the associated animation's timeline duration is unresolved or zero,
// or
// * the animation's playback rate is zero
// return false
// Note: We can detect a progress-based timeline by relying on the fact that
// monotonic timelines (i.e. non-progress-based timelines) have an unresolved
// timeline duration.
if (aTimelineDuration.IsNull() || aTimelineDuration.Value().IsZero() ||
aPlaybackRate == 0.0) {
return ProgressTimelinePosition::NotBoundary;
}
// 2. Let effective start time be the animation's start time if resolved, or
// zero otherwise.
const TimeDuration& effectiveStartTime = aEffectStartTime;
// 3. Let effective timeline time be (animation's current time / animation's
// playback rate) + effective start time.
// Note: we use zero if the current time is unresolved. See the spec issue:
// https://github.com/w3c/csswg-drafts/issues/7458
const TimeDuration effectiveTimelineTime =
(aCurrentTime.IsNull()
? TimeDuration()
: aCurrentTime.Value().MultDouble(1.0 / aPlaybackRate)) +
effectiveStartTime;
// 4. Let effective timeline progress be (effective timeline time / timeline
// duration)
// 5. If effective timeline progress is 0 or 1, return true,
// We avoid the division here but it is effectively the same as 4 & 5 above.
return effectiveTimelineTime.IsZero() ||
(AnimationUtils::IsWithinAnimationTimeTolerance(
effectiveTimelineTime, aTimelineDuration.Value()))
? ProgressTimelinePosition::Boundary
: ProgressTimelinePosition::NotBoundary;
}
bool Animation::IsPossiblyOrphanedPendingAnimation() const {
// Check if we are pending but might never start because we are not being
// tracked.

View File

@ -451,6 +451,23 @@ class Animation : public DOMEventTargetHelper,
return mTimeline && mTimeline->IsScrollTimeline();
}
/**
* Returns true if this is at the progress timeline boundary.
* https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary
*/
enum class ProgressTimelinePosition : uint8_t { Boundary, NotBoundary };
static ProgressTimelinePosition AtProgressTimelineBoundary(
const Nullable<TimeDuration>& aTimelineDuration,
const Nullable<TimeDuration>& aCurrentTime,
const TimeDuration& aEffectStartTime, const double aPlaybackRate);
ProgressTimelinePosition AtProgressTimelineBoundary() const {
return AtProgressTimelineBoundary(
mTimeline ? mTimeline->TimelineDuration() : nullptr,
GetCurrentTimeAsDuration(),
mStartTime.IsNull() ? TimeDuration() : mStartTime.Value(),
mPlaybackRate);
}
protected:
void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
void CancelNoUpdate();

View File

@ -101,7 +101,8 @@ void AnimationEffect::SetSpecifiedTiming(TimingParams&& aTiming) {
ComputedTiming AnimationEffect::GetComputedTimingAt(
const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
double aPlaybackRate) {
double aPlaybackRate,
Animation::ProgressTimelinePosition aProgressTimelinePosition) {
static const StickyTimeDuration zeroDuration;
// Always return the same object to benefit from return-value optimization.
@ -134,6 +135,9 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
return result;
}
const TimeDuration& localTime = aLocalTime.Value();
const bool atProgressTimelineBoundary =
aProgressTimelinePosition ==
Animation::ProgressTimelinePosition::Boundary;
StickyTimeDuration beforeActiveBoundary =
std::max(std::min(StickyTimeDuration(aTiming.Delay()), result.mEndTime),
@ -145,7 +149,8 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
zeroDuration);
if (localTime > activeAfterBoundary ||
(aPlaybackRate >= 0 && localTime == activeAfterBoundary)) {
(aPlaybackRate >= 0 && localTime == activeAfterBoundary &&
!atProgressTimelineBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::After;
if (!result.FillsForwards()) {
// The animation isn't active or filling at this time.
@ -156,7 +161,8 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
result.mActiveDuration),
zeroDuration);
} else if (localTime < beforeActiveBoundary ||
(aPlaybackRate < 0 && localTime == beforeActiveBoundary)) {
(aPlaybackRate < 0 && localTime == beforeActiveBoundary &&
!atProgressTimelineBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::Before;
if (!result.FillsBackwards()) {
// The animation isn't active or filling at this time.
@ -165,8 +171,8 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
result.mActiveTime =
std::max(StickyTimeDuration(localTime - aTiming.Delay()), zeroDuration);
} else {
MOZ_ASSERT(result.mActiveDuration,
"How can we be in the middle of a zero-duration interval?");
// Note: For progress-based timeline, it's possible to have a zero active
// duration with active phase.
result.mPhase = ComputedTiming::AnimationPhase::Active;
result.mActiveTime = localTime - aTiming.Delay();
}
@ -267,9 +273,13 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
ComputedTiming AnimationEffect::GetComputedTiming(
const TimingParams* aTiming) const {
double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
return GetComputedTimingAt(
GetLocalTime(), aTiming ? *aTiming : NormalizedTiming(), playbackRate);
const double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
const auto progressTimelinePosition =
mAnimation ? mAnimation->AtProgressTimelineBoundary()
: Animation::ProgressTimelinePosition::NotBoundary;
return GetComputedTimingAt(GetLocalTime(),
aTiming ? *aTiming : NormalizedTiming(),
playbackRate, progressTimelinePosition);
}
// Helper function for generating an (Computed)EffectTiming dictionary
@ -303,8 +313,11 @@ void AnimationEffect::GetComputedTimingAsDict(
// Computed timing
double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
const Nullable<TimeDuration> currentTime = GetLocalTime();
ComputedTiming computedTiming =
GetComputedTimingAt(currentTime, SpecifiedTiming(), playbackRate);
const auto progressTimelinePosition =
mAnimation ? mAnimation->AtProgressTimelineBoundary()
: Animation::ProgressTimelinePosition::NotBoundary;
ComputedTiming computedTiming = GetComputedTimingAt(
currentTime, SpecifiedTiming(), playbackRate, progressTimelinePosition);
aRetVal.mDuration.SetAsUnrestrictedDouble() =
computedTiming.mDuration.ToMilliseconds();

View File

@ -79,7 +79,8 @@ class AnimationEffect : public nsISupports, public nsWrapperCache {
// (because it is not currently active and is not filling at this time).
static ComputedTiming GetComputedTimingAt(
const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
double aPlaybackRate);
double aPlaybackRate,
Animation::ProgressTimelinePosition aProgressTimelinePosition);
// Shortcut that gets the computed timing using the current local time as
// calculated from the timeline time.
ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;

View File

@ -106,6 +106,13 @@ class AnimationTimeline : public nsISupports, public nsWrapperCache {
virtual bool IsScrollTimeline() const { return false; }
virtual const ScrollTimeline* AsScrollTimeline() const { return nullptr; }
// For a monotonic timeline, there is no upper bound on current time, and
// timeline duration is unresolved. For a non-monotonic (e.g. scroll)
// timeline, the duration has a fixed upper bound.
//
// https://drafts.csswg.org/web-animations-2/#timeline-duration
virtual Nullable<TimeDuration> TimelineDuration() const { return nullptr; }
protected:
nsCOMPtr<nsIGlobalObject> mWindow;

View File

@ -94,6 +94,21 @@ class AnimationUtils {
return aType == PseudoStyleType::before ||
aType == PseudoStyleType::after || aType == PseudoStyleType::marker;
}
/**
* Returns true if the difference between |aFirst| and |aSecond| is within
* the animation time tolerance (i.e. 1 microsecond).
*/
static bool IsWithinAnimationTimeTolerance(const TimeDuration& aFirst,
const TimeDuration& aSecond) {
if (aFirst == TimeDuration::Forever() ||
aSecond == TimeDuration::Forever()) {
return aFirst == aSecond;
}
TimeDuration diff = aFirst >= aSecond ? aFirst - aSecond : aSecond - aFirst;
return diff <= TimeDuration::FromMicroseconds(1);
}
};
} // namespace mozilla

View File

@ -299,7 +299,8 @@ void CSSTransition::UpdateStartValueFromReplacedTransition() {
CSSTransition::GetCurrentTimeAt(*mTimeline, TimeStamp::Now(),
mReplacedTransition->mStartTime,
mReplacedTransition->mPlaybackRate),
mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate);
mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate,
Animation::ProgressTimelinePosition::NotBoundary);
if (!computedTiming.mProgress.IsNull()) {
double valuePosition = ComputedTimingFunction::GetPortion(

View File

@ -12,6 +12,7 @@
#include "mozilla/HashTable.h"
#include "mozilla/PairHash.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/TimingParams.h"
#include "mozilla/WritingModes.h"
class nsIScrollableFrame;
@ -144,6 +145,11 @@ class ScrollTimeline final : public AnimationTimeline {
bool IsMonotonicallyIncreasing() const override { return false; }
bool IsScrollTimeline() const override { return true; }
const ScrollTimeline* AsScrollTimeline() const override { return this; }
Nullable<TimeDuration> TimelineDuration() const override {
// We are using this magic number for progress-based timeline duration
// because we don't support percentage for duration.
return TimeDuration::FromMilliseconds(PROGRESS_TIMELINE_DURATION_MILLISEC);
}
void ScheduleAnimations() {
// FIXME: Bug 1737927: Need to check the animation mutation observers for

View File

@ -221,7 +221,6 @@ void TimingParams::Normalize() {
mDelay = TimeDuration::FromMilliseconds(0);
mIterations = std::numeric_limits<double>::infinity();
mDirection = dom::PlaybackDirection::Alternate;
mFill = dom::FillMode::Both;
Update();
}

View File

@ -31,8 +31,8 @@ namespace layers {
static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
const ScrollTimelineOptions& aOptions,
const Maybe<TimeDuration>& aDuration) {
const ScrollTimelineOptions& aOptions, const Maybe<TimeDuration>& aDuration,
const TimeDuration& aStartTime, float aPlaybackRate) {
MOZ_ASSERT(aDuration);
// We return Nothing If the associated APZ controller is not available
@ -60,10 +60,9 @@ static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
// Just in case to avoid getting a progress more than 100%, for overscrolling.
progress = std::min(progress, 1.0);
// FIXME: Bug 1744850: should we take the playback rate into account? For now
// it is always 1.0 from ScrollTimeline::Timing(). We may have to update here
// in Bug 1744850.
return TimeDuration::FromMilliseconds(progress * aDuration->ToMilliseconds());
auto timelineTime = aDuration->MultDouble(progress);
return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
aPlaybackRate);
}
static dom::Nullable<TimeDuration> CalculateElapsedTime(
@ -84,7 +83,9 @@ static dom::Nullable<TimeDuration> CalculateElapsedTime(
aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
aProofOfMapLock),
aAnimation.mScrollTimelineOptions.value(),
aAnimation.mTiming.Duration());
aAnimation.mTiming.Duration(),
aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
aAnimation.mPlaybackRate);
}
// -------------------------------------
@ -173,9 +174,24 @@ static AnimationHelper::SampleResult SampleAnimationForProperty(
aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
aCurrentFrameTime, aPreviousValue);
ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
elapsedDuration, animation.mTiming, animation.mPlaybackRate);
const auto progressTimelinePosition =
animation.mScrollTimelineOptions
? dom::Animation::AtProgressTimelineBoundary(
TimeDuration::FromMilliseconds(
PROGRESS_TIMELINE_DURATION_MILLISEC),
elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
animation.mPlaybackRate)
: dom::Animation::ProgressTimelinePosition::NotBoundary;
ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
elapsedDuration, animation.mTiming, animation.mPlaybackRate,
progressTimelinePosition);
// FIXME: Bug 1776077, for the scroll-linked animations, it's possible to
// let it go from the active phase to the before phase, and its progress
// becomes null. In this case, we shouldn't just skip this animation.
// Instead, we have to reset the sampled result to that without this
// animation.
if (computedTiming.mProgress.IsNull()) {
continue;
}