tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>