[scroll-timeline] Implement element-based scroll offset
Implement basic element-based offset calculation taking
edge and threshold into account.
Test: external/wpt/scroll-animations/element-based-offset.html
Bug: 1023375
Change-Id: I38caf32e775e6827a9b7d8763bb7cf9c86fc29c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2100887
Reviewed-by: Majid Valipour <majidvp@chromium.org>
Reviewed-by: Yi Gu <yigu@chromium.org>
Commit-Queue: Majid Valipour <majidvp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#759266}
diff --git a/scroll-animations/element-based-offset.html b/scroll-animations/element-based-offset.html
new file mode 100644
index 0000000..1c7d998
--- /dev/null
+++ b/scroll-animations/element-based-offset.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test element-based scroll offset for scroll timeline.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="testcommon.js"></script>
+
+<style>
+ .scroller {
+ overflow: auto;
+ height: 500px;
+ width: 500px;
+ }
+
+ .contents {
+ height: 2000px;
+ width: 2000px;
+ position: relative;
+ }
+
+ .vertical #start, .vertical #end {
+ background: blue;
+ border-top: 5px solid pink;
+ box-sizing: border-box;
+ width: 100%;
+ height: 50px;
+ }
+
+ .vertical #start {
+ position: absolute;
+ top: 50px;
+ }
+
+ .vertical #end {
+ position: absolute;
+ top: 1050px;
+ }
+
+ .horizontal #start, .horizontal #end {
+ background: blue;
+ border-left:5px solid pink;
+ box-sizing: border-box;
+ height: 100%;
+ width: 50px;
+ }
+
+ .horizontal #start {
+ position: absolute;
+ left: 50px;
+ }
+
+ .horizontal #end {
+ position: absolute;
+ left: 1050px;
+ }
+</style>
+<div id="log"></div>
+<script>
+ 'use strict';
+
+ function createScrollerWithStartAndEnd(test, orientationClass) {
+ var scroller = createDiv(test);
+ scroller.innerHTML =
+ `<div class='contents'>
+ <div id='start'></div>
+ <div id='end'></div>
+ </div>`;
+ scroller.classList.add('scroller');
+ scroller.classList.add(orientationClass);
+
+ return scroller;
+ }
+
+ async function createScrollAnimationTest(description, config) {
+ promise_test(async t => {
+ const scroller = createScrollerWithStartAndEnd(t, config.orientation);
+ t.add_cleanup(() => scroller.remove());
+
+ const start = scroller.querySelector("#start");
+ const end = scroller.querySelector("#end")
+
+ const timeline = createScrollTimeline(t, {
+ scrollSource: scroller,
+ orientation: config.orientation,
+ timeRange: 1000,
+ fill: 'both',
+ startScrollOffset: {target: start, ...config.start},
+ endScrollOffset: {target: end, ...config.end }
+ });
+
+ const animation = createScrollLinkedAnimation(t, timeline);
+ const scrollRange = end.offsetTop - start.offsetTop;
+ const timeRange = animation.timeline.timeRange;
+
+ // Verify initial start and current times in Idle state.
+ assert_equals(animation.currentTime, null,
+ "The current time is null in Idle state.");
+ assert_equals(animation.startTime, null,
+ "The start time is null in Idle state.");
+
+ animation.play();
+ // Verify initial start and current times in Pending state.
+ assert_times_equal(animation.currentTime, 0,
+ "The current time is a hold time in Pending state.");
+ assert_equals(animation.startTime, null,
+ "The start time is null in Pending state.");
+
+ await animation.ready;
+ // Verify initial start and current times in Playing state.
+ assert_times_equal(animation.currentTime, 0,
+ "The current time is zero in Playing state.");
+ assert_times_equal(animation.startTime, 0,
+ "The start time is zero in Playing state.");
+
+ // Now do some scrolling and make sure that the Animation current time is
+ // correct.
+ if (config.orientation == 'vertical') {
+ scroller.scrollTo({top: config.scrollTo});
+ assert_equals(scroller.scrollTop, config.scrollTo);
+ } else {
+ scroller.scrollTo({left: config.scrollTo});
+ assert_equals(scroller.scrollLeft, config.scrollTo);
+ }
+
+ await waitForNextFrame();
+ assert_times_equal(animation.timeline.currentTime, config.expectedCurrentTime,
+ "The timeline current time corresponds to the scroll position of the scroller.");
+ assert_times_equal(animation.currentTime, config.expectedCurrentTime,
+ "The animation current time corresponds to the scroll position of the scroller.");
+ assert_times_equal(
+ animation.effect.getComputedTiming().localTime,
+ config.expectedCurrentTime,
+ 'Effect local time corresponds to the scroll position of the scroller.');
+ }, description);
+ }
+
+ // start is @ 50px
+ // end is @ 1050px
+ // both have 50px heights
+ // scroller has 500px heights
+ // For each test the expected start/end is in the comment to help with the
+ // verification.
+ const tests = {
+ // offsets: [100, 1100]
+ "at start": {
+ scrollTo: 100,
+ expectedCurrentTime: 0,
+ },
+ // offsets: [100, 1100]
+ "after start": {
+ scrollTo: 200,
+ expectedCurrentTime: 100,
+ },
+ // offsets: [100, 1100]
+ "at middle" : {
+ scrollTo: 600,
+ expectedCurrentTime: 500,
+ },
+ // offsets: [100, 1100]
+ "at end" : {
+ scrollTo: 1099,
+ expectedCurrentTime: 999,
+ },
+ // offsets: [100, 1100]
+ "after end" : {
+ scrollTo: 1150,
+ expectedCurrentTime: 1000,
+ },
+ // offsets: [75, 1075]
+ "with threshold 0.5" : {
+ // give threshold to both start and end to keep scrollRange
+ // 1000 which simplifies the calculation.
+ start: {threshold: 0.5},
+ end: {threshold: 0.5},
+ scrollTo: 600 - 25,
+ expectedCurrentTime: 500,
+ },
+ // offsets: [50, 1050]
+ "with threshold 1.0": {
+ start: {threshold: 1.0},
+ end: {threshold: 1.0},
+ scrollTo: 600 - 50,
+ expectedCurrentTime: 500,
+ },
+ // offset: [100, 550]
+ "with end edge" : {
+ end: {edge: "end"},
+ scrollTo: 325,
+ expectedCurrentTime: 500,
+ },
+ // offset: [100, 600]
+ "with end edge and threshold 1.0": {
+ end: {
+ threshold: 1.0,
+ edge: "end"
+ },
+ scrollTo: 350,
+ expectedCurrentTime: 500,
+ },
+ };
+
+ for (let orientation of ['vertical', 'horizontal']) {
+ for (let testName in tests) {
+ const description = `Animation start and current times are correct given
+ element-based offsets for orienation ${orientation} and ${testName}.`;
+ const config = tests[testName];
+ config.orientation = orientation;
+ createScrollAnimationTest(description, config);
+ }
+ }
+</script>
\ No newline at end of file
diff --git a/scroll-animations/testcommon.js b/scroll-animations/testcommon.js
index ab22ff6..2a89d8e 100644
--- a/scroll-animations/testcommon.js
+++ b/scroll-animations/testcommon.js
@@ -5,15 +5,16 @@
return scroller;
}
-function createScrollTimeline(test) {
- return new ScrollTimeline({
+function createScrollTimeline(test, options) {
+ options = options || {
scrollSource: createScroller(test),
timeRange: 1000
- });
+ }
+ return new ScrollTimeline(options);
}
function createScrollTimelineWithOffsets(test, startOffset, endOffset) {
- return new ScrollTimeline({
+ return createScrollTimeline(test, {
scrollSource: createScroller(test),
orientation: "vertical",
startScrollOffset: startOffset,
@@ -23,7 +24,7 @@
}
function createScrollLinkedAnimation(test, timeline) {
- if(timeline === undefined)
+ if (timeline === undefined)
timeline = createScrollTimeline(test);
const DURATION = 1000; // ms
const KEYFRAMES = { opacity: [1, 0] };