Bug 1818096 [wpt PR 38625] - [view-timeline]: Avoid reparse of keyframe rules containing timeline offsets, a=testonly

Automatic update from web-platform-tests
[view-timeline]: Avoid reparse of keyframe rules containing timeline offsets

A full reparse of the keyframes is wasteful, when we just need to
re-sort. Previously, we had a bug where the effect invalidation caused
the composited animation to lag.  This is because validateSnapshot
could trigger a second pass of layout update, but updateSnapshot could
not. We now avoid this problem entirely. Instead, we track if any
keyframe offsets are affected and simply clear the keyframe effect
cache if needed.

Injecting the neutral keyframes when processing the keyframe rules is
wasteful since already handled for property specific keyframes.
Removal of the neutral keyframes required updating getKeyframes to
return the expected results.  Overall, the process seems cleaner now.

Added tests for keyframe retrieval as well as for interpolation at the keyframe boundaries.

Bug: 1408475
Change-Id: I18fc726c6f42e414760eb52dd8478c6930690238
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4261369
Reviewed-by: Robert Flack <flackr@chromium.org>
Commit-Queue: Kevin Ellis <kevers@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1112198}

--

wpt-commits: c6b7fa7682f4ff3c5aa928443516473523234352
wpt-pr: 38625
This commit is contained in:
Kevin Ellis 2023-03-06 14:00:55 +00:00 committed by moz-wptsync-bot
parent 3c2e448d1a
commit 3a153dcb62
7 changed files with 467 additions and 18 deletions

View File

