Skip to content

Commit

Permalink
Bug 1775327 - Part 3: Do normalization for NormalizedTiming(). r=fire…
Browse files Browse the repository at this point in the history
…fox-animation-reviewers,birtles

This implements the normalization of the specified time, defined in
[web-animations-2]:
https://drafts.csswg.org/web-animations-2/#normalize-specified-timing.
However, it is possible to update this, based on the spec issue:
w3c/csswg-drafts#4862.

For now, we just do normalization for delay, end delay, and
iteration duration based on the end time. And make sure the end time is
equal to the timeline duration.

Differential Revision: https://phabricator.services.mozilla.com/D149685
  • Loading branch information
BorisChiou committed Jul 7, 2022
1 parent db1b58d commit 74f6246
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 12 deletions.
6 changes: 4 additions & 2 deletions dom/animation/AnimationEffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,10 @@ void AnimationEffect::UpdateNormalizedTiming() {
return;
}

mNormalizedTiming.emplace(mTiming);
mNormalizedTiming->Normalize();
// Since `mAnimation` has a scroll timeline, we can be sure `GetTimeline()`
// and `TimelineDuration()` will not return null.
mNormalizedTiming.emplace(
mTiming.Normalize(mAnimation->GetTimeline()->TimelineDuration().Value()));
}

Nullable<TimeDuration> AnimationEffect::GetLocalTime() const {
Expand Down
3 changes: 2 additions & 1 deletion dom/animation/ScrollTimeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
#include "mozilla/HashTable.h"
#include "mozilla/PairHash.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/TimingParams.h"
#include "mozilla/WritingModes.h"

#define PROGRESS_TIMELINE_DURATION_MILLISEC 100000

class nsIScrollableFrame;

namespace mozilla {
Expand Down
77 changes: 71 additions & 6 deletions dom/animation/TimingParams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,78 @@ bool TimingParams::operator==(const TimingParams& aOther) const {
mFunction == aOther.mFunction;
}

void TimingParams::Normalize() {
// FIXME: Bug 1775327, do normalization, instead of using these magic numbers.
mDuration = Some(StickyTimeDuration::FromMilliseconds(
PROGRESS_TIMELINE_DURATION_MILLISEC));
mDelay = TimeDuration::FromMilliseconds(0);
// FIXME: This is a tentative way to normalize the timing which is defined in
// [web-animations-2] [1]. I borrow this implementation and some concepts for
// the edge cases from Chromium [2] so we can match the behavior with them. The
// implementation here ignores the case of percentage of start delay, end delay,
// and duration because Gecko doesn't support them. We may have to update the
// calculation if the spec issue [3] gets any update.
//
// [1]
// https://drafts.csswg.org/web-animations-2/#time-based-animation-to-a-proportional-animation
// [2] https://chromium-review.googlesource.com/c/chromium/src/+/2992387
// [3] https://github.com/w3c/csswg-drafts/issues/4862
TimingParams TimingParams::Normalize(
const TimeDuration& aTimelineDuration) const {
MOZ_ASSERT(aTimelineDuration,
"the timeline duration of scroll-timeline is always non-zero now");

TimingParams normalizedTiming(*this);

// Handle iteration duration value of "auto" first.
// FIXME: Bug 1676794: Gecko doesn't support `animation-duration:auto` and we
// don't support JS-generated scroll animations, so we don't fall into this
// case for now. Need to check this again after we support ScrollTimeline
// interface.
if (!mDuration) {
// If the iteration duration is auto, then:
// Set start delay and end delay to 0, as it is not possible to mix time
// and proportions.
normalizedTiming.mDelay = TimeDuration();
normalizedTiming.mEndDelay = TimeDuration();
normalizedTiming.Update();
return normalizedTiming;
}

if (mEndTime.IsZero()) {
// mEndTime of zero causes division by zero so we handle it here.
//
// FIXME: The spec doesn't mention this case, so we might have to update
// this based on the spec issue,
// https://github.com/w3c/csswg-drafts/issues/7459.
normalizedTiming.mDelay = TimeDuration();
normalizedTiming.mEndDelay = TimeDuration();
normalizedTiming.mDuration = Some(TimeDuration());
} else if (mEndTime == TimeDuration::Forever()) {
// The iteration count or duration may be infinite; however, start and
// end delays are strictly finite. Thus, in the limit when end time
// approaches infinity:
// start delay / end time = finite / infinite = 0
// end delay / end time = finite / infinite = 0
// iteration duration / end time = 1 / iteration count
// This condition can be reached by switching to a scroll timeline on
// an existing infinite duration animation.
//
// FIXME: The spec doesn't mention this case, so we might have to update
// this based on the spec issue,
// https://github.com/w3c/csswg-drafts/issues/7459.
normalizedTiming.mDelay = TimeDuration();
normalizedTiming.mEndDelay = TimeDuration();
normalizedTiming.mDuration =
Some(aTimelineDuration.MultDouble(1.0 / mIterations));
} else {
// Convert to percentages then multiply by the timeline duration.
const double endTimeInSec = mEndTime.ToSeconds();
normalizedTiming.mDelay =
aTimelineDuration.MultDouble(mDelay.ToSeconds() / endTimeInSec);
normalizedTiming.mEndDelay =
aTimelineDuration.MultDouble(mEndDelay.ToSeconds() / endTimeInSec);
normalizedTiming.mDuration = Some(StickyTimeDuration(
aTimelineDuration.MultDouble(mDuration->ToSeconds() / endTimeInSec)));
}

Update();
normalizedTiming.Update();
return normalizedTiming;
}

} // namespace mozilla
6 changes: 3 additions & 3 deletions dom/animation/TimingParams.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
#include "mozilla/dom/AnimationEffectBinding.h" // for FillMode
// and PlaybackDirection

#define PROGRESS_TIMELINE_DURATION_MILLISEC 100000

namespace mozilla {

namespace dom {
Expand Down Expand Up @@ -204,7 +202,9 @@ struct TimingParams {
return mFunction;
}

void Normalize();
// This is called only for progress-based timeline (i.e. non-monotonic
// timeline). That is, |aTimelineDuration| should be resolved already.
TimingParams Normalize(const TimeDuration& aTimelineDuration) const;

private:
void Update() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<!DOCTYPE html>
<title>The various animation longhands with progress based animations</title>
<link rel="help" src="https://drafts.csswg.org/css-animations-2">
<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/4862">
<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
@keyframes anim {
from { translate: 0px; }
to { translate: 100px; }
}
#container {
width: 300px;
height: 300px;
overflow: scroll;
}
#target {
width: 100px;
height: 100px;
translate: none;
}
</style>
<body>
<div id="log"></div>
<script>
"use strict";

const createTargetAndScroller = function(t) {
let container = document.createElement('div');
container.id = 'container';
let target = document.createElement('div');
target.id = 'target';
let content = document.createElement('div');
content.style.blockSize = '100%';

// The height of target is 100px and the content is 100%, so the scroll range
// is [0, 100].

// <div id='container'>
// <div id='target'></div>
// <div style='block-size: 100%;'></div>
// </div>
document.body.appendChild(container);
container.appendChild(target);
container.appendChild(content);

if (t && typeof t.add_cleanup === 'function') {
t.add_cleanup(() => {
content.remove();
target.remove();
container.remove();
});
}

return [target, container];
};

// ------------------------------
// Test animation-duration
// ------------------------------

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '25px');
}, 'animation-duration');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '0s linear anim scroll(nearest)';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
}, 'animation-duration: 0s');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = 'infinite linear anim scroll(nearest)';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
}, 'animation-duration: infinite');


