| <!DOCTYPE html> |
| <meta charset="utf-8"> |
| <title>ScrollTimeline current time algorithm</title> |
| <link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/web-animations/testcommon.js"></script> |
| <script src="./resources/scrolltimeline-utils.js"></script> |
| |
| <body></body> |
| |
| <script> |
| 'use strict'; |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const blockScrollTimeline = new ScrollTimeline( |
| {scrollSource: scroller, timeRange: scrollerSize, orientation: 'block'}); |
| const inlineScrollTimeline = new ScrollTimeline( |
| {scrollSource: scroller, timeRange: scrollerSize, orientation: 'inline'}); |
| const horizontalScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'horizontal' |
| }); |
| const verticalScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'vertical' |
| }); |
| |
| // Unscrolled, all timelines should read a currentTime of 0. |
| assert_equals( |
| blockScrollTimeline.currentTime, 0, 'Unscrolled block timeline'); |
| assert_equals( |
| inlineScrollTimeline.currentTime, 0, 'Unscrolled inline timeline'); |
| assert_equals( |
| horizontalScrollTimeline.currentTime, 0, |
| 'Unscrolled horizontal timeline'); |
| assert_equals( |
| verticalScrollTimeline.currentTime, 0, 'Unscrolled vertical timeline'); |
| |
| // Do some scrolling and make sure that the ScrollTimelines update. |
| scroller.scrollTop = 50; |
| scroller.scrollLeft = 75; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| |
| // As noted above timeRange is mapped such that currentTime should be the |
| // scroll offset. |
| assert_equals(blockScrollTimeline.currentTime, 50, 'Scrolled block timeline'); |
| assert_equals( |
| inlineScrollTimeline.currentTime, 75, 'Scrolled inline timeline'); |
| assert_equals( |
| horizontalScrollTimeline.currentTime, 75, 'Scrolled horizontal timeline'); |
| assert_equals( |
| verticalScrollTimeline.currentTime, 50, 'Scrolled vertical timeline'); |
| }, 'currentTime calculates the correct time based on scrolled amount'); |
| |
| promise_test(async t => { |
| // It is difficult to calculate the scroll offset which results in an exact |
| // currentTime. Scrolling is calculated in integers which allows for the |
| // possibility of rounding, and scrollbar widths differ between platforms |
| // which means it is not possible to ensure a divisible scroller size. Instead |
| // the scroller content is made large enough that rounding differences result |
| // in negligible deltas in the output value. |
| const contentOverrides = new Map([['width', '1000px'], ['height', '1000px']]); |
| const scroller = setupScrollTimelineTest(new Map(), contentOverrides); |
| const scrollTimeline = new ScrollTimeline( |
| {scrollSource: scroller, timeRange: 100, orientation: 'block'}); |
| |
| // Mapping timeRange to 100 means the output is 'percentage scrolled', so |
| // calculate where the 50% scroll mark would be. |
| const halfwayY = (scroller.scrollHeight - scroller.clientHeight) / 2; |
| scroller.scrollTop = halfwayY; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| |
| assert_approx_equals(scrollTimeline.currentTime, 50, 0.5); |
| }, 'currentTime adjusts correctly for the timeRange'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const lengthScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(20), 'auto'] |
| }); |
| const percentageScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(20), 'auto'] |
| }); |
| const calcScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(20).sub(CSS.px(5)), 'auto'] |
| }); |
| |
| // Unscrolled all timelines should read a current time of 0, as the |
| // current offset (0) will be less than the startScrollOffset. |
| assert_equals( |
| lengthScrollTimeline.currentTime, 0, |
| 'Unscrolled length-based timeline current time'); |
| assert_equals( |
| lengthScrollTimeline.phase, "before", |
| 'Unscrolled length-based timeline phase'); |
| assert_equals( |
| percentageScrollTimeline.currentTime, 0, |
| 'Unscrolled percentage-based timeline current time'); |
| assert_equals( |
| percentageScrollTimeline.phase, "before", |
| 'Unscrolled percentage-based timeline phase'); |
| assert_equals( |
| calcScrollTimeline.currentTime, 0, |
| 'Unscrolled calc-based timeline current time'); |
| assert_equals( |
| calcScrollTimeline.phase, "before", |
| 'Unscrolled calc-based timeline phase'); |
| |
| // Check the length-based ScrollTimeline. |
| scroller.scrollTop = 19; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| assert_equals( |
| lengthScrollTimeline.currentTime, 0, |
| 'Length-based timeline current time before the startScrollOffset' |
| + ' point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "before", |
| 'Length-based timeline phase before the startScrollOffset point'); |
| scroller.scrollTop = 20; |
| await waitForNextFrame(); |
| assert_equals( |
| lengthScrollTimeline.currentTime, 0, |
| 'Length-based timeline current time at the startScrollOffset point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "active", |
| 'Length-based timeline phase at the startScrollOffset point'); |
| scroller.scrollTop = 50; |
| await waitForNextFrame(); |
| assert_times_equal( |
| lengthScrollTimeline.currentTime, |
| calculateCurrentTime(50, 20, scrollerSize, scrollerSize), |
| 'Length-based timeline current time after the startScrollOffset point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "active", |
| 'Length-based timeline phase after the startScrollOffset point'); |
| |
| // Check the percentage-based ScrollTimeline. |
| scroller.scrollTop = 0.19 * scrollerSize; |
| await waitForNextFrame(); |
| assert_equals( |
| percentageScrollTimeline.currentTime, 0, |
| 'Percentage-based timeline current time before the startScrollOffset' |
| + ' point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "before", |
| 'Percentage-based timeline phase before the startScrollOffset point'); |
| scroller.scrollTop = 0.20 * scrollerSize; |
| await waitForNextFrame(); |
| assert_equals( |
| percentageScrollTimeline.currentTime, 0, |
| 'Percentage-based timeline current time at the startScrollOffset point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "active", |
| 'Percentage-based timeline phase at the startScrollOffset point'); |
| scroller.scrollTop = 0.50 * scrollerSize; |
| await waitForNextFrame(); |
| assert_times_equal( |
| percentageScrollTimeline.currentTime, |
| calculateCurrentTime( |
| scroller.scrollTop, 0.2 * scrollerSize, scrollerSize, scrollerSize), |
| 'Percentage-based timeline current time after the startScrollOffset' |
| + ' point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "active", |
| 'Percentage-based timeline phase after the startScrollOffset point'); |
| |
| // Check the calc-based ScrollTimeline. |
| scroller.scrollTop = 0.2 * scrollerSize - 10; |
| await waitForNextFrame(); |
| assert_equals( |
| calcScrollTimeline.currentTime, 0, |
| 'Calc-based timeline current time before the startScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "before", |
| 'Calc-based timeline phase at the startScrollOffset point'); |
| scroller.scrollTop = 0.2 * scrollerSize - 5; |
| await waitForNextFrame(); |
| assert_equals( |
| calcScrollTimeline.currentTime, 0, |
| 'Calc-based timeline current time at the startScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "active", |
| 'Calc-based timeline phase at the startScrollOffset point'); |
| scroller.scrollTop = 0.2 * scrollerSize; |
| await waitForNextFrame(); |
| assert_times_equal( |
| calcScrollTimeline.currentTime, |
| calculateCurrentTime( |
| scroller.scrollTop, 0.2 * scrollerSize - 5, scrollerSize, |
| scrollerSize), |
| 'Calc-based timeline current time after the startScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "active", |
| 'Calc-based timeline phase at the startScrollOffset point'); |
| }, 'currentTime handles startScrollOffset correctly'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| // When the endScrollOffset is equal to the maximum scroll offset (and there |
| // are no fill modes), the endScrollOffset is treated as inclusive. |
| const inclusiveAutoScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| }); |
| const inclusiveLengthScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(scrollerSize)] |
| }); |
| const inclusivePercentageScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(100)] |
| }); |
| const inclusiveCalcScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(80).add(CSS.px(0.2 * scrollerSize))] |
| }); |
| |
| scroller.scrollTop = scrollerSize; |
| let expectedCurrentTime = calculateCurrentTime( |
| scroller.scrollTop, 0, scrollerSize, scrollerSize); |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| |
| assert_times_equal( |
| inclusiveAutoScrollTimeline.currentTime, expectedCurrentTime, |
| 'Inclusive auto timeline at the endScrollOffset point'); |
| assert_times_equal( |
| inclusiveLengthScrollTimeline.currentTime, expectedCurrentTime, |
| 'Inclusive length-based timeline at the endScrollOffset point'); |
| assert_times_equal( |
| inclusivePercentageScrollTimeline.currentTime, expectedCurrentTime, |
| 'Inclusive percentage-based timeline at the endScrollOffset point'); |
| assert_times_equal( |
| inclusiveCalcScrollTimeline.currentTime, expectedCurrentTime, |
| 'Inclusive calc-based timeline at the endScrollOffset point'); |
| }, 'currentTime handles endScrollOffset correctly (inclusive cases)'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const lengthScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(scrollerSize - 20)] |
| }); |
| const percentageScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(80)] |
| }); |
| const calcScrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.percent(80).add(CSS.px(5))] |
| }); |
| |
| // Check the length-based ScrollTimeline. |
| scroller.scrollTop = scrollerSize; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| assert_equals( |
| lengthScrollTimeline.currentTime, lengthScrollTimeline.timeRange, |
| 'Length-based timeline current time after the endScrollOffset point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "after", |
| 'Length-based timeline phase after the endScrollOffset point'); |
| scroller.scrollTop = scrollerSize - 20; |
| await waitForNextFrame(); |
| assert_equals( |
| lengthScrollTimeline.currentTime, lengthScrollTimeline.timeRange, |
| 'Length-based timeline current time at the endScrollOffset point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "after", |
| 'Length-based timeline phase at the endScrollOffset point'); |
| scroller.scrollTop = scrollerSize - 50; |
| await waitForNextFrame(); |
| assert_times_equal( |
| lengthScrollTimeline.currentTime, |
| calculateCurrentTime( |
| scrollerSize - 50, 0, scrollerSize - 20, scrollerSize), |
| 'Length-based timeline current time before the endScrollOffset point'); |
| assert_equals( |
| lengthScrollTimeline.phase, "active", |
| 'Length-based timeline phase before the endScrollOffset point'); |
| |
| // Check the percentage-based ScrollTimeline. |
| scroller.scrollTop = 0.81 * scrollerSize; |
| await waitForNextFrame(); |
| assert_equals( |
| percentageScrollTimeline.currentTime, percentageScrollTimeline.timeRange, |
| 'Percentage-based timeline current time after the endScrollOffset point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "after", |
| 'Percentage-based timeline phase after the endScrollOffset point'); |
| scroller.scrollTop = 0.80 * scrollerSize; |
| await waitForNextFrame(); |
| assert_equals( |
| percentageScrollTimeline.currentTime, percentageScrollTimeline.timeRange, |
| 'Percentage-based timeline current time at the endScrollOffset point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "after", |
| 'Percentage-based timeline phase at the endScrollOffset point'); |
| scroller.scrollTop = 0.50 * scrollerSize; |
| await waitForNextFrame(); |
| assert_times_equal( |
| percentageScrollTimeline.currentTime, |
| calculateCurrentTime( |
| scroller.scrollTop, 0, 0.8 * scrollerSize, scrollerSize), |
| 'Percentage-based timeline current time before the endScrollOffset point'); |
| assert_equals( |
| percentageScrollTimeline.phase, "active", |
| 'Percentage-based timeline phase before the endScrollOffset point'); |
| |
| // Check the calc-based ScrollTimeline. |
| scroller.scrollTop = 0.8 * scrollerSize + 6; |
| await waitForNextFrame(); |
| assert_equals( |
| calcScrollTimeline.currentTime, calcScrollTimeline.timeRange, |
| 'Calc-based timeline current time after the endScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "after", |
| 'Calc-based timeline phase after the endScrollOffset point'); |
| scroller.scrollTop = 0.8 * scrollerSize + 5; |
| await waitForNextFrame(); |
| assert_equals( |
| calcScrollTimeline.currentTime, calcScrollTimeline.timeRange, |
| 'Calc-based timeline current time at the endScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "after", |
| 'Calc-based timeline phase at the endScrollOffset point'); |
| scroller.scrollTop = 0.5 * scrollerSize; |
| await waitForNextFrame(); |
| assert_times_equal( |
| calcScrollTimeline.currentTime, |
| calculateCurrentTime( |
| scroller.scrollTop, 0, 0.8 * scrollerSize + 5, scrollerSize), |
| 'Calc-based timeline current time before the endScrollOffset point'); |
| assert_equals( |
| calcScrollTimeline.phase, "active", |
| 'Calc-based timeline phase before the endScrollOffset point'); |
| }, 'currentTime handles endScrollOffset correctly'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const scrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(20), CSS.px(scrollerSize - 50)] |
| }); |
| |
| scroller.scrollTop = 150; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| assert_times_equal( |
| scrollTimeline.currentTime, |
| calculateCurrentTime(150, 20, scrollerSize - 50, scrollerSize)); |
| }, 'currentTime handles startScrollOffset and endScrollOffset together correctly'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const scrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(20), CSS.px(20)] |
| }); |
| |
| scroller.scrollTop = 150; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| assert_equals(scrollTimeline.currentTime, scrollTimeline.timeRange); |
| }, 'currentTime handles startScrollOffset == endScrollOffset correctly'); |
| |
| promise_test(async t => { |
| const scroller = setupScrollTimelineTest(); |
| // Set the timeRange such that currentTime maps directly to the value |
| // scrolled. The contents and scroller are square, so it suffices to compute |
| // one edge and use it for all the timelines. |
| const scrollerSize = scroller.scrollHeight - scroller.clientHeight; |
| |
| const scrollTimeline = new ScrollTimeline({ |
| scrollSource: scroller, |
| timeRange: scrollerSize, |
| orientation: 'block', |
| scrollOffsets: [CSS.px(50), CSS.px(10)] |
| }); |
| |
| scroller.scrollTop = 40; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| assert_equals(scrollTimeline.currentTime, 0); |
| scroller.scrollTop = 60; |
| await waitForNextFrame(); |
| assert_equals(scrollTimeline.currentTime, scrollTimeline.timeRange); |
| }, 'currentTime handles startScrollOffset > endScrollOffset correctly'); |
| |
| </script> |