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