diff options
Diffstat (limited to 'testing/web-platform/tests/scroll-animations/view-timelines')
40 files changed, 4548 insertions, 0 deletions
diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/animation-events.html b/testing/web-platform/tests/scroll-animations/view-timelines/animation-events.html new file mode 100644 index 0000000000..b456794225 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/animation-events.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#events"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow: auto; + height: 200px; + width: 200px; + } + .spacer { + height: 400px; + } + #target { + background-color: green; + height: 100px; + } +</style> +<body> + <div id="container"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> +</body> +<script type="text/javascript"> + const keyframes = {transform: ['translateX(0)', 'translateX(100px)']}; + let target = document.getElementById('target'); + let scroller = document.querySelector('#container'); + let timeline = new ViewTimeline({subject: target}); + promise_test(async t => { + let animation = target.animate(keyframes, { + timeline, + fill: 'both' + }); + scroller.scrollTo({top: 0}); + await waitForCompositorReady(); + let finishedPromise = animation.finished; + let finished = false; + let finishEvents = 0; + finishedPromise.then(() => { + finished = true; + }); + animation.addEventListener('finish', () => { finishEvents++; }); + + scroller.scrollTo({top: 100}); + await waitForNextFrame(); + assert_false(finished, "Animation is not finished before starting"); + assert_equals(finishEvents, 0, "No finish event before scrolling"); + + scroller.scrollTo({top: 400}); + await waitForNextFrame(); + assert_false(finished, "Animation is not finished while active"); + assert_equals(finishEvents, 0, "No finish event while active"); + + scroller.scrollTo({top: 600}); + await waitForNextFrame(); + assert_true(finished, "Animation is finished after passing end"); + assert_equals(finishEvents, 1, "A finish event is generated after end"); + + scroller.scrollTo({top: 400}); + await waitForNextFrame(); + assert_not_equals(finishedPromise, animation.finished, + "A new finish promise is created when back in active range"); + finished = false; + animation.finished.then(() => { + finished = true; + }); + + scroller.scrollTo({top: 600}); + await waitForNextFrame(); + assert_true(finished, "Finishes after passing end"); + assert_equals(finishEvents, 2, "Another finish event is generated after end"); + animation.cancel(); + }, 'View timeline generates and resolves finish promises and events' ); + + +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html new file mode 100644 index 0000000000..beb380060e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time-vertical-rl.tentative.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline current-time with vertical-rl writing mode</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + writing-mode: vertical-rl; + overflow-x: scroll; + height: 200px; + width: 200px; + } + .spacer { + width: 800px; + } + #target { + background-color: green; + height: 100px; + width: 200px; + } +</style> +<body> + <div id="container"> + <div id="leading-space" class="spacer"></div> + <div id="target"></div> + <div id="trailing-space" class="spacer"></div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + container.scrollLeft = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, {axis: 'block'}); + const timeline = anim.timeline; + await anim.ready; + + // Initially before start-offset and animation effect is in the before + // phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect is inactive in the before phase'); + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollLeft = -600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to the midpoint of the animation. + container.scrollLeft = -800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at midpoint"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at midpoint"); + assert_equals(getComputedStyle(target).opacity,'0.5', + 'Effect at the midpoint of the active range'); + + // Advance to the end of the animation. + container.scrollLeft = -1000; + anim.effect.updateTiming({ fill: 'forwards' }); + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's currentTime at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at end offset"); + assert_equals(getComputedStyle(target).opacity, '0.7', + 'Opacity with fill forwards at effect end time'); + anim.effect.updateTiming({ fill: 'none' }); + assert_equals(getComputedStyle(target).opacity, '1', + 'Opacity with fill none at effect end time'); + + // Advance to the scroll limit. + container.scrollLeft = -1600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's currentTime at scroll limit"); + // Hold time set when the animation finishes, which clamps the value of + // the animation's currentTime. + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at scroll limit"); + // In the after phase, so the effect should not be applied. + assert_equals(getComputedStyle(target).opacity, '1', + 'After phase at scroll limit'); + }, 'View timeline with container having vertical-rl layout' ); + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html new file mode 100644 index 0000000000..c24d04412f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-current-time.tentative.html @@ -0,0 +1,207 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline current-time</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-y: scroll; + height: 200px; + width: 200px; + } + .spacer { + height: 800px; + } + #target { + background-color: green; + height: 200px; + width: 100px; + } +</style> +<body> + <div id="container"> + <div id="leading-space" class="spacer"></div> + <div id="target"></div> + <div id="trailing-space" class="spacer"></div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target); + const timeline = anim.timeline; + await anim.ready; + + // Initially before start-offset and animation effect is in the before + // phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect is inactive in the before phase'); + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollTop = 600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to the midpoint of the animation. + container.scrollTop = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at midpoint"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at midpoint"); + assert_equals(getComputedStyle(target).opacity,'0.5', + 'Effect at the midpoint of the active range'); + + // Advance to the end of the animation. + container.scrollTop = 1000; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's currentTime at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect is in the after phase at effect end time'); + + // Advance to the scroll limit. + container.scrollTop = 1600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's currentTime at scroll limit"); + // Hold time set when the animation finishes, which clamps the value of + // the animation's currentTime. + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at scroll limit"); + // In the after phase, so the effect should not be applied. + assert_equals(getComputedStyle(target).opacity, '1', + 'After phase at scroll limit'); + }, 'View timeline with start and end scroll offsets that do not align with ' + + 'the scroll boundaries' ); + + promise_test(async t => { + const leading = document.getElementById('leading-space'); + leading.style = 'display: none'; + t.add_cleanup(() => { + leading.style = null; + }); + + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target); + const timeline = anim.timeline; + await anim.ready; + + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "0.5", + 'Effect enters active phase at container start boundary'); + + + // Advance to midpoint + container.scrollTop = 100; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 75, + "Timeline's current time at midpoint"); + assert_percents_equal(anim.currentTime, 75, + "Animation's current time at midpoint"); + assert_equals(getComputedStyle(target).opacity, '0.6', + 'Effect at the middle of the active phase'); + + // Advance to end-offset + container.scrollTop = 200; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's current time at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's current time at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect inactive at the end offset'); + + // Advance to scroll limit. + container.scrollTop = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's current time at scroll limit"); + assert_percents_equal(anim.currentTime, 100, + "Animation's current time at scroll limit"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect inactive in the after phase'); + + }, 'View timeline does not clamp starting scroll offset at 0'); + + promise_test(async t => { + const trailing = document.getElementById('trailing-space'); + trailing.style = 'display: none'; + t.add_cleanup(() => { + trailing.style = null; + }); + + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target); + const timeline = anim.timeline; + await anim.ready; + + // Initially in before phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect enters active phase at container start boundary'); + + // Advance to start offset. + container.scrollTop = 600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to midpoint. + container.scrollTop = 700; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 25, + "Timeline's current time at the midpoint"); + assert_percents_equal(anim.currentTime, 25, + "Animation's current time at the midpoint"); + assert_equals(getComputedStyle(target).opacity, '0.4', + 'Effect at the midpoint of the active phase'); + + // Advance to end offset. + container.scrollTop = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at max scroll offset"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at max scroll offset"); + // The active-after boundary is inclusive since at the maximum scroll + // position. + assert_equals(getComputedStyle(target).opacity, "0.5", + 'Effect at end of active phase'); + }, 'View timeline does not clamp end scroll offset at max scroll'); + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html new file mode 100644 index 0000000000..6fdc7c6822 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/block-view-timeline-nested-subject.tentative.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline nested subject</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style type="text/css"> + #container { + overflow-y: scroll; + height: 300px; + width: 300px; + } + .big-spacer { + height: 800px; + } + .small-spacer { + height: 100px; + } + #block { + background-color: #ddd; + } + #target { + background-color: green; + height: 100px; + width: 100px; + } +</style> +<body> + <div id="container"> + <div class="big-spacer"></div> + <div id="block"> + <div class="small-spacer"></div> + <div id="target"></div> + </div> + <div class="big-spacer"></div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target); + const timeline = anim.timeline; + await anim.ready; + + // start offset = 800 + 100 - 300 = 600 + // end offset = 800 + 100 + 100 = 1000 + // scroll limit = L = 800 + 200 + 800 - 300 = 1500 + // progress = P = (current - start) / (end - start) + // P(0) = -600 / 400 = -1.5 + // P(L) = 900 / 400 = 2.5 + + // Initially before start-offset and animation effect is in the before + // phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect is inactive in the before phase'); + + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollTop = 600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to the midpoint of the animation. + container.scrollTop = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at midpoint"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at midpoint"); + assert_equals(getComputedStyle(target).opacity,'0.5', + 'Effect at the midpoint of the active range'); + + // Advance to the end of the animation. + container.scrollTop = 1000; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's currentTime at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect is in the after phase at effect end time'); + + // Advance to the scroll limit. + container.scrollTop = 1600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 225, + "Timeline's currentTime at scroll limit"); + // Hold time set when the animation finishes, which clamps the value of + // the animation's currentTime. + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at scroll limit"); + // In the after phase, so the effect should not be applied. + assert_equals(getComputedStyle(target).opacity, '1', + 'After phase at scroll limit'); + }, 'View timeline with subject that is not a direct descendant of the ' + + 'scroll container'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html b/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html new file mode 100644 index 0000000000..ee01070a53 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/change-animation-range-updates-play-state.html @@ -0,0 +1,88 @@ +<!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> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<title>Animation range updates play state</title> +</head> +<style type="text/css"> + @keyframes anim { + from { background-color: blue; } + to { background-color: white; } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + #target { + margin-top: 800px; + 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-timeline: --t1; + view-timeline: --t1; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + promise_test(async t => { + anim = target.getAnimations()[0]; + await anim.ready; + + // Cover range = 600px to 900px + + scroller.scrollTop = 750; + await waitForNextFrame(); + + // Animation is running in the active phase. + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = 'contain 0%'; // 700px + anim.rangeEnd = 'contain 100%'; // 800px + }); + assert_equals(anim.playState, 'running'); + assert_percents_equal(anim.startTime, 100/3); + assert_percents_equal(anim.currentTime, 100/6); + + // Animation in the after phase and switches to the finished state. + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = 'entry 0%'; // 600px + anim.rangeEnd = 'entry 100%'; // 700px + }); + assert_equals(anim.playState, 'finished'); + assert_percents_equal(anim.startTime, 0); + // In the after phase, so current time is clamped. + assert_percents_equal(anim.currentTime, 100/3); + + // Animation in the before phase and switches back to the running state. + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = 'exit 0%'; // 800px + anim.rangeEnd = 'exit 100%'; // 900px + }); + assert_equals(anim.playState, 'running'); + assert_percents_equal(anim.startTime, 200/3); + assert_percents_equal(anim.currentTime, -100/6); + + }, 'Changing the animation range updates the play state'); + } + + window.onload = runTest; +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/contain-alignment.html b/testing/web-platform/tests/scroll-animations/view-timelines/contain-alignment.html new file mode 100644 index 0000000000..8b61a9ab81 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/contain-alignment.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<html> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/"> +<style> + +@keyframes bg { + from { background-color: rgb(254, 0, 0); } + to { background-color: rgb(0 254, 0); } +} +.item { + flex-grow: 1; + width: 2em; + height: 2em; + background: #888; + animation: bg linear; + animation-timeline: view(); + animation-range: contain; +} + +.inline .item { + animation-timeline: view(inline); +} + +.scroller { + width: 10em; + height: 10em; + outline: 1px solid; + margin: 1em; + overflow: auto; + display: inline-flex; + vertical-align: top; + flex-direction: column; + gap: 1em; + resize: vertical; +} + +.inline { + resize: horizontal; + flex-direction: row; +} + +.block .spacer { + height: 20em; + width: 1em; +} + +.inline .spacer { + width: 20em; + height: 1em; +} +</style> +<body> +<div class="scroller block"> + <div class="item" id="a"></div> + <div class="item" id="b"></div> + <div class="item" id="c"></div> +</div> + +<div class="scroller inline"> + <div class="item" id="d"></div> + <div class="item" id="e"></div> + <div class="item" id="f"></div> +</div> + +<br> + +<div class="scroller block"> + <div class="item" id="g"></div> + <div class="item" id="h"></div> +</div> + +<div class="scroller inline"> + <div class="item" id="i"></div> + <div class="item" id="j"></div> +</div> +</body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script type="text/javascript"> + promise_test(async t => { + let anims = document.getAnimations(); + await Promise.all(anims.map(anim => anim.ready)); + await waitForNextFrame(); + + const expected_results = [ + { id: "a", progress: 1.0, bg: 'rgb(0, 254, 0)' }, + { id: "b", progress: 0.5, bg: 'rgb(127, 127, 0)' }, + { id: "c", progress: 0.0, bg: 'rgb(254, 0, 0)' }, + { id: "d", progress: 1.0, bg: 'rgb(0, 254, 0)' }, + { id: "e", progress: 0.5, bg: 'rgb(127, 127, 0)' }, + { id: "f", progress: 0.0, bg: 'rgb(254, 0, 0)' }, + { id: "g", progress: 1.0, bg: 'rgb(0, 254, 0)' }, + { id: "h", progress: 0.0, bg: 'rgb(254, 0, 0)' }, + { id: "i", progress: 1.0, bg: 'rgb(0, 254, 0)' }, + { id: "j", progress: 0.0, bg: 'rgb(254, 0, 0)' } + ]; + + expected_results.forEach(result => { + const element = document.getElementById(result.id); + const anim = element.getAnimations()[0]; + assert_approx_equals(anim.effect.getComputedTiming().progress, + result.progress, 1e-3, + `${result.id}: Unexpected progress`); + assert_equals(getComputedStyle(element).backgroundColor, + result.bg, `${result.id}: Mismatched background color`); + }); + + }, 'Stability of animated elements aligned to the bounds of a contain region'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/fieldset-source.html b/testing/web-platform/tests/scroll-animations/view-timelines/fieldset-source.html new file mode 100644 index 0000000000..d75f30e664 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/fieldset-source.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<html> +<meta charset="utf-8"> +<title>View timeline with fieldset as source</title> +<link rel="help" href="https://www.w3.org/TR/scroll-animations-1/#dom-viewtimeline-viewtimeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> + @keyframes colorize { + from { background-color: #ccf; } + to { background-color: white; } + } + + .input { + background-color: white; + view-timeline: --timeline; + animation: colorize; + animation-timeline: --timeline; + margin-top: 0px; + margin-bottom: 3px; + margin-left: 8px; + height: 20px; + width: 150px; + } + + .input:last-child { + margin-bottom: 0px; + } + + fieldset { + display: inline-block; + overflow-x: hidden; + overflow-y: scroll; + height: 80px; + } + + div { + display: flex; + justify-content: flex-end; + align-items: center; + } +</style> +<body> + <fieldset id="fieldset"> + <legend id="legend">Reservation Details</legend> + <div> + <label for="name">Name: </label> + <input type="text" class="input" id="input1" value="Jane Doe" /> + </div> + <div> + <label for="date">Date: </label> + <input type="date" class="input" id="input2" value="2024-01-16"/> + </div> + <div> + <label for="time">Time: </label> + <input type="time" class="input" id="input3" value="18:30"/> + </div> + <div> + <label for="name">Number of guests: </label> + <input type="number" class="input" id="input4" value="5" /> + </div> + <div> + <label for="name">Contact info: </label> + <input type="text" class="input" id="input5" value="(555) 555-5555" /> + </div> + </fieldset> +</body> +<script> + async function runTest() { + promise_test(async t => { + const anims = document.getAnimations(); + assert_equals(anims.length, 5); + await Promise.all(anims.map(anim => anim.ready)); + + // The bottom of the legend aligns with the top of the fieldset's + // scrollable area. + const fieldset = document.getElementById('fieldset'); + const legend = document.getElementById('legend'); + const fieldsetContentTop = + legend.getBoundingClientRect().bottom; + + // The bottom of the scroll container aligns with the bottom of the + // fieldset's content box. + const fieldsetContentBottom = + fieldset.getBoundingClientRect().bottom - + parseFloat(getComputedStyle(fieldset).borderBottom); + + // Validate the start and end offsets for each view timeline. + anims.forEach(async (anim) => { + assert_equals(anim.timeline.source.id, 'fieldset'); + assert_equals(anim.timeline.subject.tagName, 'INPUT'); + const bounds = anim.effect.target.getBoundingClientRect(); + + const expectedStartOffset = bounds.top - fieldsetContentBottom; + const expectedEndOffset = bounds.bottom - fieldsetContentTop; + assert_approx_equals( + parseFloat(anim.timeline.startOffset), + expectedStartOffset, 0.1, + `Unexpected start offset for ${anim.effect.target.id}`); + assert_approx_equals( + parseFloat(anim.timeline.endOffset), + expectedEndOffset, 0.1, + `Unexpected end offset for ${anim.effect.target.id}`); + }); + }, 'Fieldset is a valid source for a view timeline'); + } + + window.onload = runTest(); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html b/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html new file mode 100644 index 0000000000..02f910d04e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html @@ -0,0 +1,203 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<!-- TODO(kevers): Insert link once resolutions present in spec --> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/web-animations/resources/keyframe-utils.js"></script> +<script src="support/testcommon.js"></script> +<title>Reported keyframes containing timeline offset</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 createAnimation(t, keyframes, use_view_timeline = true) { + const options = { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + duration: 'auto', + fill: 'both' + }; + if (use_view_timeline) { + options.timeline = new ViewTimeline( { subject: target }); + } + const anim = target.animate(keyframes, options); + t.add_cleanup(() => { + anim.cancel(); + }); + return anim; + } + + promise_test(async t => { + let anim = createAnimation(t, [ + { offset: "contain 25%", marginLeft: "0px", opacity: "0" }, + { offset: "contain 75%", marginRight: "0px", opacity: "1" } + ]); + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: { rangeName: 'contain', offset: CSS.percent(25) }, + computedOffset: 0.25, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'contain', offset: CSS.percent(75) }, + computedOffset: 0.75, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Report specified timeline offsets'); + + promise_test(async t => { + let anim = createAnimation(t, [ + { offset: "cover 0%", marginLeft: "0px", opacity: "0" }, + { offset: "cover 100%", marginRight: "0px", opacity: "1" } + ]); + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: -1, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: 2, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Computed offsets can be outside [0,1] for keyframes with timeline ' + + 'offsets'); + + promise_test(async t => { + let anim = createAnimation(t, [ + { offset: "contain 75%", marginLeft: "0px", opacity: "0" }, + { offset: "contain 25%", marginRight: "0px", opacity: "1" } + ]); + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: { rangeName: 'contain', offset: CSS.percent(75) }, + computedOffset: 0.75, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'contain', offset: CSS.percent(25) }, + computedOffset: 0.25, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Retain specified ordering of keyframes with timeline offsets'); + + promise_test(async t => { + let anim = createAnimation(t, [ + { offset: "cover 0%", marginLeft: "0px", opacity: "0" }, + { offset: "cover 100%", marginRight: "0px", opacity: "1" } + ], /* use_view_timeline */ false); + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: null, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: null, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Include unreachable keyframes'); + + + promise_test(async t => { + let anim = createAnimation(t, [ + { offset: "cover 0%", marginLeft: "0px", opacity: 0 }, + { offset: "cover 100%", marginRight: "0px", opacity: 1 }, + { opacity: 0 }, + { opacity: 0.5 }, + { opacity: 1.0 } + ]); + let frames = anim.effect.getKeyframes(); + let expected = [ + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: -1, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: 2, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" }, + { offset: null, computedOffset: 0, easing: "linear", composite: "auto", + opacity: "0" }, + { offset: null, computedOffset: 0.5, easing: "linear", + composite: "auto", opacity: "0.5" }, + { offset: null, computedOffset: 1.0, easing: "linear", + composite: "auto", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + + anim = createAnimation(t, [ + { opacity: 0 }, + { offset: "cover 0%", marginLeft: "0px", opacity: 0 }, + { opacity: 0.5 }, + { offset: "cover 100%", marginRight: "0px", opacity: 1 }, + { opacity: 1.0 } + ]); + frames = anim.effect.getKeyframes(); + expected = [ + { offset: null, computedOffset: 0, easing: "linear", composite: "auto", + opacity: "0" }, + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: -1, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: null, computedOffset: 0.5, easing: "linear", + composite: "auto", opacity: "0.5" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: 2, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" }, + { offset: null, computedOffset: 1.0, easing: "linear", + composite: "auto", opacity: "1" } + ]; + assert_frame_lists_equal(frames, expected); + + anim = createAnimation(t, [ + { opacity: 0.2, offset: 0.2 }, + { offset: "cover 0%", marginLeft: "0px", opacity: 0 }, + { opacity: 0.4 }, + { opacity: 0.6 }, + { offset: "cover 100%", marginRight: "0px", opacity: 1 }, + { opacity: 0.8, offset: 0.8 } + ]); + frames = anim.effect.getKeyframes(); + expected = [ + { offset: 0.2, computedOffset: 0.2, easing: "linear", composite: "auto", + opacity: "0.2" }, + { offset: { rangeName: 'cover', offset: CSS.percent(0) }, + computedOffset: -1, easing: "linear", composite: "auto", + marginLeft: "0px", opacity: "0" }, + { offset: null, computedOffset: 0.4, easing: "linear", + composite: "auto", opacity: "0.4" }, + { offset: null, computedOffset: 0.6, easing: "linear", + composite: "auto", opacity: "0.6" }, + { offset: { rangeName: 'cover', offset: CSS.percent(100) }, + computedOffset: 2, easing: "linear", composite: "auto", + marginRight: "0px", opacity: "1" }, + { offset: 0.8, computedOffset: 0.8, easing: "linear", composite: "auto", + opacity: "0.8" } + ]; + assert_frame_lists_equal(frames, expected); + }, 'Mix of computed and timeline offsets.'); + } + + window.onload = runTest; +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/inline-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/inline-subject.html new file mode 100644 index 0000000000..6b1d216dea --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/inline-subject.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>View Timeline attached to an SVG graphics element</title> +</head> +<style type="text/css"> + @keyframes bg { + from { background-color: blue; } + to { background-color: green; } + } + + #colorize { + animation: bg steps(2, jump-none) both; + animation-timeline: view(); + animation-range: contain; + background-color: red; + color: white; + } + + .spacer { + height: 80vh; + } +</style> +<body> +<div class="spacer"></div> +<div id="content"> + <p>Hello <span id="colorize">world</span></p> +</div> +<div class="spacer"></div> +</body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script> + promise_test(async t => { + const scroller = document.scrollingElement; + const anim = document.getAnimations()[0]; + await anim.ready; + assert_equals(getComputedStyle(anim.effect.target) + .backgroundColor, 'rgb(0, 0, 255)'); + scroller.scrollTop = + scroller.scrollHeight - scroller.clientHeight; + await waitForNextFrame(); + assert_equals(getComputedStyle(anim.effect.target) + .backgroundColor, 'rgb(0, 128, 0)'); + }, 'View timeline attached to SVG graphics element'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html new file mode 100644 index 0000000000..59d73d0cdf --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html @@ -0,0 +1,302 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline current-time</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 200px; + display: inline-block; + } +</style> +<body> + <div id="container"> + <div id="content"> + <div id="leading-space" class="spacer"></div> + <div id="target"></div> + <div id="trailing-space" class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + container.scrollLeft = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, + { + timeline: + {axis: 'inline'} + }); + const timeline = anim.timeline; + await anim.ready; + + // Initially before start-offset and animation effect is in the before + // phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect is inactive in the before phase'); + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollLeft = 600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to the midpoint of the animation. + container.scrollLeft = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at midpoint"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at midpoint"); + assert_equals(getComputedStyle(target).opacity,'0.5', + 'Effect at the midpoint of the active range'); + + // Advance to the end of the animation. + container.scrollLeft = 1000; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's currentTime at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect is in the after phase at effect end time'); + + // Advance to the scroll limit. + container.scrollLeft = 1600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's currentTime at scroll limit"); + // Hold time set when the animation finishes, which clamps the value of + // the animation's currentTime. + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at scroll limit"); + // In the after phase, so the effect should not be applied. + assert_equals(getComputedStyle(target).opacity, '1', + 'After phase at scroll limit'); + }, 'View timeline with start and end scroll offsets that do not align with ' + + 'the scroll boundaries' ); + + promise_test(async t => { + const leading = document.getElementById('leading-space'); + leading.style = 'display: none'; + content.style = 'width: 1000px'; + t.add_cleanup(() => { + leading.style = null; + content.style = null; + }); + + container.scrollLeft = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, + { + timeline: + {axis: 'inline'} + }); + const timeline = anim.timeline; + await anim.ready; + + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "0.5", + 'Effect enters active phase at container start boundary'); + + + // Advance to midpoint + container.scrollLeft = 100; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 75, + "Timeline's current time at the midpoint"); + assert_percents_equal(anim.currentTime, 75, + "Animation's current time at the midpoint"); + assert_equals(getComputedStyle(target).opacity, '0.6', + 'Effect at the midpoint of the active phase'); + + // Advance to end-offset + container.scrollLeft = 200; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's current time at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's current time at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect at the end of the active phase'); + + // Advance to scroll limit. + container.scrollLeft = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's current time at the scroll limit"); + assert_percents_equal(anim.currentTime, 100, + "Animation's current time at the scroll limit"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect at the scroll limit'); + + }, 'View timeline does not clamp starting scroll offset at 0'); + + promise_test(async t => { + const trailing = document.getElementById('trailing-space'); + trailing.style = 'display: none'; + content.style = 'width: 1000px'; + t.add_cleanup(() => { + trailing.style = null; + content.style = null; + }); + + container.scrollLeft = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, + { + timeline: + {axis: 'inline'} + }); + const timeline = anim.timeline; + await anim.ready; + + // Initially in before phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect enters active phase at container start boundary'); + + // Advance to start offset. + container.scrollLeft = 600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to midpoint + container.scrollLeft = 700; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 25, + "Timeline's current time at midpoint"); + assert_percents_equal(anim.currentTime, 25, + "Animation's current time at midpoint"); + assert_equals(getComputedStyle(target).opacity, '0.4', + 'Effect at the midpoint of the active phase'); + + // Advance to end offset. + container.scrollLeft = 800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at max scroll offset"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at max scroll offset"); + // The active-after boundary is inclusive since at the scroll-limit. + assert_equals(getComputedStyle(target).opacity, "0.5", + 'Effect at end of active phase'); + }, 'View timeline does not clamp end scroll offset at max scroll'); + + + promise_test(async t => { + container.style = "direction: rtl"; + container.scrollLeft = 0; + t.add_cleanup(() => { + content.style = null; + }); + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, + { + timeline: + {axis: 'inline'} + }); + const timeline = anim.timeline; + await anim.ready; + + // Initially before start-offset and animation effect is in the before + // phase. + assert_percents_equal(timeline.currentTime, -150, + "Timeline's currentTime at container start boundary"); + assert_percents_equal(anim.currentTime, -150, + "Animation's currentTime at container start boundary"); + assert_equals(getComputedStyle(target).opacity, "1", + 'Effect is inactive in the before phase'); + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollLeft = -600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 0, + "Timeline's current time at start offset"); + assert_percents_equal(anim.currentTime, 0, + "Animation's current time at start offset"); + assert_equals(getComputedStyle(target).opacity, '0.3', + 'Effect at the start of the active phase'); + + // Advance to the midpoint of the animation. + container.scrollLeft = -800; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 50, + "Timeline's currentTime at midpoint"); + assert_percents_equal(anim.currentTime, 50, + "Animation's currentTime at midpoint"); + assert_equals(getComputedStyle(target).opacity,'0.5', + 'Effect at the midpoint of the active range'); + + // Advance to the end of the animation. + container.scrollLeft = -1000; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 100, + "Timeline's currentTime at end offset"); + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at end offset"); + assert_equals(getComputedStyle(target).opacity, '1', + 'Effect is in the after phase at effect end time'); + + // Advance to the scroll limit. + container.scrollLeft = -1600; + await waitForNextFrame(); + assert_percents_equal(timeline.currentTime, 250, + "Timeline's currentTime at scroll limit"); + // Hold time set when the animation finishes, which clamps the value of + // the animation's currentTime. + assert_percents_equal(anim.currentTime, 100, + "Animation's currentTime at scroll limit"); + // In the after phase, so the effect should not be applied. + assert_equals(getComputedStyle(target).opacity, '1', + 'After phase at scroll limit'); + }, 'View timeline with container having RTL layout' ); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary-ref.html b/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary-ref.html new file mode 100644 index 0000000000..057d0afabc --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary-ref.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title></title> +</head> +<style type="text/css"> + .scroller { + display: inline-block; + border: 2px solid black; + height: 100px; + width: 100px; + overflow: hidden; + } + .box { + background: gray; + height: 50px; + width: 50px; + margin: 0; + } + .half-shift { + transform: translateX(25px); + } + .full-shift { + transform: translateX(50px); + } + .blue { + background-color: #99f; + } + .green { + background-color: #9f9; + } +</style> +<body> + <div id="scroller-1" class="scroller"> + <div class="box green"></div> + <div class="box blue full-shift"></div> + </div> + <div id="scroller-2" class="scroller"> + <div class="box"></div> + <div class="box blue"></div> + </div> + <br> + <div id="scroller-3" class="scroller"> + <div class="box"></div> + <div class="box blue"></div> + </div> + <div id="scroller-4" class="scroller"> + <div class="box"></div> + <div class="box green"></div> + </div> + <br> + <div id="scroller-5" class="scroller"> + <div class="box blue"></div> + <div class="box half-shift green"></div> + </div> + <div id="scroller-6" class="scroller"> + <div class="box"></div> + <div class="box green"></div> + </div> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary.html b/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary.html new file mode 100644 index 0000000000..e2ca394ec0 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/range-boundary.html @@ -0,0 +1,153 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="match" href="range-boundary-ref.html"> + <title></title> +</head> +<style type="text/css"> + @keyframes transform { + 0% { transform: translateX(25px); } + 100% { transform: translateX(50px); } + } + + @keyframes background { + 0% { background-color: #99f; } + 100% { background-color: #9f9; } + } + + .scroller { + display: inline-block; + border: 2px solid black; + height: 100px; + width: 100px; + overflow: hidden; + } + .spacer { + height: 300px; + margin: 0; + } + .box { + background: gray; + height: 50px; + width: 50px; + margin: 0; + animation: transform auto, background auto; + animation-timeline: view(), view(); + animation-range: entry 0% entry 100%, contain 0% contain 100%; + } +</style> +<body> + <!-- scroll to bottom + top-box: + transform: none (after phase) + bg-color: #9f9 (at active-after boundary with inclusive endpoint) + bottom-box: + transform: 100px (at active-after boundary with inclusive endpoint) + bg-color: #99f (at active-before boundary with inclusive endpoint) + --> + <div id="scroller-1" class="scroller"> + <div class="spacer"></div> + <div class="box"></div> + <div class="box"></div> + </div> + <!-- scroll to top + top-box: + transform: none (after phase) + bg-color: gray (at active-after boundary with exclusive endpoint) + bottom-box: + transform: none (at active-after boundary with exclusive endpoint) + bg-color: #99f (at active-before boundary with inclusive endpoint) + --> + <div id="scroller-2" class="scroller"> + <div class="box"></div> + <div class="box"></div> + <div class="spacer"></div> + </div> + <br> + <!-- scroll to midpoint + top-box: + transform: none (after phase) + bg-color: gray (at active-after boundary with exclusive endpoint) + bottom-box: + transform: none (at active-after boundary with exclusive endpoint) + bg-color: #99f (at active-before boundary with inclusive endpoint) + --> + <div id="scroller-3" class="scroller"> + <div class="spacer"></div> + <div class="box"></div> + <div class="box"></div> + <div class="spacer"></div> + </div> + <!-- scroll to bottom + reverse + top-box: + transform: none (before phase) + bg-color: gray (at active-before boundary with exclusive endpoint) + bottom-box: + transform: none (at active-before boundary with exclusive endpoint) + bg-color: #9f9 (at active-after boundary with inclusive endpoint) + --> + <div id="scroller-4" class="scroller"> + <div class="spacer"></div> + <div class="box reverse"></div> + <div class="box reverse"></div> + </div> + <br> + <!-- scroll to top + reverse + top-box: + transform: none (before phase) + bg-color: #99f (at active-before boundary with inclusive endpoint) + bottom-box: + transform: 25px (at active-before boundary with inclusive endpoint) + bg-color: #9f9 (at active-after boundary with inclusive endpoint) + --> + <div id="scroller-5" class="scroller"> + <div class="box reverse"></div> + <div class="box reverse"></div> + <div class="spacer"></div> + </div> + <!-- scroll to midpoint + reverse + top-box: + transform: none (before phase) + bg-color: gray (at active-before boundary with exclusive endpoint) + bottom-box: + transform: none (at active-before boundary with exclusive endpoint) + bg-color: #9f9 (at active-before boundary with inclusive endpoint) + --> + <div id="scroller-6" class="scroller"> + <div class="spacer"></div> + <div class="box reverse"></div> + <div class="box reverse"></div> + <div class="spacer"></div> + </div> +</body> +<script src="/common/reftest-wait.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script> + function scrollTo(scroller_id, relative_offset) { + const scroller = document.getElementById(scroller_id); + const max_scroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = relative_offset * max_scroll; + } + + window.onload = async () => { + await waitForCompositorReady(); + document.querySelectorAll('.reverse').forEach(elem => { + elem.getAnimations().forEach(anim => { + anim.reverse(); + }); + }); + // Playing forward + scrollTo('scroller-1', 1); + scrollTo('scroller-2', 0); + scrollTo('scroller-3', 0.5); + // Playing reverse + scrollTo('scroller-4', 1); + scrollTo('scroller-5', 0); + scrollTo('scroller-6', 0.5); + await waitForNextFrame(); + takeScreenshot(); + }; +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-1.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-1.html new file mode 100644 index 0000000000..d8756769c5 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-1.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* top-sticky during entry */ +.stickycase1 { + background: yellow; + position: sticky; + top: 400px; + height: 200px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 100px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase1"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const STATIC_END = 850; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const TARGET_HEIGHT = 100; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline top-sticky during entry.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-2.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-2.html new file mode 100644 index 0000000000..2d098dcbe3 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-2.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* bottom-sticky during entry and top-sticky during exit */ +.stickycase2 { + background: yellow; + position: sticky; + top: -100px; + bottom: -100px; + height: 200px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 100px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase2"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const STATIC_END = 850; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const TARGET_HEIGHT = 100; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + TARGET_HEIGHT, + endOffset: STATIC_END - TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: STATIC_END - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_END - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline bottom-sticky during entry and top-sticky during exit.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-3.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-3.html new file mode 100644 index 0000000000..c87dfc4dcb --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-3.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* top-sticky and bottom-sticky during entry */ +.stickycase3 { + background: yellow; + position: sticky; + top: 375px; + bottom: -125px; + height: 200px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 100px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase3"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const STATIC_END = 850; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const TARGET_HEIGHT = 100; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline top-sticky and bottom-sticky during entry.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.html new file mode 100644 index 0000000000..f6b02ffb2e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* top-sticky before entry */ +.stickycase4 { + background: yellow; + position: sticky; + top: 600px; + height: 200px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 100px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase4"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const STATIC_END = 850; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const TARGET_HEIGHT = 100; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START + ROOM_BELOW, + endOffset: STATIC_START + ROOM_BELOW + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_END + ROOM_BELOW - TARGET_HEIGHT, + endOffset: STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline top-sticky before entry.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html new file mode 100644 index 0000000000..380c01297e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* bottom-sticky before entry and top-sticky after exit */ +.stickycase5 { + background: yellow; + position: sticky; + top: -200px; + bottom: -200px; + height: 200px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 100px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase5"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const STATIC_END = 850; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const TARGET_HEIGHT = 100; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_END, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + TARGET_HEIGHT, + endOffset: STATIC_END - TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_START + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START, + endOffset: STATIC_START + TARGET_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: STATIC_END - TARGET_HEIGHT, + endOffset: STATIC_END, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_END - TARGET_HEIGHT, + endOffset: STATIC_END, + axis: 'block' + }); +}, 'View timeline bottom-sticky before entry and top-sticky after exit.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.html new file mode 100644 index 0000000000..94f0abc9b1 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* target > viewport, bottom-sticky during entry and top-sticky during exit */ +.stickycase6 { + background: yellow; + position: sticky; + top: -200px; + bottom: -200px; + height: 700px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 600px; +} + +.space:has(.stickycase6), +.space:has(.stickycase7) { + height: 1050px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase6"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const BIG_TARGET_STATIC_END = 1350; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const BIG_TARGET_HEIGHT = 600; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START + VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END - VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START + VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: BIG_TARGET_STATIC_END - VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: BIG_TARGET_STATIC_END - VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START + VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline target > viewport, ' + + 'bottom-sticky during entry and top-sticky during exit.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html new file mode 100644 index 0000000000..83115249fa --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky during entry/exit</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 550px; +} + +/* target > viewport, bottom-sticky and top-sticky during contain */ +.stickycase7 { + background: yellow; + position: sticky; + top: -100px; + bottom: -100px; + height: 700px; +} + +#target { + position: relative; + top: 50px; + background: orange; + height: 600px; +} + +.space:has(.stickycase6), +.space:has(.stickycase7) { + height: 1050px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 150px"></div> + <div id="sticky" class="stickycase7"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +// The "cover" range would be [STATIC_START, STATIC_END] if we ignored +// stickiness (i.e., considered only static position). +// +// STATIC_START = scroll distance to second spacer (50px) +// + position of sticky element within its container (150px) +// + position of target within sticky element (50px) +// STATIC_END = STATIC_START +// + viewport height (500px) +// + target height (100px) +const STATIC_START = 250; +const BIG_TARGET_STATIC_END = 1350; + +// This is how far the sticky element can move upwards when bottom-stuck. +const ROOM_ABOVE = 150; + +// This is how far the sticky element can move downwards when top-stuck. +const ROOM_BELOW = 200; + +const BIG_TARGET_HEIGHT = 600; +const VIEWPORT_HEIGHT = 500; + +promise_test(async t => { + sticky.className = "stickycase7"; + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE + VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW - VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: STATIC_START - ROOM_ABOVE + VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW - VIEWPORT_HEIGHT, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: BIG_TARGET_STATIC_END + ROOM_BELOW - VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: STATIC_START - ROOM_ABOVE + VIEWPORT_HEIGHT, + endOffset: BIG_TARGET_STATIC_END + ROOM_BELOW, + axis: 'block' + }); +}, 'View timeline target > viewport, ' + + 'bottom-sticky and top-sticky during contain.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/subject-br-crash.html b/testing/web-platform/tests/scroll-animations/view-timelines/subject-br-crash.html new file mode 100644 index 0000000000..36627dbea6 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/subject-br-crash.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<link rel="help" href="https://www.w3.org/TR/scroll-animations-1/#viewtimeline-interface"> +<html> +<!-- crbug.com/1470522 ---> +<script> + function main() { + var b = document.createElement("br"); + document.body.append(b); + new ViewTimeline({ subject: b }); + } +</script> +<body onload=main()> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-001.html b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-001.html new file mode 100644 index 0000000000..9b100a0b64 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-001.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>View Timeline attached to an SVG graphics element</title> +</head> +<style type="text/css"> + @keyframes stroke { + from { stroke: rgb(0, 0, 254); } + to { stroke: rgb(0, 128, 0); } + } + + #line { + animation: stroke auto linear both; + animation-timeline: view(); + animation-range: exit-crossing; + } + .spacer { + height: 100vh; + } +</style> +<body> +<svg width="100" height="3000" stroke="red" stroke-width="5"> + <path id="line" d="M 50 0 V 3000"></path> +</svg> +<div class="spacer"></div> +</body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script> + promise_test(async t => { + const scroller = document.scrollingElement; + const target = document.getElementById('line'); + const anim = target.getAnimations()[0]; + await anim.ready; + assert_equals(getComputedStyle(target).stroke, 'rgb(0, 0, 254)'); + scroller.scrollTop = + 0.5*(scroller.scrollHeight - scroller.clientHeight); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).stroke, 'rgb(0, 64, 127)'); + }, 'View timeline attached to SVG graphics element'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-002.html b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-002.html new file mode 100644 index 0000000000..e173a649ef --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-002.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>View Timeline attached to an SVG graphics element in a nested <svg></title> +</head> +<style type="text/css"> + @keyframes stroke { + from { stroke: rgb(0, 0, 254); } + to { stroke: rgb(0, 128, 0); } + } + + #line { + animation: stroke auto linear both; + animation-timeline: view(); + animation-range: exit-crossing; + } + .spacer { + height: 100vh; + } +</style> +<body> +<svg width="100" height="3000" stroke="red" stroke-width="5"> + <svg> + <path id="line" d="M 50 0 V 3000"></path> + </svg> +</svg> +<div class="spacer"></div> +</body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script> + promise_test(async t => { + const scroller = document.scrollingElement; + const target = document.getElementById('line'); + const anim = target.getAnimations()[0]; + await anim.ready; + assert_equals(getComputedStyle(target).stroke, 'rgb(0, 0, 254)'); + scroller.scrollTop = + 0.5*(scroller.scrollHeight - scroller.clientHeight); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).stroke, 'rgb(0, 64, 127)'); + }, 'View timeline attached to SVG graphics element'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-003.html b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-003.html new file mode 100644 index 0000000000..48e238c8ed --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/svg-graphics-element-003.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>View Timeline attached to an SVG graphics element (<foreignObject>)</title> +</head> +<style type="text/css"> + @keyframes color { + from { color: rgb(0, 0, 254); } + to { color: rgb(0, 128, 0); } + } + + #fo { + animation: color auto linear both; + animation-timeline: view(); + animation-range: exit-crossing; + } + .spacer { + height: 100vh; + } +</style> +<body> +<svg width="100" height="3000" color="red"> + <foreignObject id="fo" x="47.5" width="3000" height="5" + transform="rotate(90, 47.5, 0)"> + <div style="width: 100%; height: 200%; background-color: currentcolor"></div> + </foreignObject> +</svg> +<div class="spacer"></div> +</body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script> + promise_test(async t => { + const scroller = document.scrollingElement; + const target = document.getElementById('fo'); + const anim = target.getAnimations()[0]; + await anim.ready; + assert_equals(getComputedStyle(target).color, 'rgb(0, 0, 254)'); + scroller.scrollTop = + 0.5*(scroller.scrollHeight - scroller.clientHeight); + await waitForNextFrame(); + assert_equals(getComputedStyle(target).color, 'rgb(0, 64, 127)'); + }, 'View timeline attached to SVG graphics element'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js b/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js new file mode 100644 index 0000000000..a798fe918d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/testcommon.js @@ -0,0 +1,146 @@ +'use strict'; + +function assert_px_equals(observed, expected, description) { + assert_equals(observed.unit, 'px', + `Unexpected unit type for '${description}'`); + assert_approx_equals(observed.value, expected, 0.0001, + `Unexpected value for ${description}`); +} + +function CreateViewTimelineOpacityAnimation(test, target, options) { + const timeline_options = { + subject: target, + axis: 'block' + }; + if (options && 'timeline' in options) { + for (let key in options.timeline) { + timeline_options[key] = options.timeline[key]; + } + } + const animation_options = { + timeline: new ViewTimeline(timeline_options) + }; + if (options && 'animation' in options) { + for (let key in options.animation) { + animation_options[key] = options.animation[key]; + } + } + + const anim = + target.animate({ opacity: [0.3, 0.7] }, animation_options); + test.add_cleanup(() => { + anim.cancel(); + }); + return anim; +} + +// Verify that range specified in the options aligns with the active range of +// the animation. +// +// Sample call: +// await runTimelineBoundsTest(t, { +// timeline: { inset: [ CSS.percent(0), CSS.percent(20)] }, +// timing: { fill: 'both' } +// startOffset: 600, +// endOffset: 900 +// }); +async function runTimelineBoundsTest(t, options, message) { + const scrollOffsetProp = options.axis == 'block' ? 'scrollTop' : 'scrollLeft'; + container[scrollOffsetProp] = 0; + await waitForNextFrame(); + + const anim = + options.anim || + CreateViewTimelineOpacityAnimation(t, target, options); + if (options.timing) + anim.effect.updateTiming(options.timing); + + const timeline = anim.timeline; + await anim.ready; + + // Advance to the start offset, which triggers entry to the active phase. + container[scrollOffsetProp] = options.startOffset; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity, '0.3', + `Effect at the start of the active phase: ${message}`); + + // Advance to the midpoint of the animation. + container[scrollOffsetProp] = (options.startOffset + options.endOffset) / 2; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity,'0.5', + `Effect at the midpoint of the active range: ${message}`); + + // Advance to the end of the animation. + container[scrollOffsetProp] = options.endOffset; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity, '0.7', + `Effect is in the active phase at effect end time: ${message}`); + + // Return the animation so that we can continue testing with the same object. + return anim; +} + +// Sets the start and end range for a view timeline and ensures that the +// range aligns with expected values. +// +// Sample call: +// await runTimelineRangeTest(t, { +// rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , +// rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, +// startOffset: 600, +// endOffset: 900 +// }); +async function runTimelineRangeTest(t, options) { + const rangeToString = range => { + const parts = []; + if (range.rangeName) + parts.push(range.rangeName); + if (range.offset) + parts.push(`${range.offset.value}%`); + return parts.join(' '); + }; + const range = + `${rangeToString(options.rangeStart)} to ` + + `${rangeToString(options.rangeEnd)}`; + + options.timeline = { + axis: options.axis || 'inline' + }; + options.animation = { + rangeStart: options.rangeStart, + rangeEnd: options.rangeEnd, + }; + options.timing = { + // Set fill to accommodate floating point precision errors at the + // endpoints. + fill: 'both' + }; + + return runTimelineBoundsTest(t, options, range); +} + +// Sets the Inset for a view timeline and ensures that the range aligns with +// expected values. +// +// Sample call: +// await runTimelineInsetTest(t, { +// inset: [ CSS.px(20), CSS.px(40) ] +// startOffset: 600, +// endOffset: 900 +// }); +async function runTimelineInsetTest(t, options) { + options.timeline = { + axis: 'inline', + inset: options.inset + }; + options.timing = { + // Set fill to accommodate floating point precision errors at the + // endpoints. + fill: 'both' + } + const length = options.inset.length; + const range = + (options.inset instanceof Array) ? options.inset.join(' ') + : options.inset; + return runTimelineBoundsTest(t, options, range); +} diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html b/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html new file mode 100644 index 0000000000..1168893854 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/timeline-offset-in-keyframe.html @@ -0,0 +1,264 @@ +<!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> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/unattached-subject-inset.html b/testing/web-platform/tests/scroll-animations/view-timelines/unattached-subject-inset.html new file mode 100644 index 0000000000..86262db8f8 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/unattached-subject-inset.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Test construction of a view timeline with a detached subject</title> +</head> +<style type="text/css"> + #container { + overflow: hidden; + height: 200px; + width: 200px; + } + + #block { + background: green; + height: 100px; + width: 100px; + } + + .filler { + height: 200px; + } +</style> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<body> + <div id="container"> + <div class="filler"></div> + </div> +</body> +<script> + promise_test(async t => { + const element = document.createElement('div'); + element.id = 'block'; + const timeline = new ViewTimeline({ + subject: element, + inset: new CSSMathNegate(CSS.px(144)) + }); + assert_equals(timeline.source, null, 'Null source while detached'); + await waitForNextFrame(); + const scroller = document.getElementById('container'); + scroller.appendChild(element); + assert_equals(timeline.source, scroller, 'Source resolved once attached'); + await waitForNextFrame(); + + // Start offset = cover 0% + // = target offset - viewport height + end side inset + // = 200 - 200 + (-144) = -144 + assert_equals(timeline.startOffset.toString(), CSS.px(-144).toString()); + // End offset = cover 100% + // = target offset + target height - start side inset + // = 200 + 100 - (-144) = 444 + assert_equals(timeline.endOffset.toString(), CSS.px(444).toString()); + }, 'Creating a view timeline with a subject that is not attached to the ' + + 'document works as expected'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html new file mode 100644 index 0000000000..25e477e1a9 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-current-time-range-name.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 100px; + display: inline-block; + } +</style> +<body> + <div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + const MAX_SCROLL = 1600; + + promise_test(async t => { + // Points of interest along view timeline: + // 600 px cover start, entry start + // 700 px contain start, entry end + // 800 px contain end, exit start + // 900 px cover end, exit end + const anim = + CreateViewTimelineOpacityAnimation(t, target, + { + timeline: { axis: 'inline' }, + animation: { fill: 'both' } + }); + let timeline = anim.timeline; + + container.scrollLeft = 600; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('cover'), 0, + MAX_SCROLL, 'Scroll aligned with cover start'); + assert_percents_approx_equal(timeline.getCurrentTime('entry'), 0, + MAX_SCROLL, 'Scroll aligned with entry start'); + assert_percents_approx_equal(timeline.getCurrentTime(), 0, + MAX_SCROLL, + 'Scroll aligned with timeline start offset'); + + container.scrollLeft = 650; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('entry'), 50, + MAX_SCROLL, 'Scroll at entry midpoint'); + + container.scrollLeft = 700; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('entry'), 100, + MAX_SCROLL, 'Scroll at entry end'); + assert_percents_approx_equal(timeline.getCurrentTime('contain'), 0, + MAX_SCROLL, 'Scroll at contain start'); + + container.scrollLeft = 750; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('contain'), 50, + MAX_SCROLL, 'Scroll at contain midpoint'); + assert_percents_approx_equal(timeline.getCurrentTime(), 50, + MAX_SCROLL, 'Scroll at timeline midpoint'); + + container.scrollLeft = 800; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('exit'), 0, + MAX_SCROLL, 'Scroll at exit start'); + assert_percents_approx_equal(timeline.getCurrentTime('contain'), 100, + MAX_SCROLL, 'Scroll at contain end'); + + container.scrollLeft = 850; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('exit'), 50, + MAX_SCROLL, 'Scroll at exit midpoint'); + + container.scrollLeft = 900; + await waitForNextFrame(); + + assert_percents_approx_equal(timeline.getCurrentTime('exit'), 100, + MAX_SCROLL, 'Scroll at exit end'); + assert_percents_approx_equal(timeline.getCurrentTime('cover'), 100, + MAX_SCROLL, 'Scroll at cover end'); + assert_percents_approx_equal(timeline.getCurrentTime(), 100, + MAX_SCROLL, 'Scroll at end of timeline'); + + assert_equals(timeline.getCurrentTime('gibberish'), null, + 'No current time for unknown named range'); + + // Add insets to force the start and end offsets to align. This forces + // the timeline to become inactive. + // start_offset = target_offset - viewport_size + end_side_inset + // = 600 + end_side_inset + // end_offset = target_offset + target_size - start_side_inset + // = 900 - start_side_inset + // Equating start_offset and end_offset: + // end_side_inset = 300 - start_side_inset; + timeline = + new ViewTimeline ({ + subject: target, + axis: 'inline', + inset: [ CSS.px(150), CSS.px(150) ] + }); + anim.timeline = timeline; + await waitForNextFrame(); + + assert_equals(timeline.currentTime, null, + 'Current time is null when scroll-range is zero'); + assert_equals(timeline.getCurrentTime(), null, + 'getCurrentTime with an inactive timeline.'); + assert_equals(timeline.getCurrentTime('contain'), null, + 'getCurrentTime on a ranged name with an inactive timeline.'); + + }, 'View timeline current time for named range'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html new file mode 100644 index 0000000000..94660abcf2 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-get-set-range.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<script src="/css/css-typed-om/resources/testhelper.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 100px; + display: inline-block; + font-size: 10px; + } +</style> +<body> + <div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + function assert_timeline_offset(actual, expected, errorMessage) { + assert_equals(actual.rangeName, expected.rangeName, errorMessage); + assert_style_value_equals(actual.offset, expected.offset); + } + + promise_test(async t => { + const timeline = new ViewTimeline({ subject: target, axis: 'inline' }); + const anim = target.animate({ opacity: [0, 1 ] }, { timeline: timeline }); + t.add_cleanup(() => { + anim.cancel(); + }); + await anim.ready; + + container.scrollLeft = 750; + await waitForNextFrame(); + + // normal ==> cover 0% to cover 100% + // cover 0% @ 600px + // cover 100% @ 900px + // expected opacity = (750 - 600) / (900 - 600) = 0.5 + assert_equals(anim.rangeStart, 'normal', 'Initial value for rangeStart'); + assert_equals(anim.rangeEnd, 'normal', 'Initial value for rangeEnd'); + assert_equals(getComputedStyle(target).opacity, '0.5', + 'Opacity with range set to [normal, normal]'); + + // contain 0% @ 700px + // cover 100% @ 900px + // expected opacity = (750 - 700) / (900 - 700) = 0.25 + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = "contain 0%"; + anim.rangeEnd = "cover 100%"; + }); + + assert_timeline_offset( + anim.rangeStart, + { rangeName: 'contain', offset: CSS.percent(0) }, + 'rangeStart set to contain 0%'); + assert_timeline_offset( + anim.rangeEnd, + { rangeName: 'cover', offset: CSS.percent(100) }, + 'rangeEnd set to cover 100%'); + assert_equals(getComputedStyle(target).opacity, '0.25', + 'opacity with range set to [contain 0%, cover 100%]'); + + // entry -20px @ 580px + // exit-crossing 10% @ 810px + // expected opacity = (750 - 580) / (810 - 580) = 0.739130 + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = { rangeName: 'entry', offset: CSS.px(-20), }; + anim.rangeEnd = { rangeName: 'exit-crossing', offset: CSS.percent(10) }; + }); + assert_timeline_offset( + anim.rangeStart, + { rangeName: 'entry', offset: CSS.px(-20) }, + 'rangeStart set to entry -20px'); + assert_timeline_offset( + anim.rangeEnd, + { rangeName: 'exit-crossing', offset: CSS.percent(10) }, + 'rangeEnd set to exit-crossing 10%'); + assert_approx_equals( + parseFloat(getComputedStyle(target).opacity), 0.739130, 1e-6, + 'opacity with range set to [entry -20px, exit-crossing 10%]'); + + // normal [start] @ 600px + // contain 100% @ 800px + // expected opacity = (750 - 600) / (800 - 600) = 0.75 + await runAndWaitForFrameUpdate(() => { + anim.rangeStart = "normal"; + anim.rangeEnd = "contain calc(60% + 40%)"; + }); + assert_equals(anim.rangeStart, 'normal','rangeStart set to normal'); + assert_timeline_offset( + anim.rangeEnd, + { rangeName: 'contain', offset: CSS.percent(100) }, + 'rangeEnd set to contain 100%'); + assert_equals(getComputedStyle(target).opacity, '0.75', + 'opacity with range set to [normal, contain 100%]'); + }, 'Getting and setting the animation range'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html new file mode 100644 index 0000000000..357d8558f9 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-inset.html @@ -0,0 +1,226 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 100px; + display: inline-block; + font-size: 16px; + } + #target.big-font { + font-size: 20px; + } + #container.scroll-padded { + scroll-padding-inline: 10px 20px; + } +</style> +</style> +<body> + <div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + + function verifyTimelineOffsets(anim, start, end) { + const timeline = anim.timeline; + assert_px_equals(timeline.startOffset, start, 'startOffset'); + assert_px_equals(timeline.endOffset, end, 'endOffset'); + }; + + promise_test(async t => { + // These tests are all based on the cover range, which has bounds + // [600, 900] if there are no insets. + // startOffset = target_pos - viewport_size + end_side_inset + // = 600 + end_side_inset + // endOffset = target_pos + target_size - start_side_inset + // = 900 - start_side_inset + await runTimelineInsetTest(t, { + inset: [ CSS.px(0), CSS.px(0) ], + startOffset: 600, + endOffset: 900 + }).then(anim => verifyTimelineOffsets(anim, 600, 900)); + await runTimelineInsetTest(t, { + inset: [ CSS.px(10), CSS.px(20) ], + startOffset: 620, + endOffset: 890 + }).then(anim => verifyTimelineOffsets(anim, 620, 890)); + await runTimelineInsetTest(t, { + inset: [ CSS.px(10) ], + startOffset: 610, + endOffset: 890 + }).then(anim => verifyTimelineOffsets(anim, 610, 890)); + }, 'View timeline with px based inset.'); + + promise_test(async t => { + // These tests are all based on the cover range, which has bounds + // [600, 900]. + // Percentages are relative to the viewport size, which is 200 for this + // test. + await runTimelineInsetTest(t, { + inset: [ CSS.percent(0), CSS.percent(0) ], + startOffset: 600, + endOffset: 900 + }).then(anim => verifyTimelineOffsets(anim, 600, 900)); + await runTimelineInsetTest(t, { + inset: [ CSS.percent(10), CSS.percent(20) ], + startOffset: 640, + endOffset: 880 + }).then(anim => verifyTimelineOffsets(anim, 640, 880)); + await runTimelineInsetTest(t, { + inset: [ CSS.percent(10) ], + startOffset: 620, + endOffset: 880 + }).then(anim => verifyTimelineOffsets(anim, 620, 880)); + }, 'View timeline with percent based inset.'); + + promise_test(async t => { + t.add_cleanup(() => { + container.classList.remove('scroll-padded'); + }); + const anim = await runTimelineInsetTest(t, { + inset: [ "auto", "auto" ], + startOffset: 600, + endOffset: 900 + }); + verifyTimelineOffsets(anim, 600, 900); + container.classList.add('scroll-padded'); + await runTimelineBoundsTest(t, { + anim: anim, + startOffset: 620, + endOffset: 890, + }, 'Adjust for scroll-padding') + .then(anim => verifyTimelineOffsets(anim, 620, 890)); + }, 'view timeline with inset auto.'); + +promise_test(async t => { + t.add_cleanup(() => { + target.classList.remove('big-font'); + }); + const anim = await runTimelineInsetTest(t, { + inset: [ CSS.em(1), CSS.em(2) ], + startOffset: 632, + endOffset: 884 + }); + verifyTimelineOffsets(anim, 632, 884); + target.classList.add('big-font'); + await runTimelineBoundsTest(t, { + anim: anim, + startOffset: 640, + endOffset: 880, + }, 'Adjust for font size increase') + .then(anim => verifyTimelineOffsets(anim, 640, 880)); +}, 'view timeline with font relative inset.'); + +promise_test(async t => { + const vw = window.innerWidth; + const vh = window.innerHeight; + const vmin = Math.min(vw, vh); + await runTimelineInsetTest(t, { + inset: [ CSS.vw(10), CSS.vw(20) ], + startOffset: 600 + 0.2 * vw, + endOffset: 900 - 0.1 * vw + }); + await runTimelineInsetTest(t, { + inset: [ CSS.vmin(10), CSS.vmin(20) ], + startOffset: 600 + 0.2 * vmin, + endOffset: 900 - 0.1 * vmin + }); +}, 'view timeline with viewport relative insets.'); + +promise_test(async t => { + await runTimelineInsetTest(t, { + inset: "10px", + startOffset: 610, + endOffset: 890 + }); + await runTimelineInsetTest(t, { + inset: "10px 20px", + startOffset: 620, + endOffset: 890 + }); + await runTimelineInsetTest(t, { + inset: "10%", + startOffset: 620, + endOffset: 880 + }); + await runTimelineInsetTest(t, { + inset: "10% 20%", + startOffset: 640, + endOffset: 880 + }); + await runTimelineInsetTest(t, { + inset: "auto", + startOffset: 600, + endOffset: 900 + }); + await runTimelineInsetTest(t, { + inset: "1em 2em", + startOffset: 632, + endOffset: 884 + }); + assert_throws_js(TypeError, () => { + new ViewTimeline({ + subject: target, + inset: "go fish" + }); + }); + + assert_throws_js(TypeError, () => { + new ViewTimeline({ + subject: target, + inset: "1 2" + }); + }); + +}, 'view timeline inset as string'); + +promise_test(async t => { + assert_throws_js(TypeError, () => { + new ViewTimeline({ + subject: target, + inset: [ CSS.rad(1) ] + }); + }); + + assert_throws_js(TypeError, () => { + new ViewTimeline({ + subject: target, + inset: [ CSS.px(10), CSS.px(10), CSS.px(10) ] + }); + }); + + +}, 'view timeline with invalid inset'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html new file mode 100644 index 0000000000..01ca021524 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-missing-subject.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> +<title>ViewTimeline with missing subject</title> +<link rel="help" href="https://www.w3.org/TR/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<style type="text/css"> + #target { + background: blue; + height: 100px; + width: 100px; + } + #scroller { + overflow: scroll; + } + #filler { + height: 300vh; + } +</style> +<body onload="runTests()"> + <div id="scroller"> + <div id="target"></div> + <div id="filler"></div> + </div> +</body> +<script type="text/javascript"> + function raf() { + return new Promise(resolve => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }) + }); + } + function runTests() { + promise_test(async t => { + const timeline = new ViewTimeline(); + const anim = + target.animate( + { backgroundColor: ['green', 'red' ] }, + { duration: 100, + timeline: timeline }); + await raf(); + scroller.scrollTop = 50; + await raf(); + assert_equals(timeline.currentTime, null, + 'ViewTimeline with missing subject is inactive'); + }); + } + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html new file mode 100644 index 0000000000..1cc23fe626 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-on-display-none-element.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>View timeline on element with display:none</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timelines"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<script src="/css/css-typed-om/resources/testhelper.js"></script> + +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 100px; + display: none; + } +</style> + +<div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> +</div> + +<script> +promise_test(async t => { + const timeline = new ViewTimeline({ subject: target }); + const anim = target.animate({ opacity: [0, 0.5] }, { timeline: timeline }); + t.add_cleanup(() => { + anim.cancel(); + }); + anim.rangeStart = "1em"; + container.scrollLeft = 750; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity, "1", + "Opacity with inactive timeline"); +}, "element with display: none should have inactive viewtimeline"); +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html new file mode 100644 index 0000000000..f87a57584e --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range-large-subject.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 2100px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + /* target size > viewport size, which changes interpretation of the + contain range */ + width: 400px; + display: inline-block; + } +</style> +<body> + <div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 1200 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: 800, + endOffset: 1000 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 800 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 1000 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: 1000, + endOffset: 1200 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: 800, + endOffset: 1200 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(-50) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(200) }, + startOffset: 700, + endOffset: 1000 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry' }, + rangeEnd: { rangeName: 'exit' }, + startOffset: 600, + endOffset: 1200 + }); + await runTimelineRangeTest(t, { + rangeStart: { offset: CSS.percent(0) }, + rangeEnd: { offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 1200 + }); + + }, 'View timeline with range set via delays.' ); +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html new file mode 100644 index 0000000000..5042c6c2a0 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-range.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-x: scroll; + height: 200px; + width: 200px; + } + #content { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + width: 1800px; + margin: 0; + } + .spacer { + width: 800px; + display: inline-block; + } + #target { + background-color: green; + height: 100px; + width: 100px; + display: inline-block; + font-size: 10px; + } +</style> +<body> + <div id="container"> + <div id="content"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> + </div> +</body> +<script type="text/javascript"> + promise_test(async t => { + // Delays are associated with the animation and not with the timeline. + // Thus adjusting the delays has no effect on the timeline offsets. The + // offsets always correspond to the 'cover' range. + const verifyTimelineOffsets = anim => { + const timeline = anim.timeline; + assert_px_equals(timeline.startOffset, 600, 'startOffset'); + assert_px_equals(timeline.endOffset, 900, 'endOffset'); + }; + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 900 + }).then(anim => { + verifyTimelineOffsets(anim); + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: 700, + endOffset: 800 + }).then(anim => { + verifyTimelineOffsets(anim); + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 700 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 700 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: 800, + endOffset: 900 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: 800, + endOffset: 900 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(-50) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(200) }, + startOffset: 650, + endOffset: 800 + }); + }, 'View timeline with range as <name> <percent> pair.' ); + + promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry' }, + rangeEnd: { rangeName: 'exit' }, + startOffset: 600, + endOffset: 900 + }); + await runTimelineRangeTest(t, { + rangeStart: { offset: CSS.percent(0) }, + rangeEnd: { offset: CSS.percent(100) }, + startOffset: 600, + endOffset: 900 + }); + }, 'View timeline with range and inferred name or offset.' ); + + promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.px(20) } , + rangeEnd: { rangeName: 'cover', offset: CSS.px(100) }, + startOffset: 620, + endOffset: 700 + }); + + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.px(20) } , + rangeEnd: { rangeName: 'contain', offset: CSS.px(100) }, + startOffset: 720, + endOffset: 800 + }); + + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.px(20) } , + rangeEnd: { rangeName: 'entry', offset: CSS.px(100) }, + startOffset: 620, + endOffset: 700 + }); + + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.px(20) } , + rangeEnd: { rangeName: 'exit', offset: CSS.px(80) }, + startOffset: 820, + endOffset: 880 + }); + + }, 'View timeline with range as <name> <px> pair.' ); + + promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { + rangeName: 'contain', + offset: new CSSMathSum(CSS.percent(0), CSS.px(20)) + }, + rangeEnd: { + rangeName: 'contain', + offset: new CSSMathSum(CSS.percent(100), CSS.px(-10)) + }, + startOffset: 720, + endOffset: 790 + }); + + }, 'View timeline with range as <name> <percent+px> pair.' ); + + promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: "contain -50%", + rangeEnd: "entry 200%", + startOffset: 650, + endOffset: 800 + }); + + await runTimelineRangeTest(t, { + rangeStart: "contain 20px", + rangeEnd: "contain 100px", + startOffset: 720, + endOffset: 800 + }); + + await runTimelineRangeTest(t, { + rangeStart: "contain calc(0% + 20px)", + rangeEnd: "contain calc(100% - 10px)", + startOffset: 720, + endOffset: 790 + }); + + await runTimelineRangeTest(t, { + rangeStart: "exit 2em", + rangeEnd: "exit 8em", + startOffset: 820, + endOffset: 880 + }); + + + }, 'View timeline with range as strings.'); + +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html new file mode 100644 index 0000000000..20ac9c5464 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-root-source.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline delay</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #target { + margin: 200vh; + background-color: green; + height: 100px; + width: 100px; + } +</style> +<body> + <div id="target"></div> +</body> +<script type="text/javascript"> + promise_test(async t => { + const timeline = new ViewTimeline({ subject: target }); + const anim = target.animate({ opacity: [0, 1 ] }, + { timeline: timeline, + rangeStart: "entry 0%", + rangeEnd: "entry 100%", + fill: "both" }); + const scroller = document.scrollingElement; + const scrollRange = scroller.scrollHeight - scroller.clientHeight; + + await anim.ready; + + await waitForNextFrame(); + scroller.scrollTop = scrollRange / 2; + await waitForNextFrame(); + + assert_equals(getComputedStyle(target).opacity, "1"); + }, 'Test view-timeline with document scrolling element.'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html new file mode 100644 index 0000000000..5d68d37037 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-snapport.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>ViewTimeline vs. scroll-padding-*</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-timelines"> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#view-progress-visibility-range"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-y: scroll; + height: 200px; + width: 200px; + scroll-padding: 40px; + } + .spacer { + height: 800px; + } + #target { + background-color: green; + height: 200px; + width: 100px; + } +</style> +<body> + <div id="container"> + <div id="leading-space" class="spacer"></div> + <div id="target"></div> + <div id="trailing-space" class="spacer"></div> + </div> +</body> +<script> + promise_test(async t => { + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target); + await anim.ready; + + // 0% + container.scrollTop = 600; + await waitForNextFrame(); + assert_percents_equal(anim.currentTime, 0); + + // 50% + container.scrollTop = 800; + await waitForNextFrame(); + assert_percents_equal(anim.currentTime, 50); + + // 100% + container.scrollTop = 1000; + await waitForNextFrame(); + assert_percents_equal(anim.currentTime, 100); + }, 'Default ViewTimeline is not affected by scroll-padding'); +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html new file mode 100644 index 0000000000..f8aabc8bdd --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-source.tentative.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline source</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<style> +#outer { + height: 400px; + width: 400px; + overflow: clip; +} + +#inner { + height: 300px; + width: 300px; + overflow: clip; +} + +#outer.scroller, +#inner.scroller { + overflow: scroll; +} + +#spacer { + height: 1000px; +} + +#target { + background: green; + height: 40px; + width: 40px; +} +</style> +<body> + <div id="outer" class="scroller"> + <div id="inner" class="scroller"> + <div id="target"></div> + <div id="spacer"></div> + </div> + </div> +</body> +<script> +'use strict'; + +function resetScrollers() { + inner.classList.add('scroller'); + outer.classList.add('scroller'); +} + +function assert_source_id(viewTimeline, expected) { + const source = viewTimeline.source; + assert_true(!!source, 'No source'); + assert_equals(source.id, expected); +} + +promise_test(async t => { + t.add_cleanup(resetScrollers); + const viewTimeline = new ViewTimeline({ subject: target }); + assert_equals(viewTimeline.subject, target); + assert_source_id(viewTimeline, 'inner'); + + inner.classList.remove('scroller'); + assert_source_id(viewTimeline, 'outer'); + + outer.classList.remove('scroller'); + assert_source_id(viewTimeline, 'top'); +}, 'Default source for a View timeline is the nearest scroll ' + + 'ancestor to the subject'); + +promise_test(async t => { + t.add_cleanup(resetScrollers); + const viewTimeline = + new ViewTimeline({ source: outer, subject: target }); + assert_equals(viewTimeline.subject, target); + assert_source_id(viewTimeline, 'inner'); +}, 'View timeline ignores explicitly set source'); + +promise_test(async t => { + t.add_cleanup(resetScrollers); + const viewTimeline = + new ViewTimeline({ subject: target }); + assert_equals(viewTimeline.subject, target); + assert_source_id(viewTimeline, 'inner'); + + target.style = "display: none"; + assert_equals(viewTimeline.source, null); + +}, 'View timeline source is null when display:none'); + +</script> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-block.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-block.html new file mode 100644 index 0000000000..43b717560d --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-block.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + height: 500px; + overflow: auto; +} +.space { + height: 500px; +} +#targetp { + background: yellow; + position: sticky; + top: 0px; + bottom: 0px; + height: 50px; +} +#target { + height: 50px; +} + +</style> +</head> +<body> +<div id="container"> + <div class="space"></div> + <div class="space"> + <div style="height: 200px"></div> + <div id="targetp"> + <div id="target">Subject</div> + </div> + </div> + <div class="space"></div> +</div> +<script type="text/javascript"> + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 1000, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: 50, + endOffset: 950, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 50, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 50, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: 950, + endOffset: 1000, + axis: 'block' + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: 950, + endOffset: 1000, + axis: 'block' + }); +}, 'View timeline with sticky target, block axis.' ); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-inline.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-inline.html new file mode 100644 index 0000000000..4dc8331d9f --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-sticky-inline.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<html id="top"> +<head> +<meta charset="utf-8"> +<title>View timeline with sticky</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + +#container { + width: 500px; + height: 500px; + overflow: auto; + white-space: nowrap; +} +.space { + display: inline-block; + width: 500px; + height: 400px; + white-space: nowrap; +} +#target { + display: inline-block; + background: yellow; + position: sticky; + left: 0px; + right: 0px; + width: 50px; + height: 400px; +} + +</style> +</head> +<body> +<div id="container"><!-- + --><div class="space"></div><!-- + --><div class="space"><!-- + --><div style="display:inline-block; width:200px"></div><!-- + --><div id="target"></div><!-- + --></div><!-- + --><div class="space"></div><!-- +--></div> +<script type="text/javascript"> + +promise_test(async t => { + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'cover', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 1000 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'contain', offset: CSS.percent(0) } , + rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, + startOffset: 50, + endOffset: 950 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 50 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'entry-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry-crossing', offset: CSS.percent(100) }, + startOffset: 0, + endOffset: 50 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit', offset: CSS.percent(100) }, + startOffset: 950, + endOffset: 1000 + }); + await runTimelineRangeTest(t, { + rangeStart: { rangeName: 'exit-crossing', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'exit-crossing', offset: CSS.percent(100) }, + startOffset: 950, + endOffset: 1000 + }); +}, 'View timeline with sticky target, block axis.' ); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html new file mode 100644 index 0000000000..ee7ce90678 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/view-timeline-subject-size-changes.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html id="top"> +<meta charset="utf-8"> +<title>View timeline Subject size changes after creation of Animation</title> +<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#viewtimeline-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/web-animations/testcommon.js"></script> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<script src="/scroll-animations/view-timelines/testcommon.js"></script> +<style> + #container { + border: 10px solid lightgray; + overflow-y: scroll; + height: 400px; + width: 400px; + } + .spacer { + height: 500px; + } + #target { + background-color: green; + height: 100px; + width: 100px; + } +</style> +<body> + <div id="container"> + <div class="spacer"></div> + <div id="target"></div> + <div class="spacer"></div> + </div> +</body> + +<script type="text/javascript"> +promise_test(async t => { + const options = { + timeline: { axis: 'y' }, + animation: { + rangeStart: { rangeName: 'entry', offset: CSS.percent(0) }, + rangeEnd: { rangeName: 'entry', offset: CSS.percent(100) }, + // Set fill to accommodate floating point precision errors at the + // endpoints. + fill: 'both' + } + }; + + container.scrollTop = 0; + await waitForNextFrame(); + + const anim = CreateViewTimelineOpacityAnimation(t, target, options); + const timeline = anim.timeline; + anim.effect.updateTiming(options.timing); + await anim.ready; + + // Advance to the start offset, which triggers entry to the active phase. + container.scrollTop = 100; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity, '0.3', + `Effect at the start of the active phase`); + + // Advance to the midpoint of the animation. + container.scrollTop = 150; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity,'0.5', + `Effect at the midpoint of the active range`); + + // Since the height of the target is cut in half, the animation should be at the end now. + target.style.height = '50px'; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity, '0.7', + `Effect at the end of the active range`); + + // Advance to the midpoint of the animation again. + container.scrollTop = 125; + await waitForNextFrame(); + assert_equals(getComputedStyle(target).opacity,'0.5', + `Effect at the midpoint of the active range again`); + + }, 'View timeline with subject size change after the creation of the animation'); +</script> diff --git a/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html b/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html new file mode 100644 index 0000000000..4eec5d8f13 --- /dev/null +++ b/testing/web-platform/tests/scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html @@ -0,0 +1,106 @@ +<!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> +<script src="/scroll-animations/scroll-timelines/testcommon.js"></script> +<title>Animation range updates play state</title> +</head> +<style type="text/css"> + @keyframes anim { + from { background-color: blue; } + to { background-color: white; } + } + #scroller { + border: 10px solid lightgray; + overflow-y: scroll; + overflow-x: hidden; + width: 300px; + height: 200px; + } + #target { + margin-top: 800px; + margin-bottom: 800px; + margin-left: 10px; + margin-right: 10px; + width: 100px; + height: 100px; + z-index: -1; + background-color: green; + animation: anim auto linear; + animation-timeline: --t1; + view-timeline: --t1; + } +</style> +<body> + <div id="scroller"> + <div id="target"></div> + </div> +</body> +<script type="text/javascript"> + async function runTest() { + promise_test(async t => { + const anim = target.getAnimations()[0]; + await anim.ready; + + let duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(100), + 'Default duration is 100%'); + + // Start and end boundaries coincide. + anim.rangeStart = "entry 100%"; + anim.rangeEnd = "contain 0%"; + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(0), + "Duration is zero when boundaries coincide"); + + // Start > end, clamp at zero duration. + anim.rangeEnd = "entry 0%" + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(0), + "Duration is zero when start > end"); + + anim.rangeStart = "normal"; + anim.rangeEnd = "normal"; + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(100), + "Duration is 100% after range reset"); + + // Consumed 100% of timeline duration with delays + anim.effect.updateTiming({ + delay: CSS.percent(60), + endDelay: CSS.percent(40) + }); + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(0), + "Duration is 0% after delays sum to 100%"); + + // Delays sum to > 100% + anim.effect.updateTiming({ + delay: CSS.percent(60), + endDelay: CSS.percent(60) + }); + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal(duration, CSS.percent(0), + "Duration is 0% after delays sum to > 100%"); + + anim.effect.updateTiming({ + delay: CSS.percent(40), + endDelay: CSS.percent(40) + }); + duration = anim.effect.getComputedTiming().duration; + assert_percents_equal( + duration, CSS.percent(20), + "Duration is 20% if normal range and delays sum to 80%"); + + }, 'Intrinsic iteration duration is non-negative'); + } + + + window.onload = runTest; +</script> |