<!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">
#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;
}
</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);
}
async function runTimelineOffsetsInKeyframesTest(keyframes) {
const testcase = JSON.stringify(keyframes);
const anim = target.animate(keyframes, {
timeline: new ViewTimeline( { subject: target }),
rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
duration: 'auto', fill: 'both'
});
await anim.ready;
await waitForNextFrame();
// @ contain 0%
scroller.scrollTop = 700;
await waitForNextFrame();
assert_progress_equals(
anim, 0, `Testcase '${testcase}': progress at contain 0%`);
assert_opacity_equals(
1/3, `Testcase '${testcase}': opacity at contain 0%`);
// @ contain 50%
scroller.scrollTop = 750;
await waitForNextFrame();
assert_progress_equals(
anim, 0.5, `Testcase '${testcase}': progress at contain 50%`);
assert_opacity_equals(
0.5, `Testcase '${testcase}': opacity at contain 50%`);
// @ contain 100%
scroller.scrollTop = 800;
await waitForNextFrame();
assert_progress_equals(
anim, 1, `Testcase '${testcase}': progress at contain 100%`);
assert_opacity_equals(
2/3, `Testcase '${testcase}': opacity at contain 100%`);
anim.cancel();
}
async function runParseNumberOrPercentInKeyframesTest(keyframes) {
const anim = target.animate(keyframes, {
timeline: new ViewTimeline( { subject: target }),
rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
duration: 'auto', fill: 'both'
});
await anim.ready;
await waitForNextFrame();
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = maxScroll / 2;
await waitForNextFrame();
const testcase = JSON.stringify(keyframes);
assert_progress_equals(anim, 0.5, testcase);
assert_opacity_equals(0.5, testcase);
anim.cancel();
}
async function runInvalidKeyframesTest(keyframes) {
assert_throws_js(TypeError, () => {
target.animate(keyframes, {
timeline: new ViewTimeline( { subject: target }),
});
}, `Invalid keyframes test case "${JSON.stringify(keyframes)}"`);
}
promise_test(async t => {
// Test equivalent typed-OM and CSS representations of timeline offsets.
// Test array and object form for keyframes.
const keyframeTests = [
// BaseKeyframe form with offsets expressed as typed-OM.
[
{
offset: { rangeName: 'cover', offset: CSS.percent(0) },
opacity: 0
},
{
offset: { rangeName: 'cover', offset: CSS.percent(100) },
opacity: 1
}
],
// BaseKeyframe form with offsets expressed as CSS text.
[
{ offset: "cover 0%", opacity: 0 },
{ offset: "cover 100%", opacity: 1 }
],
// BasePropertyIndexedKeyframe form with offsets expressed as typed-OM.
{
opacity: [0, 1],
offset: [
{ rangeName: 'cover', offset: CSS.percent(0) },
{ rangeName: 'cover', offset: CSS.percent(100) }
]
},
// BasePropertyIndexedKeyframe form with offsets expressed as CSS text.
{ opacity: [0, 1], offset: [ "cover 0%", "cover 100%" ]}
];
for (let i = 0; i < keyframeTests.length; i++) {
await runTimelineOffsetsInKeyframesTest(keyframeTests[i]);
}
}, 'Timeline offsets in programmatic keyframes');
promise_test(async t => {
const keyframeTests = [
[{offset: "0.5", opacity: 0.5 }],
[{offset: "50%", opacity: 0.5 }],
[{offset: "calc(20% + 30%)", opacity: 0.5 }]
];
for (let i = 0; i < keyframeTests.length; i++) {
await runParseNumberOrPercentInKeyframesTest(keyframeTests[i]);
}
}, 'String offsets in programmatic keyframes');
promise_test(async t => {
const invalidKeyframeTests = [
// BasePropertyKefyrame:
[{ offset: { rangeName: 'somewhere', offset: CSS.percent(0) }}],
[{ offset: { rangeName: 'entry', offset: CSS.px(0) }}],
[{ offset: "here 0%" }],
[{ offset: "entry 3px" }],
// BasePropertyIndexedKeyframe with sequence:
{ offset: [{ rangeName: 'somewhere', offset: CSS.percent(0) }]},
{ offset: [{ rangeName: 'entry', offset: CSS.px(0) }]},
{ offset: ["here 0%"] },
{ offset: ["entry 3px" ]},
// BasePropertyIndexedKeyframe without sequence:
{ offset: { rangeName: 'somewhere', offset: CSS.percent(0) }},
{ offset: { rangeName: 'entry', offset: CSS.px(0) }},
{ offset: "here 0%" },
{ offset: "entry 3px" },
// <number> or <percent> as string:
[{ offset: "-1" }],
[{ offset: "2" }],
[{ offset: "-10%" }],
[{ offset: "110%" }],
{ offset: ["-1"], opacity: [0.5] },
{ offset: ["2"], opacity: [0.5] },
{ offset: "-1", opacity: 0.5 },
{ offset: "2", opacity: 0.5 },
// Extra stuff at the end.
[{ offset: "0.5 trailing nonsense" }],
[{ offset: "cover 50% eureka" }]
];
for( let i = 0; i < invalidKeyframeTests.length; i++) {
await runInvalidKeyframesTest(invalidKeyframeTests[i]);
}
}, 'Invalid timeline offset in programmatic keyframe throws');
promise_test(async t => {
const anim = target.animate([
{ offset: "cover 0%", opacity: 0 },
{ offset: "cover 100%", opacity: 1 }
], {
rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
duration: 10000, fill: 'both'
});
scroller.scrollTop = 750;
await anim.ready;
assert_opacity_equals(1, `Opacity with document timeline`);
anim.timeline = new ViewTimeline( { subject: target });
await anim.ready;
assert_progress_equals(anim, 0.5, `Progress at contain 50%`);
assert_opacity_equals(0.5, `Opacity at contain 50%`);
anim.timeline = document.timeline;
assert_false(anim.pending);
await waitForNextFrame();
assert_opacity_equals(1, `Opacity after resetting timeline`);
anim.cancel();
}, 'Timeline offsets in programmatic keyframes adjust for change in ' +
'timeline');
promise_test(async t => {
const anim = target.animate([], {
timeline: new ViewTimeline( { subject: target }),
rangeStart: { rangeName: 'contain', offset: CSS.percent(0) },
rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) },
duration: 'auto', fill: 'both'
});
await anim.ready;
await waitForNextFrame();
scroller.scrollTop = 750;
await waitForNextFrame();
assert_progress_equals(
anim, 0.5, `Progress at contain 50% before effect change`);
assert_opacity_equals(1, `Opacity at contain 50% before effect change`);
anim.effect = new KeyframeEffect(target, [
{ offset: "cover 0%", opacity: 0 },
{ offset: "cover 100%", opacity: 1 }
], { duration: 'auto', fill: 'both' });
await waitForNextFrame();
assert_progress_equals(
anim, 0.5, `Progress at contain 50% after effect change`);
assert_opacity_equals(0.5, `Opacity at contain 50% after effect change`);
}, 'Timeline offsets in programmatic keyframes resolved when updating ' +
'the animation effect');
}
// TODO(kevers): Add tests for getKeyframes once
// https://github.com/w3c/csswg-drafts/issues/8507 is resolved.
window.onload = runTest;
</script>
</html>