<!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>