<!DOCTYPE html>
<title>Basic use of stateful animator</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 src="/web-animations/testcommon.js"></script>
<script src="common.js"></script>

<div id="target"></div>

<script id="stateful_animator_basic" type="text/worklet">
  registerAnimator("stateful_animator_basic", class {
    constructor(options, state = { test_local_time: 0 }) {
      this.test_local_time = state.test_local_time;
    }
    animate(currentTime, effect) {
      effect.localTime = this.test_local_time++;
    }
    state() {
      return {
        test_local_time: this.test_local_time
      };
    }
  });
</script>

<script id="stateless_animator_basic" type="text/worklet">
  registerAnimator("stateless_animator_basic", class {
    constructor(options, state = { test_local_time: 0 }) {
      this.test_local_time = state.test_local_time;
    }
    animate(currentTime, effect) {
      effect.localTime = this.test_local_time++;
    }
    // Unless a valid state function is provided, the animator is considered
    // stateless. e.g. animator with incorrect state function name.
    State() {
      return {
        test_local_time: this.test_local_time
      };
    }
  });
</script>

<script id="stateless_animator_preserves_effect_local_time" type="text/worklet">
  registerAnimator("stateless_animator_preserves_effect_local_time", class {
    animate(currentTime, effect) {
      // The local time will be carried over to the new global scope.
      effect.localTime = effect.localTime ? effect.localTime + 1 : 1;
    }
  });
</script>

<script id="stateless_animator_does_not_copy_effect_object" type="text/worklet">
  registerAnimator("stateless_animator_does_not_copy_effect_object", class {
    animate(currentTime, effect) {
      effect.localTime = effect.localTime ? effect.localTime + 1 : 1;
      effect.foo = effect.foo ? effect.foo + 1 : 1;
      // This condition becomes true once we switch global scope and only preserve local time
      // otherwise these values keep increasing in lock step.
      if (effect.localTime > effect.foo) {
        // This works as long as we switch global scope before 10000 frames.
        // which is a safe assumption.
        effect.localTime = 10000;
      }
    }
  });
</script>

<script id="state_function_returns_empty" type="text/worklet">
  registerAnimator("state_function_returns_empty", class {
    constructor(options, state = { test_local_time: 0 }) {
      this.test_local_time = state.test_local_time;
    }
    animate(currentTime, effect) {
      effect.localTime = this.test_local_time++;
    }
    state() {}
  });
</script>

<script id="state_function_returns_not_serializable" type="text/worklet">
  registerAnimator("state_function_returns_not_serializable", class {
    constructor(options) {
      this.test_local_time = 0;
    }
    animate(currentTime, effect) {
      effect.localTime = this.test_local_time++;
    }
    state() {
      return new Symbol('foo');
    }
  });
</script>

<script>
  const EXPECTED_FRAMES_TO_A_SCOPE_SWITCH = 15;
  async function localTimeDoesNotUpdate(animation) {
    // The local time stops increasing after the animator instance being dropped.
    // e.g. 0, 1, 2, .., n, n, n, n, .. where n is the frame that the global
    // scope switches at.
    let last_local_time = animation.effect.getComputedTiming().localTime;
    let frame_count = 0;
    const FRAMES_WITHOUT_CHANGE = 10;
    do {
      await new Promise(window.requestAnimationFrame);
      let current_local_time = animation.effect.getComputedTiming().localTime;
      if (approxEquals(last_local_time, current_local_time))
        ++frame_count;
      else
        frame_count = 0;
      last_local_time = current_local_time;
    } while (frame_count < FRAMES_WITHOUT_CHANGE);
  }

  async function localTimeResetsToZero(animation) {
    // The local time is reset upon global scope switching. e.g.
    // 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, ...
    let reset_count = 0;
    const LOCAL_TIME_RESET_CHECK = 3;
    do {
      await new Promise(window.requestAnimationFrame);
      if (approxEquals(0, animation.effect.getComputedTiming().localTime))
        ++reset_count;
    } while (reset_count < LOCAL_TIME_RESET_CHECK);
  }

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('stateful_animator_basic').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 });
    const animation = new WorkletAnimation('stateful_animator_basic', effect);
    animation.play();

    // effect.localTime should be correctly increased upon global scope
    // switches for stateful animators.
    await waitForAnimationFrameWithCondition(_ => {
      return approxEquals(animation.effect.getComputedTiming().localTime,
          EXPECTED_FRAMES_TO_A_SCOPE_SWITCH);
    });

    animation.cancel();
  }, "Stateful animator can use its state to update the animation. Pass if test does not timeout");

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('stateless_animator_basic').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 });
    const animation = new WorkletAnimation('stateless_animator_basic', effect);
    animation.play();

    // The local time should be reset to 0 upon global scope switching for
    // stateless animators.
    await localTimeResetsToZero(animation);

    animation.cancel();
  }, "Stateless animator gets reecreated with 'undefined' state.");

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('stateless_animator_preserves_effect_local_time').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 });
    const animation = new WorkletAnimation('stateless_animator_preserves_effect_local_time', effect);
    animation.play();

    await waitForAnimationFrameWithCondition(_ => {
        return approxEquals(animation.effect.getComputedTiming().localTime,
            EXPECTED_FRAMES_TO_A_SCOPE_SWITCH);
    });

    animation.cancel();
  }, "Stateless animator should preserve the local time of its effect.");

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('stateless_animator_does_not_copy_effect_object').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 });
    const animation = new WorkletAnimation('stateless_animator_does_not_copy_effect_object', effect);
    animation.play();

    await waitForAnimationFrameWithCondition(_ => {
        return approxEquals(animation.effect.getComputedTiming().localTime, 10000);
    });

    animation.cancel();
  }, "Stateless animator should not copy the effect object.");

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('state_function_returns_empty').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 });
    const animation = new WorkletAnimation('state_function_returns_empty', effect);
    animation.play();

    // The local time should be reset to 0 upon global scope switching for
    // stateless animators.
    await localTimeResetsToZero(animation);

    animation.cancel();
  }, "Stateful animator gets recreated with 'undefined' state if state function returns undefined.");

  promise_test(async t => {
    await runInAnimationWorklet(document.getElementById('state_function_returns_not_serializable').textContent);
    const target = document.getElementById('target');
    const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000, iteration: Infinity });
    const animation = new WorkletAnimation('state_function_returns_not_serializable', effect);
    animation.play();

    // The local time of an animation increases until the registered animator
    // gets removed.
    await localTimeDoesNotUpdate(animation);

    animation.cancel();
  }, "Stateful Animator instance gets dropped (does not get migrated) if state function is not serializable.");
</script>