@ -249,7 +249,7 @@ test(t => {
"number of frames when @keyframes only has frames with " +
"non-animatable properties");
}, 'KeyframeEffect.getKeyframes() returns no frames for various kinds'
+ ' of empty enimations');
+ ' of empty animations');
test(t => {
const div = addDiv(t);

View File

@ -0,0 +1,130 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="support/testcommon.js"></script>
<script src="/web-animations/resources/keyframe-utils.js"></script>
<title>Animation range and delay</title>
</head>
<style type="text/css">
@keyframes anim {
cover 0% {
opacity: 0;
margin-left: 0px;
}
cover 100% {
opacity: 1;
margin-right: 0px;
}
}
#scroller {
border: 10px solid lightgray;
overflow-y: scroll;
overflow-x: hidden;
width: 300px;
height: 200px;
}
#block {
margin-top: 800px;
margin-left: 10px;
margin-right: 10px;
width: 100px;
height: 50px;
background-color: blue;
view-timeline: block block;
}
#target {
margin-bottom: 800px;
margin-left: 10px;
margin-right: 10px;
width: 100px;
height: 100px;
z-index: -1;
background-color: green;
animation: anim auto both linear;
/* using document timeline by default */
animation-range-start: contain 0%;
animation-range-end: contain 100%;
view-timeline: target block;
}
#target.with-view-timeline {
animation-timeline: target;
}
#target.with-view-timeline.retarget {
animation-timeline: block;
}
</style>
<body>
<div id="scroller">
<div id="block"></div>
<div id="target"></div>
</div>
</body>
<script type="text/javascript">
async function runTest() {
promise_test(async t => {
await waitForNextFrame();
const anim = document.getAnimations()[0];
await anim.ready;
await waitForNextFrame();
// Initially using a document timeline, so the keyframes should be
// ignored.
let frames = anim.effect.getKeyframes();
let expected = [];
assert_frame_lists_equal(frames, expected);
// Once a view-timeline is added, the kefyrames must update to reflect
// the new keyframe offsets.
target.classList.add('with-view-timeline');
assert_equals(getComputedStyle(target).animationTimeline, 'target',
'Switch to view timeline');
await waitForNextFrame();
frames = anim.effect.getKeyframes();
expected = [
{ offset: -1, computedOffset: -1, easing: "linear", composite: "auto",
marginLeft: "0px", opacity: "0" },
{ offset: 0, computedOffset: 0, easing: "linear", composite: "replace",
marginRight: "10px" },
{ offset: 1, computedOffset: 1, easing: "linear", composite: "replace",
marginLeft: "10px" },
{ offset: 2, computedOffset: 2, easing: "linear", composite: "auto",
marginRight: "0px", opacity: "1" },
];
assert_frame_lists_equal(frames, expected);
target.classList.add('retarget');
assert_equals(getComputedStyle(target).animationTimeline, 'block',
'Switch to another view timeline');
await waitForNextFrame();
frames = anim.effect.getKeyframes();
expected = [
{ offset: -1/3, computedOffset: -1/3, easing: "linear",
composite: "auto", marginLeft: "0px", opacity: "0" },
{ offset: 0, computedOffset: 0, easing: "linear", composite: "replace",
marginRight: "10px" },
{ offset: 1, computedOffset: 1, easing: "linear", composite: "replace",
marginLeft: "10px" },
{ offset: 4/3, computedOffset: 4/3, easing: "linear", composite: "auto",
marginRight: "0px", opacity: "1" },
];
assert_frame_lists_equal(frames, expected);
target.classList.toggle('with-view-timeline');
assert_equals(getComputedStyle(target).animationTimeline, 'auto',
'Switch back to document timeline');
frames = anim.effect.getKeyframes();
expected = [];
assert_frame_lists_equal(frames, expected);
}, 'getKeyframes with timeline-offsets');
}
window.onload = runTest;
</script>

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="support/testcommon.js"></script>
<script src="/web-animations/resources/keyframe-utils.js"></script>
<title>Animation range and delay</title>
</head>
<style type="text/css">
@keyframes anim {
cover 0% {
margin-left: 0px;
}
50% {
opacity: 0.5;
}
cover 100% {
margin-right: 0px;
}
}
#scroller {
border: 10px solid lightgray;
overflow-y: scroll;
overflow-x: hidden;
width: 300px;
height: 200px;
}
#block {
margin-top: 800px;
margin-left: 10px;
margin-right: 10px;
width: 100px;
height: 50px;
background-color: blue;
view-timeline: t1 block;
}
#target {
margin-bottom: 800px;
margin-left: 10px;
margin-right: 10px;
width: 100px;
height: 100px;
z-index: -1;
background-color: green;
animation: anim auto both linear;
animation-range-start: contain 0%;
animation-range-end: contain 100%;
animation-timeline: t1;
}
</style>
<body>
<div id="scroller">
<div id="block"></div>
<div id="target"></div>
</div>
</body>
<script type="text/javascript">
async function runTest() {
promise_test(async t => {
await waitForNextFrame();
const anim = document.getAnimations()[0];
await anim.ready;
await waitForNextFrame();
let frames = anim.effect.getKeyframes();
let expected_resolved_offsets = [
{ offset: -1/3, computedOffset: -1/3, easing: "linear",
composite: "auto", marginLeft: "0px" },
{ offset: 0, computedOffset: 0, easing: "linear", composite: "replace",
marginRight: "10px", opacity: "1" },
{ offset: 1/2, computedOffset: 1/2, easing: "linear",
composite: "auto", opacity: "0.5" },
{ offset: 1, computedOffset: 1, easing: "linear", composite: "replace",
marginLeft: "10px", opacity: "1" },
{ offset: 4/3, computedOffset: 4/3, easing: "linear", composite: "auto",
marginRight: "0px" },
];
assert_frame_lists_equal(frames, expected_resolved_offsets,
'Initial keyframes with active view-timeline');
block.style.display = 'none';
// View-timeline becomes invalid. Keyframes with timeline offsets must be
// ignored.
frames = anim.effect.getKeyframes();
let expected_unresolved_offsets = [
{ offset: 0, computedOffset: 0, opacity: "1", easing: "linear",
composite: "replace" },
{ offset: 0.5, computedOffset: 0.5, opacity: "0.5", easing: "linear",
composite: "auto", },
{ offset: 1, computedOffset: 1, opacity: "1", easing: "linear",
composite: "replace" }
];
assert_frame_lists_equal(frames, expected_unresolved_offsets,
'Keyframes with invalid view timeline');
block.style.display = 'block';
// Ensure that keyframes with timeline-offsets are restored.
frames = anim.effect.getKeyframes();
assert_frame_lists_equal(frames, expected_resolved_offsets,
'Keyframes with restored view timeline');
}, 'Keyframes with timeline-offsets ignored when timeline is inactive');
}
window.onload = runTest;
</script>

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="support/testcommon.js"></script>
<script src="/web-animations/resources/keyframe-utils.js"></script>
<title>Animation range and delay</title>
</head>
<style type="text/css">
@keyframes anim {
cover 0% {
margin-left: 0px;
}
50% {
opacity: 0.5;
}
cover 100% {
margin-right: 0px;
}
}
#scroller {
border: 10px solid lightgray;
overflow-y: scroll;
overflow-x: hidden;
width: 300px;
height: 200px;
}
#target {
margin-bottom: 800px;
margin-top: 800px;
margin-left: 10px;
margin-right: 10px;
width: 100px;
height: 100px;
z-index: -1;
background-color: green;
animation: anim auto both linear;
/* using document timeline by default */
}
</style>
<body>
<div id="scroller">
<div id="target"></div>
</div>
</body>
<script type="text/javascript">
async function runTest() {
promise_test(async t => {
await waitForNextFrame();
const anim = document.getAnimations()[0];
await anim.ready;
await waitForNextFrame();
// Using a document timeline, so only the 50% keyframe is used.
let frames = anim.effect.getKeyframes();
let expected = [
{ offset: 0, computedOffset: 0, opacity: "1", easing: "linear",
composite: "replace" },
{ offset: 0.5, computedOffset: 0.5, opacity: "0.5", easing: "linear",
composite: "auto", },
{ offset: 1, computedOffset: 1, opacity: "1", easing: "linear",
composite: "replace" }
];
assert_frame_lists_equal(frames, expected);
}, 'Keyframes with timeline-offsets ignored when using document ' +
'timeline');
}
window.onload = runTest;
</script>

