scroll-animation-effect-phases.tentative.html (19030B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>Verify timeline time, animation time, effect time, and effect progress for all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title> 4 <meta name="timeout" content="long"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="/web-animations/testcommon.js"></script> 8 <script src="testcommon.js"></script> 9 <style> 10 .scroller { 11 overflow: hidden; 12 height: 200px; 13 width: 200px; 14 } 15 .contents { 16 /* Make scroll range 1000 to simplify the math and avoid rounding errors */ 17 height: 1200px; 18 width: 100%; 19 } 20 </style> 21 <div id="log"></div> 22 <script> 23 'use strict'; 24 // Note: effects are scaled to fill the timeline. 25 26 // Each entry is [[test input], [test expectations]] 27 // test input = ["description", delay, end_delay, scroll percent] 28 // test expectations = [timeline time, animation current time, 29 // effect local time, effect progress, effect phase, 30 // opacity] 31 32 /* All interesting transitions: 33 at timeline start 34 before effect delay 35 at effect start 36 in active range 37 at effect end 38 after effect end 39 at timeline end 40 */ 41 const test_cases = [ 42 // Case 1: No delays. 43 // Boundary at end of active phase is inclusive. 44 [ 45 ["at start", 0, 0, 0], 46 [0, 0, 0, 0, "active", 0.3] 47 ], 48 [ 49 ["in active range", 0, 0, 0.50], 50 [50, 50, 50, 0.5, "active", 0.5] 51 ], 52 [ 53 ["at effect end time", 0, 0, 1.0], 54 [100, 100, 100, 1.0, "active", 0.7] 55 ], 56 57 // Case 2: Positive start delay and no end delay. 58 // Boundary at end of active phase is inclusive. 59 [ 60 ["at timeline start", 500, 0, 0], 61 [0, 0, 0, null, "before", 1] 62 ], 63 [ 64 ["before start delay", 500, 0, 0.25], 65 [25, 25, 25, null, "before", 1] 66 ], 67 [ 68 ["at start delay", 500, 0, 0.5], 69 [50, 50, 50, 0, "active", 0.3] 70 ], 71 [ 72 ["in active range", 500, 0, 0.75], 73 [75, 75, 75, 0.5, "active", 0.5] 74 ], 75 [ 76 ["at effect end time", 500, 0, 1.0], 77 [100, 100, 100, 1.0, "active", 0.7] 78 ], 79 80 // case 3: No start delay, Positive end delay. 81 // Boundary at end of active phase is exclusive. 82 [ 83 ["at timeline start", 0, 500, 0], 84 [0, 0, 0, 0, "active", 0.3] 85 ], 86 [ 87 ["in active range", 0, 500, 0.25], 88 [25, 25, 25, 0.5, "active", 0.5] 89 ], 90 [ 91 ["at effect end time", 0, 500, 0.5], 92 [50, 50, 50, null, "after", 1.0] 93 ], 94 [ 95 ["after effect end time", 0, 500, 0.75], 96 [75, 75, 75, null, "after", 1.0] 97 ], 98 [ 99 ["at timeline boundary", 0, 500, 1.0], 100 [100, 100, 100, null, "after", 1.0] 101 ], 102 103 // case 4: Positive start and end delays. 104 // Boundary at end of active phase is exclusive. 105 [ 106 ["at timeline start", 250, 250, 0], 107 [0, 0, 0, null, "before", 1] 108 ], 109 [ 110 ["before start delay", 250, 250, 0.1], 111 [10, 10, 10, null, "before", 1] 112 ], 113 [ 114 ["at start delay", 250, 250, 0.25], 115 [25, 25, 25, 0, "active", 0.3] 116 ], 117 [ 118 ["in active range", 250, 250, 0.5], 119 [50, 50, 50, 0.5, "active", 0.5] 120 ], 121 [ 122 ["at effect end time", 250, 250, 0.75], 123 [75, 75, 75, null, "after", 1.0] 124 ], 125 [ 126 ["after effect end time", 250, 250, 0.9], 127 [90, 90, 90, null, "after", 1.0] 128 ], 129 [ 130 ["at timeline boundary", 250, 250, 1.0], 131 [100, 100, 100, null, "after", 1.0] 132 ], 133 134 // Case 5: Negative start and end delays. 135 // Effect boundaries are not reachable. 136 [ 137 ["at timeline start", -125, -125, 0], 138 [0, 0, 0, 0.25, "active", 0.4] 139 ], 140 [ 141 ["in active range", -125, -125, 0.5], 142 [50, 50, 50, 0.5, "active", 0.5] 143 ], 144 [ 145 ["at timeline end", -125, -125, 1.0], 146 [100, 100, 100, 0.75, "active", 0.6] 147 ] 148 ]; 149 150 for (const test_case of test_cases) { 151 const [inputs, expected] = test_case; 152 const [test_name, delay, end_delay, scroll_percentage] = inputs; 153 154 const description = `Current times and effect phase ${test_name} when` + 155 ` delay = ${delay} and endDelay = ${end_delay} |`; 156 157 promise_test( 158 create_scroll_timeline_delay_test( 159 delay, end_delay, scroll_percentage, expected), 160 description); 161 } 162 163 function create_scroll_timeline_delay_test( 164 delay, end_delay, scroll_percentage, expected){ 165 return async t => { 166 const target = createDiv(t); 167 const timeline = createScrollTimeline(t); 168 const effect = new KeyframeEffect( 169 target, 170 { 171 opacity: [0.3, 0.7] 172 }, 173 { 174 duration: 500, 175 delay: delay, 176 endDelay: end_delay 177 } 178 ); 179 const animation = new Animation(effect, timeline); 180 t.add_cleanup(() => { 181 animation.cancel(); 182 }); 183 const scroller = timeline.source; 184 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 185 186 animation.play(); 187 188 await animation.ready; 189 190 scroller.scrollTop = scroll_percentage * maxScroll; 191 192 // Wait for new animation frame which allows the timeline to compute 193 // new current time. 194 await waitForNextFrame(); 195 196 const [expected_timeline_current_time, 197 expected_animation_current_time, 198 expected_effect_local_time, 199 expected_effect_progress, 200 expected_effect_phase, 201 expected_opacity] = expected; 202 203 assert_percents_equal( 204 animation.timeline.currentTime, 205 expected_timeline_current_time, 206 "timeline current time"); 207 assert_percents_equal( 208 animation.currentTime, 209 expected_animation_current_time, 210 "animation current time"); 211 assert_percents_equal( 212 animation.effect.getComputedTiming().localTime, 213 expected_effect_local_time, 214 "animation effect local time"); 215 assert_approx_equals_or_null( 216 animation.effect.getComputedTiming().progress, 217 expected_effect_progress, 218 0.001, 219 "animation effect progress"); 220 assert_phase( 221 animation, expected_effect_phase); 222 assert_approx_equals( 223 parseFloat(getComputedStyle(target).opacity), expected_opacity, 224 0.001, 225 'target opacity'); 226 } 227 } 228 229 function createKeyframeEffectOpacity(test){ 230 return new KeyframeEffect( 231 createDiv(test), 232 { 233 opacity: [0.3, 0.7] 234 }, 235 { 236 duration: 1000 237 } 238 ); 239 } 240 241 function verifyEffectBeforePhase(animation) { 242 // If currentTime is null, we are either idle, or running with an 243 // inactive timeline. Either way, the animation is not in effect and cannot 244 // be in the before phase. 245 assert_true(animation.currentTime != null, 246 'Animation is not in effect'); 247 248 const fillMode = animation.effect.getTiming().fill; 249 animation.effect.updateTiming({ fill: 'none' }); 250 251 // progress == null AND opacity == 1 implies we are in the effect before 252 // or after phase. 253 assert_equals(animation.effect.getComputedTiming().progress, null); 254 assert_equals( 255 window.getComputedStyle(animation.effect.target) 256 .getPropertyValue("opacity"), 257 "1"); 258 259 // If the progress is no longer null after adding fill: backwards, then we 260 // are in the before phase. 261 animation.effect.updateTiming({ fill: 'backwards' }); 262 assert_true(animation.effect.getComputedTiming().progress != null); 263 assert_equals( 264 window.getComputedStyle(animation.effect.target) 265 .getPropertyValue("opacity"), 266 "0.3"); 267 268 // Reset fill mode to avoid side-effects. 269 animation.effect.updateTiming({ fill: fillMode }); 270 } 271 272 function createScrollLinkedOpacityAnimationWithDelays(t) { 273 const animation = new Animation( 274 createKeyframeEffectOpacity(t), 275 createScrollTimeline(t) 276 ); 277 t.add_cleanup(() => { 278 animation.cancel(); 279 }); 280 animation.effect.updateTiming({ 281 duration: 1000, 282 delay: 500, 283 endDelay: 500 284 }); 285 return animation; 286 } 287 288 289 promise_test(async t => { 290 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 291 const scroller = animation.timeline.source; 292 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 293 294 // scroll pos 295 // current time 296 // start time 297 // | 298 // |---- 25% before ----|---- 50% active ----|---- 25% after ----| 299 animation.play(); 300 await animation.ready; 301 assert_percents_equal(animation.startTime, 0); 302 assert_phase(animation, 'before'); 303 304 // start time scroll pos 305 // | current time 306 // | | 307 // |---- 25% before ----|---- 50% active ----|---- 25% after ----| 308 scroller.scrollTop = 0.5 * maxScroll; 309 await waitForNextFrame(); 310 assert_phase(animation, 'active'); 311 312 // start time scroll pos current time 313 // | | | 314 // |---- 25% before ----|---- 50% active ----|---- 25% after ----| 315 animation.playbackRate = 2; 316 assert_phase(animation, 'after'); 317 318 // start time scroll pos current time 319 // | | | 320 // |---- 33.3% before ----|---- 66.7% active ---------------------| 321 animation.effect.updateTiming({ endDelay: 0 }); 322 assert_phase(animation, 'active'); 323 324 // scroll pos start time 325 // current time | 326 // | | 327 // |---- 33.3% before ----|---- 66.7% active ----------------------| 328 animation.playbackRate = -1; 329 assert_percents_equal(animation.startTime, 100); 330 assert_phase(animation, 'active'); 331 332 // start time 333 // scroll pos current time 334 // | | | 335 // |---- 33.3% before ----|---- 66.7% active -----------------------| 336 animation.playbackRate = -2; 337 assert_phase(animation, 'active'); 338 339 // current time start time 340 // | scroll pos 341 // | | 342 // |---- 33.3% before ----|---- 66.7% active -----------------------| 343 scroller.scrollTop = maxScroll; 344 await waitForNextFrame(); 345 assert_phase(animation, 'before'); 346 347 // current time start time 348 // | scroll pos 349 // | | 350 // |--------------------- 100% active -------------------------------| 351 animation.effect.updateTiming({ delay: 0 }); 352 assert_phase(animation, 'active'); 353 354 // Finally, switch to a document timeline. The before-active boundary 355 // becomes exclusive. 356 animation.timeline = document.timeline; 357 animation.currentTime = 0; 358 await waitForNextFrame(); 359 assert_phase(animation, 'before'); 360 361 }, 'Playback rate affects whether active phase boundary is inclusive.'); 362 363 promise_test(async t => { 364 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 365 const scroller = animation.timeline.source; 366 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 367 368 animation.play(); 369 await animation.ready; 370 verifyEffectBeforePhase(animation); 371 372 animation.pause(); 373 await waitForNextFrame(); 374 verifyEffectBeforePhase(animation); 375 376 animation.play(); 377 await waitForNextFrame(); 378 379 verifyEffectBeforePhase(animation); 380 }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.'); 381 382 promise_test(async t => { 383 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 384 const scroller = animation.timeline.source; 385 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 386 387 animation.play(); 388 await animation.ready; 389 verifyEffectBeforePhase(animation); 390 391 animation.pause(); 392 await animation.ready; 393 verifyEffectBeforePhase(animation); 394 395 // Scrolling should not cause the animation effect to change. 396 scroller.scrollTop = 0.5 * maxScroll; 397 await waitForNextFrame(); 398 399 // Check timeline phase 400 assert_percents_equal(animation.timeline.currentTime, 50); 401 assert_percents_equal(animation.currentTime, 0); 402 assert_percents_equal(animation.effect.getComputedTiming().localTime, 0, 403 "effect local time"); 404 405 // Make sure the effect is still in the before phase even though the 406 // timeline is not. 407 verifyEffectBeforePhase(animation); 408 }, 'Pause in before phase, scroll timeline into active phase, animation ' + 409 'should remain in the before phase'); 410 411 promise_test(async t => { 412 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 413 const scroller = animation.timeline.source; 414 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 415 416 animation.play(); 417 await animation.ready; 418 verifyEffectBeforePhase(animation); 419 420 animation.pause(); 421 await waitForNextFrame(); 422 verifyEffectBeforePhase(animation); 423 424 // Setting the current time should force the animation into effect. 425 const expected_time = 50; 426 animation.currentTime = CSS.percent(expected_time); 427 await waitForNextFrame(); 428 assert_percents_equal(animation.timeline.currentTime, 0); 429 assert_percents_equal(animation.currentTime, expected_time, 430 'Current time matches set value'); 431 assert_percents_equal( 432 animation.effect.getComputedTiming().localTime, 433 expected_time, "Effect local time after setting animation.currentTime"); 434 assert_equals(animation.effect.getComputedTiming().progress, 0.5, 435 "Progress after setting animation.currentTime"); 436 assert_equals( 437 window.getComputedStyle(animation.effect.target) 438 .getPropertyValue("opacity"), 439 "0.5", "Opacity after setting animation.currentTime"); 440 441 // Scrolling should not cause the animation effect to change since 442 // paused. 443 scroller.scrollTop = 0.75 * maxScroll; // scroll so that timeline is 75% 444 await waitForNextFrame(); 445 assert_percents_equal(animation.timeline.currentTime, 75); 446 447 // animation and effect timings are unchanged. 448 assert_percents_equal(animation.currentTime, expected_time, 449 "Current time after scrolling while paused"); 450 assert_percents_equal( 451 animation.effect.getComputedTiming().localTime, 452 expected_time, 453 "Effect local time after scrolling while paused"); 454 assert_equals(animation.effect.getComputedTiming().progress, 0.5, 455 "Progress after scrolling while paused"); 456 assert_equals( 457 window.getComputedStyle(animation.effect.target) 458 .getPropertyValue("opacity"), 459 "0.5", "Opacity after scrolling while paused"); 460 }, 'Pause in before phase, set animation current time to be in active ' + 461 'range, animation should become active. Scrolling should have no effect.'); 462 463 promise_test(async t => { 464 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 465 const scroller = animation.timeline.source; 466 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 467 468 animation.play(); 469 await animation.ready; 470 471 // Causes the timeline to be inactive 472 scroller.style.overflow = "visible"; 473 await waitForNextFrame(); 474 await waitForNextFrame(); 475 476 // Verify that he timeline is inactive 477 assert_equals(animation.timeline.currentTime, null, 478 "Timeline is inactive"); 479 assert_equals( 480 animation.currentTime, null, 481 "Current time for running animation with an inactive timeline"); 482 assert_equals(animation.effect.getComputedTiming().localTime, null, 483 "effect local time with inactive timeline"); 484 485 // Setting the current time while timeline is inactive should pause the 486 // animation at the specified time. 487 animation.currentTime = CSS.percent(50); 488 await waitForNextFrame(); 489 await waitForNextFrame(); 490 491 // Verify that animation currentTime is properly set despite the inactive 492 // timeline. 493 assert_equals(animation.timeline.currentTime, null); 494 assert_percents_equal(animation.currentTime, 50); 495 assert_percents_equal(animation.effect.getComputedTiming().localTime, 50, 496 "effect local time after setting animation current time"); 497 498 // Check effect phase 499 // progress == 0.5 AND opacity == 0.5 shows we are in the effect active 500 // phase. 501 assert_equals(animation.effect.getComputedTiming().progress, 0.5, 502 "effect progress"); 503 assert_equals( 504 window.getComputedStyle(animation.effect.target) 505 .getPropertyValue("opacity"), 506 "0.5", 507 "effect opacity after setting animation current time"); 508 }, 'Make scroller inactive, then set current time to an in range time'); 509 510 promise_test(async t => { 511 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 512 const scroller = animation.timeline.source; 513 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 514 scroller.scrollTop = 0.5 * maxScroll; 515 // Update timeline.currentTime. 516 await waitForNextFrame(); 517 518 animation.pause(); 519 await animation.ready; 520 // verify effect is applied. 521 const expected_progress = 0.5; 522 assert_equals( 523 animation.effect.getComputedTiming().progress, 524 expected_progress, 525 "Verify effect progress after pausing."); 526 527 // cause the timeline to become inactive 528 scroller.style.overflow = 'visible'; 529 await waitForAnimationFrames(2); 530 assert_equals(animation.timeline.currentTime, null, 531 'Sanity check the timeline is inactive.'); 532 assert_equals( 533 animation.effect.getComputedTiming().progress, 534 expected_progress, 535 "Verify effect progress after the timeline goes inactive."); 536 }, 'Animation effect is still applied after pausing and making timeline ' + 537 'inactive.'); 538 539 promise_test(async t => { 540 const animation = createScrollLinkedOpacityAnimationWithDelays(t); 541 const scroller = animation.timeline.source; 542 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 543 544 animation.play(); 545 await animation.ready; 546 547 // cause the timeline to become inactive 548 scroller.style.overflow = 'visible'; 549 550 scroller.scrollTop; 551 552 animation.pause(); 553 }, 'Make timeline inactive, force style update then pause the animation. ' + 554 'No crashing indicates test success.'); 555 </script>