<!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>