1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
|
<!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>
|