| <!DOCTYPE html> | |
| <meta charset=utf-8> | |
| <title>The playback rate of a worklet animation</title> | |
| <link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script> | |
| 'use strict'; | |
| // Presence of playback rate adds FP operations to calculating start_time | |
| // and current_time of animations. That's why it's needed to increase FP error | |
| // for comparing times in these tests. | |
| window.assert_times_equal = (actual, expected, description) => { | |
| assert_approx_equals(actual, expected, 0.002, description); | |
| }; | |
| </script> | |
| <script src="/web-animations/testcommon.js"></script> | |
| <script src="common.js"></script> | |
| <style> | |
| .scroller { | |
| overflow: auto; | |
| height: 100px; | |
| width: 100px; | |
| } | |
| .contents { | |
| height: 1000px; | |
| width: 100%; | |
| } | |
| </style> | |
| <body> | |
| <div id="log"></div> | |
| <script> | |
| 'use strict'; | |
| function createWorkletAnimation(test) { | |
| const DURATION = 10000; // ms | |
| const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; | |
| return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), | |
| KEYFRAMES, DURATION), document.timeline); | |
| } | |
| function createScroller(test) { | |
| var scroller = createDiv(test); | |
| scroller.innerHTML = "<div class='contents'></div>"; | |
| scroller.classList.add('scroller'); | |
| return scroller; | |
| } | |
| function createScrollLinkedWorkletAnimation(test) { | |
| const timeline = new ScrollTimeline({ | |
| scrollSource: createScroller(test) | |
| }); | |
| const DURATION = 10000; // ms | |
| const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; | |
| return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), | |
| KEYFRAMES, DURATION), timeline); | |
| } | |
| setup(setupAndRegisterTests, {explicit_done: true}); | |
| function setupAndRegisterTests() { | |
| registerPassthroughAnimator().then(() => { | |
| promise_test(async t => { | |
| const animation = createWorkletAnimation(t); | |
| animation.playbackRate = 0.5; | |
| animation.play(); | |
| assert_equals(animation.currentTime, 0, | |
| 'Zero current time is not affected by playbackRate.'); | |
| }, 'Zero current time is not affected by playbackRate set while the ' + | |
| 'animation is in idle state.'); | |
| promise_test(async t => { | |
| const animation = createWorkletAnimation(t); | |
| animation.play(); | |
| animation.playbackRate = 0.5; | |
| assert_equals(animation.currentTime, 0, | |
| 'Zero current time is not affected by playbackRate.'); | |
| }, 'Zero current time is not affected by playbackRate set while the ' + | |
| 'animation is in play-pending state.'); | |
| promise_test(async t => { | |
| const animation = createWorkletAnimation(t); | |
| const playbackRate = 2; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| // Make sure the current time is not Zero. | |
| await waitForDocumentTimelineAdvance(); | |
| // Set playback rate while the animation is playing. | |
| const prevCurrentTime = animation.currentTime; | |
| animation.playbackRate = playbackRate; | |
| assert_times_equal(animation.currentTime, prevCurrentTime, | |
| 'The current time should stay unaffected by setting playback rate.'); | |
| }, 'Non zero current time is not affected by playbackRate set while the ' + | |
| 'animation is in play state.'); | |
| promise_test(async t => { | |
| const animation = createWorkletAnimation(t); | |
| const playbackRate = 0.2; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| // Set playback rate while the animation is playing. | |
| const prevCurrentTime = animation.currentTime; | |
| const prevTimelineTime = document.timeline.currentTime; | |
| animation.playbackRate = playbackRate; | |
| // Play the animation some more. | |
| await waitForDocumentTimelineAdvance(); | |
| const currentTime = animation.currentTime; | |
| const currentTimelineTime = document.timeline.currentTime; | |
| assert_times_equal( | |
| currentTime - prevCurrentTime, | |
| (currentTimelineTime - prevTimelineTime) * playbackRate, | |
| 'The current time should increase 0.2 times faster than timeline.'); | |
| }, 'The playback rate affects the rate of progress of the current time.'); | |
| promise_test(async t => { | |
| const animation = createWorkletAnimation(t); | |
| const playbackRate = 2; | |
| // Set playback rate while the animation is in 'idle' state. | |
| animation.playbackRate = playbackRate; | |
| const prevTimelineTime = document.timeline.currentTime; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| await waitForDocumentTimelineAdvance(); | |
| const currentTime = animation.currentTime; | |
| const timelineTime = document.timeline.currentTime; | |
| assert_times_equal( | |
| currentTime, | |
| (timelineTime - prevTimelineTime) * playbackRate, | |
| 'The current time should increase two times faster than timeline.'); | |
| }, 'The playback rate set before the animation started playing affects ' + | |
| 'the rate of progress of the current time'); | |
| promise_test(async t => { | |
| const timing = { duration: 100, | |
| easing: 'linear', | |
| fill: 'none', | |
| iterations: 1 | |
| }; | |
| // TODO(crbug.com/937382): Currently composited | |
| // workletAnimation.currentTime and the corresponding | |
| // effect.getComputedTiming().localTime are computed by main and | |
| // compositing threads respectively and, as a result, don't match. | |
| // To workaround this limitation we compare the output of two identical | |
| // animations that only differ in playback rate. The expectation is that | |
| // their output matches after taking their playback rates into | |
| // consideration. This works since these two animations start at the same | |
| // time on the same thread. | |
| // Once the issue is fixed, this test needs to change so expected | |
| // effect.getComputedTiming().localTime is compared against | |
| // workletAnimation.currentTime. | |
| const target = createDiv(t); | |
| const targetRef = createDiv(t); | |
| const keyframeEffect = new KeyframeEffect( | |
| target, { opacity: [1, 0] }, timing); | |
| const keyframeEffectRef = new KeyframeEffect( | |
| targetRef, { opacity: [1, 0] }, timing); | |
| const animation = new WorkletAnimation( | |
| 'passthrough', keyframeEffect, document.timeline); | |
| const animationRef = new WorkletAnimation( | |
| 'passthrough', keyframeEffectRef, document.timeline); | |
| const playbackRate = 2; | |
| animation.playbackRate = playbackRate; | |
| animation.play(); | |
| animationRef.play(); | |
| // wait until local times are synced back to the main thread. | |
| await waitForAnimationFrameWithCondition(_ => { | |
| return getComputedStyle(target).opacity != '1'; | |
| }); | |
| assert_times_equal( | |
| keyframeEffect.getComputedTiming().localTime, | |
| keyframeEffectRef.getComputedTiming().localTime * playbackRate, | |
| 'When playback rate is set on WorkletAnimation, the underlying ' + | |
| 'effect\'s timing should be properly updated.'); | |
| assert_approx_equals( | |
| 1 - Number(getComputedStyle(target).opacity), | |
| (1 - Number(getComputedStyle(targetRef).opacity)) * playbackRate, | |
| 0.001, | |
| 'When playback rate is set on WorkletAnimation, the underlying effect' + | |
| ' should produce correct visual result.'); | |
| }, 'When playback rate is updated, the underlying effect is properly ' + | |
| 'updated with the current time of its WorkletAnimation and produces ' + | |
| 'correct visual result.'); | |
| promise_test(async t => { | |
| const animation = createScrollLinkedWorkletAnimation(t); | |
| const scroller = animation.timeline.scrollSource; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| animation.playbackRate = 0.5; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| assert_percents_equal(animation.currentTime, 10, | |
| 'Initial current time is scaled by playbackRate.'); | |
| }, 'Initial current time is scaled by playbackRate set while ' + | |
| 'scroll-linked animation is in idle state.'); | |
| promise_test(async t => { | |
| const animation = createScrollLinkedWorkletAnimation(t); | |
| const scroller = animation.timeline.scrollSource; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| animation.play(); | |
| animation.playbackRate = 0.5; | |
| assert_percents_equal(animation.currentTime, 20, | |
| 'Initial current time is not affected by playbackRate.'); | |
| }, 'Initial current time is not affected by playbackRate set while '+ | |
| 'scroll-linked animation is in play-pending state.'); | |
| promise_test(async t => { | |
| const animation = createScrollLinkedWorkletAnimation(t); | |
| const scroller = animation.timeline.scrollSource; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| const playbackRate = 2; | |
| animation.play(); | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| // Set playback rate while the animation is playing. | |
| animation.playbackRate = playbackRate; | |
| assert_percents_equal(animation.currentTime, 20, | |
| 'The current time should stay unaffected by setting playback rate.'); | |
| }, 'The current time is not affected by playbackRate set while the ' + | |
| 'scroll-linked animation is in play state.'); | |
| promise_test(async t => { | |
| const animation = createScrollLinkedWorkletAnimation(t); | |
| const scroller = animation.timeline.scrollSource; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| const playbackRate = 2; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| scroller.scrollTop = 0.1 * maxScroll; | |
| // Set playback rate while the animation is playing. | |
| animation.playbackRate = playbackRate; | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| assert_equals( | |
| animation.currentTime.value - 10, 10 * playbackRate, | |
| 'The current time should increase twice faster than scroll timeline.'); | |
| }, 'Scroll-linked animation playback rate affects the rate of progress ' + | |
| 'of the current time.'); | |
| promise_test(async t => { | |
| const animation = createScrollLinkedWorkletAnimation(t); | |
| const scroller = animation.timeline.scrollSource; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| const playbackRate = 2; | |
| // Set playback rate while the animation is in 'idle' state. | |
| animation.playbackRate = playbackRate; | |
| animation.play(); | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| assert_percents_equal(animation.currentTime, 20 * playbackRate, | |
| 'The current time should increase two times faster than timeline.'); | |
| }, 'The playback rate set before scroll-linked animation started playing ' + | |
| 'affects the rate of progress of the current time'); | |
| promise_test(async t => { | |
| const scroller = createScroller(t); | |
| const timeline = new ScrollTimeline({ | |
| scrollSource: scroller | |
| }); | |
| const timing = { duration: 1000, | |
| easing: 'linear', | |
| fill: 'none', | |
| iterations: 1 | |
| }; | |
| const target = createDiv(t); | |
| const keyframeEffect = new KeyframeEffect( | |
| target, { opacity: [1, 0] }, timing); | |
| const animation = new WorkletAnimation( | |
| 'passthrough', keyframeEffect, timeline); | |
| const playbackRate = 2; | |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; | |
| animation.play(); | |
| animation.playbackRate = playbackRate; | |
| await waitForAnimationFrameWithCondition(_=> { | |
| return animation.playState == "running" | |
| }); | |
| scroller.scrollTop = 0.2 * maxScroll; | |
| // wait until local times are synced back to the main thread. | |
| await waitForAnimationFrameWithCondition(_ => { | |
| return getComputedStyle(target).opacity != '1'; | |
| }); | |
| assert_percents_equal( | |
| keyframeEffect.getComputedTiming().localTime, | |
| 20 * playbackRate, | |
| 'When playback rate is set on WorkletAnimation, the underlying ' + | |
| 'effect\'s timing should be properly updated.'); | |
| assert_approx_equals( | |
| Number(getComputedStyle(target).opacity), | |
| 1 - 20 * playbackRate / 1000, 0.001, | |
| 'When playback rate is set on WorkletAnimation, the underlying ' + | |
| 'effect should produce correct visual result.'); | |
| }, 'When playback rate is updated, the underlying effect is properly ' + | |
| 'updated with the current time of its scroll-linked WorkletAnimation ' + | |
| 'and produces correct visual result.'); | |
| done(); | |
| }); | |
| } | |
| </script> | |
| </body> |