Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 6 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /animation-worklet/stateful-animator.https.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<title>Basic use of stateful animator</title>
<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>