// ------------------------------
// Test animation-iteration-count
// ------------------------------

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '25px');

// Let animation become 50% in the 1st iteration.
target.style.animationIterationCount = '2';
assert_equals(getComputedStyle(target).translate, '50px');

// Let animation become 0% in the 2nd iteration.
target.style.animationIterationCount = '4';
assert_equals(getComputedStyle(target).translate, '0px');
}, 'animation-iteration-count');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationIterationCount = '0';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '0px');
}, 'animation-iteration-count: 0');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationIterationCount = 'infinite';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
}, 'animation-iteration-count: infinite');


// ------------------------------
// Test animation-direction
// ------------------------------

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';

scroller.scrollTop = 25 // [0, 100].
assert_equals(getComputedStyle(target).translate, '25px');
}, 'animation-direction: normal');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationDirection = 'reverse';

scroller.scrollTop = 25; // 25% in the reversing direction.
assert_equals(getComputedStyle(target).translate, '75px');
}, 'animation-direction: reverse');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationIterationCount = '2';
target.style.animationDirection = 'alternate';

scroller.scrollTop = 10; // 20% in the 1st iteration.
assert_equals(getComputedStyle(target).translate, '20px');

scroller.scrollTop = 60; // 20% in the 2nd iteration (reversing direction).
assert_equals(getComputedStyle(target).translate, '80px');
}, 'animation-direction: alternate');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationIterationCount = '2';
target.style.animationDirection = 'alternate-reverse';

scroller.scrollTop = 10; // 20% in the 1st iteration (reversing direction).
assert_equals(getComputedStyle(target).translate, '80px');

scroller.scrollTop = 60; // 20% in the 2nd iteration.
assert_equals(getComputedStyle(target).translate, '20px');
}, 'animation-direction: alternate-reverse');


// ------------------------------
// Test animation-delay
// ------------------------------

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';

scroller.scrollTop = 25; // [0, 100].
assert_equals(getComputedStyle(target).translate, '25px');

// (start delay: 10s) (duration: 10s)
// before active
// |--------------------|--------------------|
// 0px 50px 100px (The scroller)
// 0% 100% (The iteration progress)

// Let animation be in before phase.
target.style.animationDelay = '10s';
assert_equals(getComputedStyle(target).translate, 'none');

scroller.scrollTop = 50; // The animation enters active phase.
assert_equals(getComputedStyle(target).translate, '0px');

scroller.scrollTop = 75; // The ieration progress is 50%.
assert_equals(getComputedStyle(target).translate, '50px');
}, 'animation-delay with a positive value');

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';

// active
// |--------------------|
// 0px 100px (The scroller)
// 50% 100% (The iteration progress)

scroller.scrollTop = 20; // [0, 100].
target.style.animationDelay = '-5s';
assert_equals(getComputedStyle(target).translate, '60px');
}, 'animation-delay with a negative value');


// ------------------------------
// Test animation-fill-mode
// ------------------------------

test(t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = '10s linear anim scroll(nearest)';
target.style.animationDelay = '10s';

scroller.scrollTop = 25;
assert_equals(getComputedStyle(target).translate, 'none');

target.style.animationFillMode = 'backwards';
assert_equals(getComputedStyle(target).translate, '0px');
}, 'animation-fill-mode');

</script>
</body>

0 comments on commit 74f6246

Please sign in to comment.