setting-timeline.tentative.html (14549B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>Setting the timeline of scroll animation</title> 4 <link rel="help" 5 href="https://drafts.csswg.org/web-animations-1/#setting-the-timeline"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="/web-animations/testcommon.js"></script> 9 <script src="testcommon.js"></script> 10 <style> 11 .scroller { 12 overflow-x: hidden; 13 overflow-y: auto; 14 height: 200px; 15 width: 100px; 16 will-change: transform; 17 } 18 .contents { 19 /* The height is set to align scrolling in pixels with logical time in ms */ 20 height: 1200px; 21 width: 100%; 22 } 23 @keyframes anim { 24 from { opacity: 0; } 25 to { opacity: 1; } 26 } 27 .anim { 28 animation: anim 1s paused linear; 29 } 30 #target { 31 height: 100px; 32 width: 100px; 33 background-color: green; 34 margin-top: -1000px; 35 } 36 </style> 37 <body> 38 <script> 39 'use strict'; 40 41 function createAnimation(t) { 42 const elem = createDiv(t); 43 const animation = elem.animate({ opacity: [1, 0] }, 1000); 44 return animation; 45 } 46 47 function createPausedCssAnimation(t) { 48 const elem = createDiv(t); 49 elem.classList.add('anim'); 50 return elem.getAnimations()[0]; 51 } 52 53 function updateScrollPosition(timeline, offset) { 54 const scroller = timeline.source; 55 assert_true(!!scroller, 'source is resolved'); 56 57 // Wait for new animation frame which allows the timeline to compute new 58 // current time. 59 return runAndWaitForFrameUpdate(() => { 60 scroller.scrollTop = offset; 61 }); 62 } 63 64 function assert_timeline_current_time(animation, timeline_current_time) { 65 if (animation.currentTime instanceof CSSUnitValue){ 66 assert_percents_equal(animation.timeline.currentTime, timeline_current_time, 67 `Timeline's currentTime aligns with the scroll ` + 68 `position even when paused`); 69 } 70 else { 71 assert_times_equal(animation.timeline.currentTime, timeline_current_time, 72 `Timeline's currentTime aligns with the scroll ` + 73 `position even when paused`); 74 } 75 } 76 77 function assert_scroll_synced_times(animation, timeline_current_time, 78 animation_current_time) { 79 assert_timeline_current_time(animation, timeline_current_time); 80 if (animation.currentTime instanceof CSSUnitValue){ 81 assert_percents_equal(animation.currentTime, animation_current_time, 82 `Animation's currentTime aligns with the scroll position`); 83 } 84 else { 85 assert_times_equal(animation.currentTime, animation_current_time, 86 `Animation's currentTime aligns with the scroll position`); 87 } 88 } 89 90 function assert_paused_times(animation, timeline_current_time, 91 animation_current_time) { 92 assert_timeline_current_time(animation, timeline_current_time); 93 if (animation.currentTime instanceof CSSUnitValue){ 94 assert_percents_equal(animation.currentTime, animation_current_time, 95 `Animation's currentTime is fixed while paused`); 96 } 97 else { 98 assert_times_equal(animation.currentTime, animation_current_time, 99 `Animation's currentTime is fixed while paused`); 100 } 101 } 102 103 promise_test(async t => { 104 const scrollTimeline = createScrollTimeline(t); 105 await updateScrollPosition(scrollTimeline, 100); 106 107 const animation = createAnimation(t); 108 animation.timeline = scrollTimeline; 109 assert_true(animation.pending); 110 await animation.ready; 111 112 assert_equals(animation.playState, 'running'); 113 assert_scroll_synced_times(animation, 10, 10); 114 }, 'Setting a scroll timeline on a play-pending animation synchronizes ' + 115 'currentTime of the animation with the scroll position.'); 116 117 promise_test(async t => { 118 const scrollTimeline = createScrollTimeline(t); 119 await updateScrollPosition(scrollTimeline, 100); 120 121 const animation = createAnimation(t); 122 animation.pause(); 123 animation.timeline = scrollTimeline; 124 assert_true(animation.pending); 125 await animation.ready; 126 127 assert_equals(animation.playState, 'paused'); 128 assert_paused_times(animation, 10, 0); 129 130 await updateScrollPosition(animation.timeline, 200); 131 132 assert_equals(animation.playState, 'paused'); 133 assert_paused_times(animation, 20, 0); 134 135 animation.play(); 136 await animation.ready; 137 138 assert_scroll_synced_times(animation, 20, 20); 139 }, 'Setting a scroll timeline on a pause-pending animation fixes the ' + 140 'currentTime of the animation based on the scroll position once resumed'); 141 142 promise_test(async t => { 143 const scrollTimeline = createScrollTimeline(t); 144 await updateScrollPosition(scrollTimeline, 100); 145 146 const animation = createAnimation(t); 147 animation.reverse(); 148 animation.timeline = scrollTimeline; 149 await animation.ready; 150 151 assert_equals(animation.playState, 'running'); 152 assert_scroll_synced_times(animation, 10, 90); 153 }, 'Setting a scroll timeline on a reversed play-pending animation ' + 154 'synchronizes the currentTime of the animation with the scroll ' + 155 'position.'); 156 157 promise_test(async t => { 158 const scrollTimeline = createScrollTimeline(t); 159 await updateScrollPosition(scrollTimeline, 100); 160 161 const animation = createAnimation(t); 162 await animation.ready; 163 164 animation.timeline = scrollTimeline; 165 assert_true(animation.pending); 166 assert_equals(animation.playState, 'running'); 167 await animation.ready; 168 assert_scroll_synced_times(animation, 10, 10); 169 }, 'Setting a scroll timeline on a running animation synchronizes the ' + 170 'currentTime of the animation with the scroll position.'); 171 172 promise_test(async t => { 173 const scrollTimeline = createScrollTimeline(t); 174 await updateScrollPosition(scrollTimeline, 100); 175 176 const animation = createAnimation(t); 177 animation.pause(); 178 await animation.ready; 179 180 animation.timeline = scrollTimeline; 181 assert_false(animation.pending); 182 assert_equals(animation.playState, 'paused'); 183 assert_paused_times(animation, 10, 0); 184 185 animation.play(); 186 await animation.ready; 187 188 assert_scroll_synced_times(animation, 10, 10); 189 }, 'Setting a scroll timeline on a paused animation fixes the currentTime of ' + 190 'the animation based on the scroll position when resumed'); 191 192 promise_test(async t => { 193 const scrollTimeline = createScrollTimeline(t); 194 await updateScrollPosition(scrollTimeline, 100); 195 196 const animation = createAnimation(t); 197 animation.reverse(); 198 animation.pause(); 199 await animation.ready; 200 201 animation.timeline = scrollTimeline; 202 assert_false(animation.pending); 203 assert_equals(animation.playState, 'paused'); 204 assert_paused_times(animation, 10, 100); 205 206 animation.play(); 207 await animation.ready; 208 209 assert_scroll_synced_times(animation, 10, 90); 210 }, 'Setting a scroll timeline on a reversed paused animation ' + 211 'fixes the currentTime of the animation based on the scroll ' + 212 'position when resumed'); 213 214 promise_test(async t => { 215 const animation = createAnimation(t); 216 const scrollTimeline = createScrollTimeline(t); 217 animation.timeline = scrollTimeline; 218 await animation.ready; 219 await updateScrollPosition(scrollTimeline, 100); 220 221 animation.timeline = document.timeline; 222 assert_times_equal(animation.currentTime, 100); 223 }, 'Transitioning from a scroll timeline to a document timeline on a running ' + 224 'animation preserves currentTime'); 225 226 promise_test(async t => { 227 const animation = createAnimation(t); 228 const scrollTimeline = createScrollTimeline(t); 229 animation.timeline = scrollTimeline; 230 await animation.ready; 231 await updateScrollPosition(scrollTimeline, 100); 232 233 animation.pause(); 234 animation.timeline = document.timeline; 235 236 await animation.ready; 237 assert_times_equal(animation.currentTime, 100); 238 }, 'Transitioning from a scroll timeline to a document timeline on a ' + 239 'pause-pending animation preserves currentTime'); 240 241 promise_test(async t => { 242 const animation = createAnimation(t); 243 const scrollTimeline = createScrollTimeline(t); 244 animation.timeline = scrollTimeline; 245 246 animation.reverse(); 247 await animation.ready; 248 await updateScrollPosition(scrollTimeline, 100); 249 250 animation.pause(); 251 await animation.ready; 252 253 assert_scroll_synced_times(animation, 10, 90); 254 255 animation.timeline = document.timeline; 256 assert_false(animation.pending); 257 assert_equals(animation.playState, 'paused'); 258 assert_times_equal(animation.currentTime, 900); 259 }, 'Transition from a scroll timeline to a document timeline on a reversed ' + 260 'paused animation maintains correct currentTime'); 261 262 promise_test(async t => { 263 const animation = createAnimation(t); 264 const scrollTimeline = createScrollTimeline(t); 265 animation.timeline = scrollTimeline; 266 await animation.ready; 267 await updateScrollPosition(scrollTimeline, 100); 268 269 const progress = animation.currentTime.value / 100; 270 const duration = animation.effect.getTiming().duration; 271 animation.timeline = null; 272 273 const expectedCurrentTime = progress * duration; 274 assert_times_equal(animation.currentTime, expectedCurrentTime); 275 }, 'Transitioning from a scroll timeline to a null timeline on a running ' + 276 'animation preserves current progress.'); 277 278 promise_test(async t => { 279 const keyframeEfect = new KeyframeEffect(createDiv(t), 280 { opacity: [0, 1] }, 281 1000); 282 const animation = new Animation(keyframeEfect, null); 283 animation.startTime = 0; 284 assert_equals(animation.playState, 'running'); 285 286 const scrollTimeline = createScrollTimeline(t); 287 await updateScrollPosition(scrollTimeline, 100); 288 289 animation.timeline = scrollTimeline; 290 assert_equals(animation.playState, 'running'); 291 await animation.ready; 292 293 assert_percents_equal(animation.currentTime, 10); 294 }, 'Switching from a null timeline to a scroll timeline on an animation with ' + 295 'a resolved start time preserved the play state'); 296 297 promise_test(async t => { 298 const firstScrollTimeline = createScrollTimeline(t); 299 await updateScrollPosition(firstScrollTimeline, 100); 300 301 const secondScrollTimeline = createScrollTimeline(t); 302 await updateScrollPosition(secondScrollTimeline, 200); 303 304 const animation = createAnimation(t); 305 animation.timeline = firstScrollTimeline; 306 await animation.ready; 307 assert_percents_equal(animation.currentTime, 10); 308 309 animation.timeline = secondScrollTimeline; 310 await animation.ready; 311 312 assert_percents_equal(animation.currentTime, 20); 313 }, 'Switching from one scroll timeline to another updates currentTime'); 314 315 promise_test(async t => { 316 const scrollTimeline = createScrollTimeline(t); 317 await updateScrollPosition(scrollTimeline, 100); 318 319 const animation = createPausedCssAnimation(t); 320 animation.timeline = scrollTimeline; 321 await animation.ready; 322 assert_equals(animation.playState, 'paused'); 323 assert_percents_equal(animation.currentTime, 0); 324 325 const target = animation.effect.target; 326 target.style.animationPlayState = 'running'; 327 await animation.ready; 328 329 assert_percents_equal(animation.currentTime, 10); 330 }, 'Switching from a document timeline to a scroll timeline updates ' + 331 'currentTime when unpaused via CSS.'); 332 333 promise_test(async t => { 334 const scrollTimeline = createScrollTimeline(t); 335 await updateScrollPosition(scrollTimeline, 100); 336 337 const animation = createAnimation(t); 338 animation.pause(); 339 animation.currentTime = 500; // 50% 340 animation.timeline = scrollTimeline; 341 await animation.ready; 342 assert_percents_equal(animation.currentTime, 50); 343 344 animation.play(); 345 await animation.ready; 346 assert_percents_equal(animation.currentTime, 10); 347 }, 'Switching from a document timeline to a scroll timeline and updating ' + 348 'currentTime preserves the progress while paused.'); 349 350 promise_test(async t => { 351 const elem = createDiv(t); 352 const animation = elem.animate(null, Infinity); 353 await animation.ready; 354 355 animation.timeline = new ScrollTimeline(); 356 let timing = animation.effect.getComputedTiming(); 357 assert_percents_equal(timing.endTime, 100); 358 assert_percents_equal(timing.activeDuration, 100); 359 assert_percents_equal(timing.duration, 100); 360 361 animation.effect.updateTiming({ iterations: 2 }); 362 timing = animation.effect.getComputedTiming(); 363 assert_percents_equal(timing.endTime, 100); 364 assert_percents_equal(timing.activeDuration, 100); 365 assert_percents_equal(timing.duration, 50); 366 367 // Blink implementation does not permit setting an infinite number of 368 // iterations on a scroll-linked animation. Workaround by temporarily 369 // switching back to a document timeline. 370 animation.timeline = document.timeline; 371 animation.effect.updateTiming({ iterations: Infinity }); 372 animation.timeline = new ScrollTimeline(); 373 timing = animation.effect.getComputedTiming(); 374 // Having an infinite number of iterations with a finite timeline results in 375 // each iteration having zero duration. 376 assert_percents_equal(timing.duration, 0); 377 // If either the iteration duration or iteration count is zero, the active 378 // duration is always zero. 379 assert_percents_equal(timing.activeDuration, 0); 380 assert_percents_equal(timing.endTime, 0); 381 382 }, 'Switching from a document timeline to a scroll timeline on an infinite ' + 383 'duration animation.'); 384 385 386 promise_test(async t => { 387 const scrollTimeline = createScrollTimeline(t); 388 const view_timeline = createViewTimeline(t); 389 await updateScrollPosition(scrollTimeline, 100); 390 const animation = createAnimation(t); 391 animation.timeline = scrollTimeline; 392 // Range name is ignored while attached to a non-view scroll-timeline. 393 // Offsets are still applied to the scroll-timeline. 394 animation.rangeStart = { rangeName: 'contain', offset: CSS.percent(10) }; 395 animation.rangeEnd = { rangeName: 'contain', offset: CSS.percent(90) }; 396 await animation.ready; 397 398 assert_scroll_synced_times(animation, 10, 0); 399 assert_percents_equal(animation.startTime, 10); 400 401 animation.timeline = view_timeline; 402 assert_true(animation.pending); 403 await animation.ready; 404 405 // Cover range is [0px, 300px] 406 // Contain range is [100px, 200px] 407 // start time = (contain 10% pos - cover start pos) / cover range * 100% 408 const expected_start_time = 110 / 300 * 100; 409 // timeline time = (scroll pos - cover start pos) / cover range * 100% 410 const expected_timeline_time = 100 / 300 * 100; 411 // current time = timeline time - start time. 412 const expected_current_time = expected_timeline_time - expected_start_time; 413 414 assert_percents_equal(animation.startTime, expected_start_time); 415 assert_percents_equal(animation.timeline.currentTime, expected_timeline_time); 416 assert_percents_equal(animation.currentTime, expected_current_time); 417 }, 'Changing from a scroll-timeline to a view-timeline updates start time.'); 418 419 </script> 420 </body>