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