summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/animation-worklet/stateful-animator.https.html
blob: be29fa109cba3b2117d5029f4a70f091a440b324 (plain)
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>