stateful-animator.https.html (8488B)
1 <!DOCTYPE html> 2 <title>Basic use of stateful animator</title> 3 <link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> 4 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="common.js"></script> 9 10 <div id="target"></div> 11 12 <script id="stateful_animator_basic" type="text/worklet"> 13 registerAnimator("stateful_animator_basic", class { 14 constructor(options, state = { test_local_time: 0 }) { 15 this.test_local_time = state.test_local_time; 16 } 17 animate(currentTime, effect) { 18 effect.localTime = this.test_local_time++; 19 } 20 state() { 21 return { 22 test_local_time: this.test_local_time 23 }; 24 } 25 }); 26 </script> 27 28 <script id="stateless_animator_basic" type="text/worklet"> 29 registerAnimator("stateless_animator_basic", class { 30 constructor(options, state = { test_local_time: 0 }) { 31 this.test_local_time = state.test_local_time; 32 } 33 animate(currentTime, effect) { 34 effect.localTime = this.test_local_time++; 35 } 36 // Unless a valid state function is provided, the animator is considered 37 // stateless. e.g. animator with incorrect state function name. 38 State() { 39 return { 40 test_local_time: this.test_local_time 41 }; 42 } 43 }); 44 </script> 45 46 <script id="stateless_animator_preserves_effect_local_time" type="text/worklet"> 47 registerAnimator("stateless_animator_preserves_effect_local_time", class { 48 animate(currentTime, effect) { 49 // The local time will be carried over to the new global scope. 50 effect.localTime = effect.localTime ? effect.localTime + 1 : 1; 51 } 52 }); 53 </script> 54 55 <script id="stateless_animator_does_not_copy_effect_object" type="text/worklet"> 56 registerAnimator("stateless_animator_does_not_copy_effect_object", class { 57 animate(currentTime, effect) { 58 effect.localTime = effect.localTime ? effect.localTime + 1 : 1; 59 effect.foo = effect.foo ? effect.foo + 1 : 1; 60 // This condition becomes true once we switch global scope and only preserve local time 61 // otherwise these values keep increasing in lock step. 62 if (effect.localTime > effect.foo) { 63 // This works as long as we switch global scope before 10000 frames. 64 // which is a safe assumption. 65 effect.localTime = 10000; 66 } 67 } 68 }); 69 </script> 70 71 <script id="state_function_returns_empty" type="text/worklet"> 72 registerAnimator("state_function_returns_empty", class { 73 constructor(options, state = { test_local_time: 0 }) { 74 this.test_local_time = state.test_local_time; 75 } 76 animate(currentTime, effect) { 77 effect.localTime = this.test_local_time++; 78 } 79 state() {} 80 }); 81 </script> 82 83 <script id="state_function_returns_not_serializable" type="text/worklet"> 84 registerAnimator("state_function_returns_not_serializable", class { 85 constructor(options) { 86 this.test_local_time = 0; 87 } 88 animate(currentTime, effect) { 89 effect.localTime = this.test_local_time++; 90 } 91 state() { 92 return new Symbol('foo'); 93 } 94 }); 95 </script> 96 97 <script> 98 const EXPECTED_FRAMES_TO_A_SCOPE_SWITCH = 15; 99 async function localTimeDoesNotUpdate(animation) { 100 // The local time stops increasing after the animator instance being dropped. 101 // e.g. 0, 1, 2, .., n, n, n, n, .. where n is the frame that the global 102 // scope switches at. 103 let last_local_time = animation.effect.getComputedTiming().localTime; 104 let frame_count = 0; 105 const FRAMES_WITHOUT_CHANGE = 10; 106 do { 107 await new Promise(window.requestAnimationFrame); 108 let current_local_time = animation.effect.getComputedTiming().localTime; 109 if (approxEquals(last_local_time, current_local_time)) 110 ++frame_count; 111 else 112 frame_count = 0; 113 last_local_time = current_local_time; 114 } while (frame_count < FRAMES_WITHOUT_CHANGE); 115 } 116 117 async function localTimeResetsToZero(animation) { 118 // The local time is reset upon global scope switching. e.g. 119 // 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, .., 0, 1, 2, ... 120 let reset_count = 0; 121 const LOCAL_TIME_RESET_CHECK = 3; 122 do { 123 await new Promise(window.requestAnimationFrame); 124 if (approxEquals(0, animation.effect.getComputedTiming().localTime)) 125 ++reset_count; 126 } while (reset_count < LOCAL_TIME_RESET_CHECK); 127 } 128 129 promise_test(async t => { 130 await runInAnimationWorklet(document.getElementById('stateful_animator_basic').textContent); 131 const target = document.getElementById('target'); 132 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); 133 const animation = new WorkletAnimation('stateful_animator_basic', effect); 134 animation.play(); 135 136 // effect.localTime should be correctly increased upon global scope 137 // switches for stateful animators. 138 await waitForAnimationFrameWithCondition(_ => { 139 return approxEquals(animation.effect.getComputedTiming().localTime, 140 EXPECTED_FRAMES_TO_A_SCOPE_SWITCH); 141 }); 142 143 animation.cancel(); 144 }, "Stateful animator can use its state to update the animation. Pass if test does not timeout"); 145 146 promise_test(async t => { 147 await runInAnimationWorklet(document.getElementById('stateless_animator_basic').textContent); 148 const target = document.getElementById('target'); 149 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); 150 const animation = new WorkletAnimation('stateless_animator_basic', effect); 151 animation.play(); 152 153 // The local time should be reset to 0 upon global scope switching for 154 // stateless animators. 155 await localTimeResetsToZero(animation); 156 157 animation.cancel(); 158 }, "Stateless animator gets reecreated with 'undefined' state."); 159 160 promise_test(async t => { 161 await runInAnimationWorklet(document.getElementById('stateless_animator_preserves_effect_local_time').textContent); 162 const target = document.getElementById('target'); 163 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); 164 const animation = new WorkletAnimation('stateless_animator_preserves_effect_local_time', effect); 165 animation.play(); 166 167 await waitForAnimationFrameWithCondition(_ => { 168 return approxEquals(animation.effect.getComputedTiming().localTime, 169 EXPECTED_FRAMES_TO_A_SCOPE_SWITCH); 170 }); 171 172 animation.cancel(); 173 }, "Stateless animator should preserve the local time of its effect."); 174 175 promise_test(async t => { 176 await runInAnimationWorklet(document.getElementById('stateless_animator_does_not_copy_effect_object').textContent); 177 const target = document.getElementById('target'); 178 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); 179 const animation = new WorkletAnimation('stateless_animator_does_not_copy_effect_object', effect); 180 animation.play(); 181 182 await waitForAnimationFrameWithCondition(_ => { 183 return approxEquals(animation.effect.getComputedTiming().localTime, 10000); 184 }); 185 186 animation.cancel(); 187 }, "Stateless animator should not copy the effect object."); 188 189 promise_test(async t => { 190 await runInAnimationWorklet(document.getElementById('state_function_returns_empty').textContent); 191 const target = document.getElementById('target'); 192 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000 }); 193 const animation = new WorkletAnimation('state_function_returns_empty', effect); 194 animation.play(); 195 196 // The local time should be reset to 0 upon global scope switching for 197 // stateless animators. 198 await localTimeResetsToZero(animation); 199 200 animation.cancel(); 201 }, "Stateful animator gets recreated with 'undefined' state if state function returns undefined."); 202 203 promise_test(async t => { 204 await runInAnimationWorklet(document.getElementById('state_function_returns_not_serializable').textContent); 205 const target = document.getElementById('target'); 206 const effect = new KeyframeEffect(target, [{ opacity: 0 }], { duration: 1000, iteration: Infinity }); 207 const animation = new WorkletAnimation('state_function_returns_not_serializable', effect); 208 animation.play(); 209 210 // The local time of an animation increases until the registered animator 211 // gets removed. 212 await localTimeDoesNotUpdate(animation); 213 214 animation.cancel(); 215 }, "Stateful Animator instance gets dropped (does not get migrated) if state function is not serializable."); 216 </script>