View File

@ -51,43 +51,56 @@
// scrollTop=200 to 400 is the entry range
container.scrollTop = 200;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0', 'Effect at entry 0%');
const anim = document.getAnimations()[0];
assert_equals(getComputedStyle(subject).opacity, '0',
'Effect at entry 0%');
container.scrollTop = 300;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at entry 50%');
assert_equals(getComputedStyle(subject).opacity, '0.5',
'Effect at entry 50%');
container.scrollTop = 400;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at entry 100%');
assert_equals(getComputedStyle(subject).opacity, '1',
'Effect at entry 100%');
// scrollTop=600-800 is the exit range
container.scrollTop = 600;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at exit 0%');
assert_equals(getComputedStyle(subject).opacity, '1',
'Effect at exit 0%');
container.scrollTop = 700;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at exit 50%');
assert_equals(getComputedStyle(subject).opacity, '0.5',
'Effect at exit 50%');
container.scrollTop = 800;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0', 'Effect at exit 100%');
assert_equals(getComputedStyle(subject).opacity, '0',
'Effect at exit 100%');
// First change scrollTop so that you are at entry 100%, then resize the container in a way
// that scrollTop is the same, but now the animation is at entry 50% and check opacity.
// After changing the height of container, scrollTop=300-500 is the entry range
// First change scrollTop so that you are at entry 100%, then resize the
// container in a way that scrollTop is the same, but now the animation is
// at entry 50% and check opacity. After changing the height of container,
// scrollTop=300-500 is the entry range
container.scrollTop = 400;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '1', 'Effect at entry 100%');
assert_equals(getComputedStyle(subject).opacity, '1',
'Effect at entry 100% (post resize)');
container.style.height = '300px';
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at entry 50%');
assert_equals(getComputedStyle(subject).opacity, '0.5',
'Effect at entry 50% (post resize)');
// After changing the height of container, scrollTop=600-800 is still the exit range
// After changing the height of container, scrollTop=600-800 is still the
// exit range
container.scrollTop = 700;
await waitForNextFrame();
assert_equals(getComputedStyle(subject).opacity, '0.5', 'Effect at exit 50%');
assert_equals(getComputedStyle(subject).opacity, '0.5',
'Effect at exit 50% (post resize)');
});
}
</script>

