|  | <!DOCTYPE html> | 
|  | <meta charset=utf-8> | 
|  | <title>Setting the timeline of scroll animation</title> | 
|  | <link rel="help" | 
|  | href="https://drafts.csswg.org/web-animations-1/#setting-the-timeline"> | 
|  | <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-x: hidden; | 
|  | overflow-y: auto; | 
|  | height: 200px; | 
|  | width: 100px; | 
|  | will-change: transform; | 
|  | } | 
|  | .contents { | 
|  | /* The height is set to align scrolling in pixels with logical time in ms */ | 
|  | height: 1200px; | 
|  | width: 100%; | 
|  | } | 
|  | @keyframes anim { | 
|  | from { opacity: 0; } | 
|  | to { opacity: 1; } | 
|  | } | 
|  | .anim { | 
|  | animation: anim 1s paused linear; | 
|  | } | 
|  | #target { | 
|  | height:  100px; | 
|  | width:  100px; | 
|  | background-color: green; | 
|  | margin-top: -1000px; | 
|  | } | 
|  | </style> | 
|  | <body> | 
|  | <script> | 
|  | 'use strict'; | 
|  |  | 
|  | function createAnimation(t) { | 
|  | const elem = createDiv(t); | 
|  | const animation = elem.animate({ opacity: [1, 0] }, 1000); | 
|  | return animation; | 
|  | } | 
|  |  | 
|  | function createPausedCssAnimation(t) { | 
|  | const elem = createDiv(t); | 
|  | elem.classList.add('anim'); | 
|  | return elem.getAnimations()[0]; | 
|  | } | 
|  |  | 
|  | function updateScrollPosition(timeline, offset) { | 
|  | const scroller = timeline.source; | 
|  | assert_true(!!scroller, 'source is resolved'); | 
|  | scroller.scrollTop = offset; | 
|  | // Wait for new animation frame which allows the timeline to compute new | 
|  | // current time. | 
|  | return waitForNextFrame(); | 
|  | } | 
|  |  | 
|  | function assert_timeline_current_time(animation, timeline_current_time) { | 
|  | if (animation.currentTime instanceof CSSUnitValue){ | 
|  | assert_percents_equal(animation.timeline.currentTime, timeline_current_time, | 
|  | `Timeline's currentTime aligns with the scroll ` + | 
|  | `position even when paused`); | 
|  | } | 
|  | else { | 
|  | assert_times_equal(animation.timeline.currentTime, timeline_current_time, | 
|  | `Timeline's currentTime aligns with the scroll ` + | 
|  | `position even when paused`); | 
|  | } | 
|  | } | 
|  |  | 
|  | function assert_scroll_synced_times(animation, timeline_current_time, | 
|  | animation_current_time) { | 
|  | assert_timeline_current_time(animation, timeline_current_time); | 
|  | if (animation.currentTime instanceof CSSUnitValue){ | 
|  | assert_percents_equal(animation.currentTime, animation_current_time, | 
|  | `Animation's currentTime aligns with the scroll position`); | 
|  | } | 
|  | else { | 
|  | assert_times_equal(animation.currentTime, animation_current_time, | 
|  | `Animation's currentTime aligns with the scroll position`); | 
|  | } | 
|  | } | 
|  |  | 
|  | function assert_paused_times(animation, timeline_current_time, | 
|  | animation_current_time) { | 
|  | assert_timeline_current_time(animation, timeline_current_time); | 
|  | if (animation.currentTime instanceof CSSUnitValue){ | 
|  | assert_percents_equal(animation.currentTime, animation_current_time, | 
|  | `Animation's currentTime is fixed while paused`); | 
|  | } | 
|  | else { | 
|  | assert_times_equal(animation.currentTime, animation_current_time, | 
|  | `Animation's currentTime is fixed while paused`); | 
|  | } | 
|  | } | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_true(animation.pending); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_equals(animation.playState, 'running'); | 
|  | assert_scroll_synced_times(animation, 10, 10); | 
|  | }, 'Setting a scroll timeline on a play-pending animation synchronizes ' + | 
|  | 'currentTime of the animation with the scroll position.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.pause(); | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_true(animation.pending); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_paused_times(animation, 10, 0); | 
|  |  | 
|  | await updateScrollPosition(animation.timeline, 200); | 
|  |  | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_paused_times(animation, 20, 0); | 
|  |  | 
|  | animation.play(); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_scroll_synced_times(animation, 20, 20); | 
|  | }, 'Setting a scroll timeline on a pause-pending animation fixes the ' + | 
|  | 'currentTime of the animation based on the scroll position once resumed'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.reverse(); | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  |  | 
|  | assert_equals(animation.playState, 'running'); | 
|  | assert_scroll_synced_times(animation, 10, 90); | 
|  | },  'Setting a scroll timeline on a reversed play-pending animation ' + | 
|  | 'synchronizes the currentTime of the animation with the scroll ' + | 
|  | 'position.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | await animation.ready; | 
|  |  | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_true(animation.pending); | 
|  | assert_equals(animation.playState, 'running'); | 
|  | await animation.ready; | 
|  | assert_scroll_synced_times(animation, 10, 10); | 
|  | },  'Setting a scroll timeline on a running animation synchronizes the ' + | 
|  | 'currentTime of the animation with the scroll position.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.pause(); | 
|  | await animation.ready; | 
|  |  | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_false(animation.pending); | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_paused_times(animation, 10, 0); | 
|  |  | 
|  | animation.play(); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_scroll_synced_times(animation, 10, 10); | 
|  | }, 'Setting a scroll timeline on a paused animation fixes the currentTime of ' + | 
|  | 'the animation based on the scroll position when resumed'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.reverse(); | 
|  | animation.pause(); | 
|  | await animation.ready; | 
|  |  | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_false(animation.pending); | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_paused_times(animation, 10, 100); | 
|  |  | 
|  | animation.play(); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_scroll_synced_times(animation, 10, 90); | 
|  | }, 'Setting a scroll timeline on a reversed paused animation ' + | 
|  | 'fixes the currentTime of the animation based on the scroll ' + | 
|  | 'position when resumed'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const animation = createAnimation(t); | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | animation.timeline = document.timeline; | 
|  | assert_times_equal(animation.currentTime, 100); | 
|  | }, 'Transitioning from a scroll timeline to a document timeline on a running ' + | 
|  | 'animation preserves currentTime'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const animation = createAnimation(t); | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | animation.pause(); | 
|  | animation.timeline = document.timeline; | 
|  |  | 
|  | await animation.ready; | 
|  | assert_times_equal(animation.currentTime, 100); | 
|  | }, 'Transitioning from a scroll timeline to a document timeline on a ' + | 
|  | 'pause-pending animation preserves currentTime'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const animation = createAnimation(t); | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | animation.timeline = scrollTimeline; | 
|  |  | 
|  | animation.reverse(); | 
|  | await animation.ready; | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | animation.pause(); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_scroll_synced_times(animation, 10, 90); | 
|  |  | 
|  | animation.timeline = document.timeline; | 
|  | assert_false(animation.pending); | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_times_equal(animation.currentTime, 900); | 
|  | }, 'Transition from a scroll timeline to a document timeline on a reversed ' + | 
|  | 'paused animation maintains correct currentTime'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const animation = createAnimation(t); | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const progress = animation.currentTime.value / 100; | 
|  | const duration = animation.effect.getTiming().duration; | 
|  | animation.timeline = null; | 
|  |  | 
|  | const expectedCurrentTime = progress * duration; | 
|  | assert_times_equal(animation.currentTime, expectedCurrentTime); | 
|  | }, 'Transitioning from a scroll timeline to a null timeline on a running ' + | 
|  | 'animation preserves current progress.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const keyframeEfect = new KeyframeEffect(createDiv(t), | 
|  | { opacity: [0, 1] }, | 
|  | 1000); | 
|  | const animation = new Animation(keyframeEfect, null); | 
|  | animation.startTime = 0; | 
|  | assert_equals(animation.playState, 'running'); | 
|  |  | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | animation.timeline = scrollTimeline; | 
|  | assert_equals(animation.playState, 'running'); | 
|  | await animation.ready; | 
|  |  | 
|  | assert_percents_equal(animation.currentTime, 10); | 
|  | }, 'Switching from a null timeline to a scroll timeline on an animation with ' + | 
|  | 'a resolved start time preserved the play state'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const firstScrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(firstScrollTimeline, 100); | 
|  |  | 
|  | const secondScrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(secondScrollTimeline, 200); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.timeline = firstScrollTimeline; | 
|  | await animation.ready; | 
|  | assert_percents_equal(animation.currentTime, 10); | 
|  |  | 
|  | animation.timeline = secondScrollTimeline; | 
|  | await animation.ready; | 
|  |  | 
|  | assert_percents_equal(animation.currentTime, 20); | 
|  | }, 'Switching from one scroll timeline to another updates currentTime'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createPausedCssAnimation(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  | assert_equals(animation.playState, 'paused'); | 
|  | assert_percents_equal(animation.currentTime, 0); | 
|  |  | 
|  | const target = animation.effect.target; | 
|  | target.style.animationPlayState = 'running'; | 
|  | await animation.ready; | 
|  |  | 
|  | assert_percents_equal(animation.currentTime, 10); | 
|  | }, 'Switching from a document timeline to a scroll timeline updates ' + | 
|  | 'currentTime when unpaused via CSS.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  |  | 
|  | const animation = createAnimation(t); | 
|  | animation.pause(); | 
|  | animation.currentTime = 500; // 50% | 
|  | animation.timeline = scrollTimeline; | 
|  | await animation.ready; | 
|  | assert_percents_equal(animation.currentTime, 50); | 
|  |  | 
|  | animation.play(); | 
|  | await animation.ready; | 
|  | assert_percents_equal(animation.currentTime, 10); | 
|  | }, 'Switching from a document timeline to a scroll timeline and updating ' + | 
|  | 'currentTime preserves the progress while paused.'); | 
|  |  | 
|  | promise_test(async t => { | 
|  | const elem = createDiv(t); | 
|  | const animation = elem.animate(null, Infinity); | 
|  | await animation.ready; | 
|  |  | 
|  | animation.timeline = new ScrollTimeline(); | 
|  | let timing = animation.effect.getComputedTiming(); | 
|  | assert_percents_equal(timing.endTime, 100); | 
|  | assert_percents_equal(timing.activeDuration, 100); | 
|  | assert_percents_equal(timing.duration, 100); | 
|  |  | 
|  | animation.effect.updateTiming({ iterations: 2 }); | 
|  | timing = animation.effect.getComputedTiming(); | 
|  | assert_percents_equal(timing.endTime, 100); | 
|  | assert_percents_equal(timing.activeDuration, 100); | 
|  | assert_percents_equal(timing.duration, 50); | 
|  |  | 
|  | // Blink implementation does not permit setting an infinite number of | 
|  | // iterations on a scroll-linked animation. Workaround by temporarily | 
|  | // switching back to a document timeline. | 
|  | animation.timeline = document.timeline; | 
|  | animation.effect.updateTiming({ iterations: Infinity }); | 
|  | animation.timeline = new ScrollTimeline(); | 
|  | timing = animation.effect.getComputedTiming(); | 
|  | // Having an infinite number of iterations with a finite timeline results in | 
|  | // each iteration having zero duration. | 
|  | assert_percents_equal(timing.duration, 0); | 
|  | // If either the iteration duration or iteration count is zero, the active | 
|  | // duration is always zero. | 
|  | assert_percents_equal(timing.activeDuration, 0); | 
|  | assert_percents_equal(timing.endTime, 0); | 
|  |  | 
|  | }, 'Switching from a document timeline to a scroll timeline on an infinite ' + | 
|  | 'duration animation.'); | 
|  |  | 
|  |  | 
|  | promise_test(async t => { | 
|  | const scrollTimeline = createScrollTimeline(t); | 
|  | const view_timeline = createViewTimeline(t); | 
|  | await updateScrollPosition(scrollTimeline, 100); | 
|  | const animation = createAnimation(t); | 
|  | animation.timeline = scrollTimeline; | 
|  | // Range name is ignored while attached to a non-view scroll-timeline. | 
|  | // Offsets are still applied to the scroll-timeline. | 
|  | animation.rangeStart = { rangeName: 'contain', offset: CSS.percent(10) }; | 
|  | animation.rangeEnd = { rangeName: 'contain', offset: CSS.percent(90) }; | 
|  | await animation.ready; | 
|  |  | 
|  | assert_scroll_synced_times(animation, 10, 0); | 
|  | assert_percents_equal(animation.startTime, 10); | 
|  |  | 
|  | animation.timeline = view_timeline; | 
|  | assert_true(animation.pending); | 
|  | await animation.ready; | 
|  |  | 
|  | // Cover range is [0px, 300px] | 
|  | // Contain range is [100px, 200px] | 
|  | // start time = (contain 10% pos - cover start pos) / cover range * 100% | 
|  | const expected_start_time = 110 / 300 * 100; | 
|  | // timeline time = (scroll pos - cover start pos) / cover range * 100% | 
|  | const expected_timeline_time = 100 / 300 * 100; | 
|  | // current time = timeline time - start time. | 
|  | const expected_current_time = expected_timeline_time - expected_start_time; | 
|  |  | 
|  | assert_percents_equal(animation.startTime, expected_start_time); | 
|  | assert_percents_equal(animation.timeline.currentTime, expected_timeline_time); | 
|  | assert_percents_equal(animation.currentTime, expected_current_time); | 
|  | }, 'Changing from a scroll-timeline to a view-timeline updates start time.'); | 
|  |  | 
|  | </script> | 
|  | </body> |