playback-rate.https.html (13359B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>The playback rate of a worklet animation</title> 4 <link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script> 8 'use strict'; 9 // Presence of playback rate adds FP operations to calculating start_time 10 // and current_time of animations. That's why it's needed to increase FP error 11 // for comparing times in these tests. 12 window.assert_times_equal = (actual, expected, description) => { 13 assert_approx_equals(actual, expected, 0.002, description); 14 }; 15 </script> 16 <script src="/web-animations/testcommon.js"></script> 17 <script src="common.js"></script> 18 <style> 19 .scroller { 20 overflow: auto; 21 height: 100px; 22 width: 100px; 23 } 24 .contents { 25 height: 1000px; 26 width: 100%; 27 } 28 </style> 29 <body> 30 <div id="log"></div> 31 <script> 32 'use strict'; 33 34 function createWorkletAnimation(test) { 35 const DURATION = 10000; // ms 36 const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; 37 return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), 38 KEYFRAMES, DURATION), document.timeline); 39 } 40 41 function createScroller(test) { 42 var scroller = createDiv(test); 43 scroller.innerHTML = "<div class='contents'></div>"; 44 scroller.classList.add('scroller'); 45 return scroller; 46 } 47 48 function createScrollLinkedWorkletAnimation(test) { 49 const timeline = new ScrollTimeline({ 50 scrollSource: createScroller(test) 51 }); 52 const DURATION = 10000; // ms 53 const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; 54 return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), 55 KEYFRAMES, DURATION), timeline); 56 } 57 58 setup(setupAndRegisterTests, {explicit_done: true}); 59 60 function setupAndRegisterTests() { 61 registerPassthroughAnimator().then(() => { 62 63 promise_test(async t => { 64 const animation = createWorkletAnimation(t); 65 66 animation.playbackRate = 0.5; 67 animation.play(); 68 assert_equals(animation.currentTime, 0, 69 'Zero current time is not affected by playbackRate.'); 70 }, 'Zero current time is not affected by playbackRate set while the ' + 71 'animation is in idle state.'); 72 73 promise_test(async t => { 74 const animation = createWorkletAnimation(t); 75 76 animation.play(); 77 animation.playbackRate = 0.5; 78 assert_equals(animation.currentTime, 0, 79 'Zero current time is not affected by playbackRate.'); 80 }, 'Zero current time is not affected by playbackRate set while the ' + 81 'animation is in play-pending state.'); 82 83 promise_test(async t => { 84 const animation = createWorkletAnimation(t); 85 const playbackRate = 2; 86 87 animation.play(); 88 89 await waitForAnimationFrameWithCondition(_=> { 90 return animation.playState == "running" 91 }); 92 // Make sure the current time is not Zero. 93 await waitForDocumentTimelineAdvance(); 94 95 // Set playback rate while the animation is playing. 96 const prevCurrentTime = animation.currentTime; 97 animation.playbackRate = playbackRate; 98 99 assert_times_equal(animation.currentTime, prevCurrentTime, 100 'The current time should stay unaffected by setting playback rate.'); 101 }, 'Non zero current time is not affected by playbackRate set while the ' + 102 'animation is in play state.'); 103 104 promise_test(async t => { 105 const animation = createWorkletAnimation(t); 106 const playbackRate = 0.2; 107 108 animation.play(); 109 110 await waitForAnimationFrameWithCondition(_=> { 111 return animation.playState == "running" 112 }); 113 114 // Set playback rate while the animation is playing. 115 const prevCurrentTime = animation.currentTime; 116 const prevTimelineTime = document.timeline.currentTime; 117 animation.playbackRate = playbackRate; 118 119 // Play the animation some more. 120 await waitForDocumentTimelineAdvance(); 121 122 const currentTime = animation.currentTime; 123 const currentTimelineTime = document.timeline.currentTime; 124 125 assert_times_equal( 126 currentTime - prevCurrentTime, 127 (currentTimelineTime - prevTimelineTime) * playbackRate, 128 'The current time should increase 0.2 times faster than timeline.'); 129 }, 'The playback rate affects the rate of progress of the current time.'); 130 131 promise_test(async t => { 132 const animation = createWorkletAnimation(t); 133 const playbackRate = 2; 134 135 // Set playback rate while the animation is in 'idle' state. 136 animation.playbackRate = playbackRate; 137 const prevTimelineTime = document.timeline.currentTime; 138 animation.play(); 139 140 await waitForAnimationFrameWithCondition(_=> { 141 return animation.playState == "running" 142 }); 143 await waitForDocumentTimelineAdvance(); 144 145 const currentTime = animation.currentTime; 146 const timelineTime = document.timeline.currentTime; 147 assert_times_equal( 148 currentTime, 149 (timelineTime - prevTimelineTime) * playbackRate, 150 'The current time should increase two times faster than timeline.'); 151 }, 'The playback rate set before the animation started playing affects ' + 152 'the rate of progress of the current time'); 153 154 promise_test(async t => { 155 const timing = { duration: 100, 156 easing: 'linear', 157 fill: 'none', 158 iterations: 1 159 }; 160 // TODO(crbug.com/937382): Currently composited 161 // workletAnimation.currentTime and the corresponding 162 // effect.getComputedTiming().localTime are computed by main and 163 // compositing threads respectively and, as a result, don't match. 164 // To workaround this limitation we compare the output of two identical 165 // animations that only differ in playback rate. The expectation is that 166 // their output matches after taking their playback rates into 167 // consideration. This works since these two animations start at the same 168 // time on the same thread. 169 // Once the issue is fixed, this test needs to change so expected 170 // effect.getComputedTiming().localTime is compared against 171 // workletAnimation.currentTime. 172 const target = createDiv(t); 173 const targetRef = createDiv(t); 174 const keyframeEffect = new KeyframeEffect( 175 target, { opacity: [1, 0] }, timing); 176 const keyframeEffectRef = new KeyframeEffect( 177 targetRef, { opacity: [1, 0] }, timing); 178 const animation = new WorkletAnimation( 179 'passthrough', keyframeEffect, document.timeline); 180 const animationRef = new WorkletAnimation( 181 'passthrough', keyframeEffectRef, document.timeline); 182 const playbackRate = 2; 183 animation.playbackRate = playbackRate; 184 animation.play(); 185 animationRef.play(); 186 187 // wait until local times are synced back to the main thread. 188 await waitForAnimationFrameWithCondition(_ => { 189 return getComputedStyle(target).opacity != '1'; 190 }); 191 192 assert_times_equal( 193 keyframeEffect.getComputedTiming().localTime, 194 keyframeEffectRef.getComputedTiming().localTime * playbackRate, 195 'When playback rate is set on WorkletAnimation, the underlying ' + 196 'effect\'s timing should be properly updated.'); 197 198 assert_approx_equals( 199 1 - Number(getComputedStyle(target).opacity), 200 (1 - Number(getComputedStyle(targetRef).opacity)) * playbackRate, 201 0.001, 202 'When playback rate is set on WorkletAnimation, the underlying effect' + 203 ' should produce correct visual result.'); 204 }, 'When playback rate is updated, the underlying effect is properly ' + 205 'updated with the current time of its WorkletAnimation and produces ' + 206 'correct visual result.'); 207 208 promise_test(async t => { 209 const animation = createScrollLinkedWorkletAnimation(t); 210 const scroller = animation.timeline.scrollSource; 211 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 212 scroller.scrollTop = 0.2 * maxScroll; 213 214 animation.playbackRate = 0.5; 215 animation.play(); 216 await waitForAnimationFrameWithCondition(_=> { 217 return animation.playState == "running" 218 }); 219 assert_percents_equal(animation.currentTime, 10, 220 'Initial current time is scaled by playbackRate.'); 221 }, 'Initial current time is scaled by playbackRate set while ' + 222 'scroll-linked animation is in idle state.'); 223 224 promise_test(async t => { 225 const animation = createScrollLinkedWorkletAnimation(t); 226 const scroller = animation.timeline.scrollSource; 227 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 228 scroller.scrollTop = 0.2 * maxScroll; 229 230 animation.play(); 231 animation.playbackRate = 0.5; 232 233 assert_percents_equal(animation.currentTime, 20, 234 'Initial current time is not affected by playbackRate.'); 235 }, 'Initial current time is not affected by playbackRate set while '+ 236 'scroll-linked animation is in play-pending state.'); 237 238 promise_test(async t => { 239 const animation = createScrollLinkedWorkletAnimation(t); 240 const scroller = animation.timeline.scrollSource; 241 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 242 const playbackRate = 2; 243 244 animation.play(); 245 scroller.scrollTop = 0.2 * maxScroll; 246 await waitForAnimationFrameWithCondition(_=> { 247 return animation.playState == "running" 248 }); 249 // Set playback rate while the animation is playing. 250 animation.playbackRate = playbackRate; 251 assert_percents_equal(animation.currentTime, 20, 252 'The current time should stay unaffected by setting playback rate.'); 253 }, 'The current time is not affected by playbackRate set while the ' + 254 'scroll-linked animation is in play state.'); 255 256 promise_test(async t => { 257 const animation = createScrollLinkedWorkletAnimation(t); 258 const scroller = animation.timeline.scrollSource; 259 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 260 const playbackRate = 2; 261 262 animation.play(); 263 await waitForAnimationFrameWithCondition(_=> { 264 return animation.playState == "running" 265 }); 266 scroller.scrollTop = 0.1 * maxScroll; 267 268 // Set playback rate while the animation is playing. 269 animation.playbackRate = playbackRate; 270 271 scroller.scrollTop = 0.2 * maxScroll; 272 273 assert_equals( 274 animation.currentTime.value - 10, 10 * playbackRate, 275 'The current time should increase twice faster than scroll timeline.'); 276 }, 'Scroll-linked animation playback rate affects the rate of progress ' + 277 'of the current time.'); 278 279 promise_test(async t => { 280 const animation = createScrollLinkedWorkletAnimation(t); 281 const scroller = animation.timeline.scrollSource; 282 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 283 const playbackRate = 2; 284 285 // Set playback rate while the animation is in 'idle' state. 286 animation.playbackRate = playbackRate; 287 animation.play(); 288 await waitForAnimationFrameWithCondition(_=> { 289 return animation.playState == "running" 290 }); 291 scroller.scrollTop = 0.2 * maxScroll; 292 293 assert_percents_equal(animation.currentTime, 20 * playbackRate, 294 'The current time should increase two times faster than timeline.'); 295 }, 'The playback rate set before scroll-linked animation started playing ' + 296 'affects the rate of progress of the current time'); 297 298 promise_test(async t => { 299 const scroller = createScroller(t); 300 const timeline = new ScrollTimeline({ 301 scrollSource: scroller 302 }); 303 const timing = { duration: 1000, 304 easing: 'linear', 305 fill: 'none', 306 iterations: 1 307 }; 308 const target = createDiv(t); 309 const keyframeEffect = new KeyframeEffect( 310 target, { opacity: [1, 0] }, timing); 311 const animation = new WorkletAnimation( 312 'passthrough', keyframeEffect, timeline); 313 const playbackRate = 2; 314 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 315 316 animation.play(); 317 animation.playbackRate = playbackRate; 318 await waitForAnimationFrameWithCondition(_=> { 319 return animation.playState == "running" 320 }); 321 322 scroller.scrollTop = 0.2 * maxScroll; 323 // wait until local times are synced back to the main thread. 324 await waitForAnimationFrameWithCondition(_ => { 325 return getComputedStyle(target).opacity != '1'; 326 }); 327 328 assert_percents_equal( 329 keyframeEffect.getComputedTiming().localTime, 330 20 * playbackRate, 331 'When playback rate is set on WorkletAnimation, the underlying ' + 332 'effect\'s timing should be properly updated.'); 333 assert_approx_equals( 334 Number(getComputedStyle(target).opacity), 335 1 - 20 * playbackRate / 1000, 0.001, 336 'When playback rate is set on WorkletAnimation, the underlying ' + 337 'effect should produce correct visual result.'); 338 }, 'When playback rate is updated, the underlying effect is properly ' + 339 'updated with the current time of its scroll-linked WorkletAnimation ' + 340 'and produces correct visual result.'); 341 done(); 342 }); 343 } 344 </script> 345 </body>