View File

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="support/testcommon.js"></script>
<title>Animation range and delay</title>
</head>
<style type="text/css">
@keyframes anim {
cover 0% { /* resolves to -100% */
opacity: 0;
transform: none;
margin-left: 0px;
/* missing margin-right -- requires neutral keyframe at 0% */
}
cover 100% { /* resolves to 200% */
opacity: 1;
transform: translateX(300px);
margin-right: 0px;
/* missing margin-left -- requires neutral keyframe at 100% */
}
}
#scroller {
border: 10px solid lightgray;
overflow-y: scroll;
overflow-x: hidden;
width: 300px;
height: 200px;
}
#target {
margin: 800px 10px;
width: 100px;
height: 100px;
z-index: -1;
background-color: green;
animation: anim auto both linear;
animation-timeline: t1;
animation-range-start: contain 0%;
animation-range-end: contain 100%;
view-timeline: t1 block;
}
</style>
<body>
<div id=scroller>
<div id=target></div>
</div>
</body>
<script type="text/javascript">
async function runTest() {
function assert_progress_equals(anim, expected, errorMessage) {
assert_approx_equals(
anim.effect.getComputedTiming().progress,
expected, 1e-6, errorMessage);
}
function assert_opacity_equals(expected, errorMessage) {
assert_approx_equals(
parseFloat(getComputedStyle(target).opacity), expected, 1e-6,
errorMessage);
}
function assert_translate_x_equals(expected, errorMessage) {
const style = getComputedStyle(target).transform;
const regex = /matrix\(([^\)]*)\)/;
const captureGroupIndex = 1;
const translateIndex = 4;
const match = style.match(regex)[captureGroupIndex];
const translateX = parseFloat(match.split(',')[translateIndex].trim());
assert_approx_equals(translateX, expected, 1e-6, errorMessage);
}
function assert_property_equals(property, expected, errorMessage) {
const value = parseFloat(getComputedStyle(target)[property]);
assert_approx_equals(value, expected, 1e-6, errorMessage);
}
promise_test(async t => {
await waitForNextFrame();
const anim = document.getAnimations()[0];
await anim.ready;
await waitForNextFrame();
// @ contain 0%
scroller.scrollTop = 700;
await waitForNextFrame();
assert_progress_equals(anim, 0, 'progress at contain 0%');
assert_translate_x_equals(100, 'translation at contain 0%');
assert_opacity_equals(1/3, 'opacity at contain 0%');
assert_property_equals('margin-left', 5, 'margin-left at contain 0%');
assert_property_equals('margin-right', 10, 'margin-right at contain 0%');
// @ contain 50%
scroller.scrollTop = 750;
await waitForNextFrame();
assert_progress_equals(anim, 0.5, 'progress at contain 50%');
assert_translate_x_equals(150, 'translation at contain 50%');
assert_opacity_equals(0.5, 'opacity at contain 50%');
assert_property_equals('margin-left', 7.5, 'margin-left at contain 50%');
assert_property_equals('margin-right', 7.5, 'margin-right at contain 50%');
// @ contain 100%
scroller.scrollTop = 800;
await waitForNextFrame();
assert_progress_equals(anim, 1, 'progress at contain 100%');
assert_translate_x_equals(200, 'translation at contain 100%');
assert_opacity_equals(2/3, 'opacity at contain 100%');
assert_property_equals('margin-left', 10, 'margin-left at contain 100%');
assert_property_equals('margin-right', 5, 'margin-right at contain 100%');
}, 'ViewTimeline with timeline offset keyframes outside [0,1]');
}
window.onload = runTest;
</script>
</html>

View File

@ -16,10 +16,11 @@
* @param {Array.<ComputedKeyframe>} a - actual computed keyframes
* @param {Array.<ComputedKeyframe>} b - expected computed keyframes
*/
function assert_frame_lists_equal(a, b) {
assert_equals(a.length, b.length, 'number of frames');
function assert_frame_lists_equal(a, b, message) {
assert_equals(a.length, b.length, `number of frames: ${(message || '')}`);
for (let i = 0; i < Math.min(a.length, b.length); i++) {
assert_frames_equal(a[i], b[i], `ComputedKeyframe #${i}`);
assert_frames_equal(a[i], b[i],
`ComputedKeyframe #${i}: ${(message || '')}`);
}
}
@ -30,6 +31,9 @@ function assert_frames_equal(a, b, name) {
`properties on ${name} should match`);
// Iterates sorted keys to ensure stable failures.
for (const p of Object.keys(a).sort()) {
assert_equals(a[p], b[p], `value for '${p}' on ${name}`);
if (typeof a[p] == 'number')
assert_approx_equals(a[p], b[p], 1e-6, `value for '${p}' on ${name}`);
else
assert_equals(a[p], b[p], `value for '${p}' on ${name}`);
}
}