summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/animation-worklet/playback-rate.https.html
blob: 2e2fb9a099183753a8d0054272effc64cbf57ce5 (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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
<!DOCTYPE html>
<meta charset=utf-8>
<title>The playback rate of a worklet animation</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>
'use strict';
// Presence of playback rate adds FP operations to calculating start_time
// and current_time of animations. That's why it's needed to increase FP error
// for comparing times in these tests.
window.assert_times_equal = (actual, expected, description) => {
  assert_approx_equals(actual, expected, 0.002, description);
};
</script>
<script src="/web-animations/testcommon.js"></script>
<script src="common.js"></script>
<style>
  .scroller {
    overflow: auto;
    height: 100px;
    width: 100px;
  }
  .contents {
    height: 1000px;
    width: 100%;
  }
</style>
<body>
<div id="log"></div>
<script>
'use strict';

function createWorkletAnimation(test) {
  const DURATION = 10000; // ms
  const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] };
  return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test),
        KEYFRAMES, DURATION), document.timeline);
}

function createScroller(test) {
  var scroller = createDiv(test);
  scroller.innerHTML = "<div class='contents'></div>";
  scroller.classList.add('scroller');
  return scroller;
}

function createScrollLinkedWorkletAnimation(test) {
  const timeline = new ScrollTimeline({
    scrollSource: createScroller(test)
  });
  const DURATION = 10000; // ms
  const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] };
  return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test),
        KEYFRAMES, DURATION), timeline);
}

setup(setupAndRegisterTests, {explicit_done: true});

function setupAndRegisterTests() {
  registerPassthroughAnimator().then(() => {

    promise_test(async t => {
      const animation = createWorkletAnimation(t);

      animation.playbackRate = 0.5;
      animation.play();
      assert_equals(animation.currentTime, 0,
        'Zero current time is not affected by playbackRate.');
    }, 'Zero current time is not affected by playbackRate set while the ' +
       'animation is in idle state.');

    promise_test(async t => {
      const animation = createWorkletAnimation(t);

      animation.play();
      animation.playbackRate = 0.5;
      assert_equals(animation.currentTime, 0,
        'Zero current time is not affected by playbackRate.');
    }, 'Zero current time is not affected by playbackRate set while the ' +
       'animation is in play-pending state.');

    promise_test(async t => {
      const animation = createWorkletAnimation(t);
      const playbackRate = 2;

      animation.play();

      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      // Make sure the current time is not Zero.
      await waitForDocumentTimelineAdvance();

      // Set playback rate while the animation is playing.
      const prevCurrentTime = animation.currentTime;
      animation.playbackRate = playbackRate;

      assert_times_equal(animation.currentTime, prevCurrentTime,
        'The current time should stay unaffected by setting playback rate.');
    }, 'Non zero current time is not affected by playbackRate set while the ' +
       'animation is in play state.');

    promise_test(async t => {
      const animation = createWorkletAnimation(t);
      const playbackRate = 0.2;

      animation.play();

      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });

      // Set playback rate while the animation is playing.
      const prevCurrentTime = animation.currentTime;
      const prevTimelineTime = document.timeline.currentTime;
      animation.playbackRate = playbackRate;

      // Play the animation some more.
      await waitForDocumentTimelineAdvance();

      const currentTime = animation.currentTime;
      const currentTimelineTime = document.timeline.currentTime;

      assert_times_equal(
        currentTime - prevCurrentTime,
        (currentTimelineTime - prevTimelineTime) * playbackRate,
        'The current time should increase 0.2 times faster than timeline.');
    }, 'The playback rate affects the rate of progress of the current time.');

    promise_test(async t => {
      const animation = createWorkletAnimation(t);
      const playbackRate = 2;

      // Set playback rate while the animation is in 'idle' state.
      animation.playbackRate = playbackRate;
      const prevTimelineTime = document.timeline.currentTime;
      animation.play();

      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      await waitForDocumentTimelineAdvance();

      const currentTime = animation.currentTime;
      const timelineTime = document.timeline.currentTime;
      assert_times_equal(
        currentTime,
        (timelineTime - prevTimelineTime) * playbackRate,
        'The current time should increase two times faster than timeline.');
    }, 'The playback rate set before the animation started playing affects ' +
       'the rate of progress of the current time');

    promise_test(async t => {
      const timing = { duration: 100,
                      easing: 'linear',
                      fill: 'none',
                      iterations: 1
                     };
      // TODO(crbug.com/937382): Currently composited
      // workletAnimation.currentTime and the corresponding
      // effect.getComputedTiming().localTime are computed by main and
      // compositing threads respectively and, as a result, don't match.
      // To workaround this limitation we compare the output of two identical
      // animations that only differ in playback rate. The expectation is that
      // their output matches after taking their playback rates into
      // consideration. This works since these two animations start at the same
      // time on the same thread.
      // Once the issue is fixed, this test needs to change so expected
      // effect.getComputedTiming().localTime is compared against
      // workletAnimation.currentTime.
      const target = createDiv(t);
      const targetRef = createDiv(t);
      const keyframeEffect = new KeyframeEffect(
        target, { opacity: [1, 0] }, timing);
      const keyframeEffectRef = new KeyframeEffect(
        targetRef, { opacity: [1, 0] }, timing);
      const animation = new WorkletAnimation(
        'passthrough', keyframeEffect, document.timeline);
      const animationRef = new WorkletAnimation(
        'passthrough', keyframeEffectRef, document.timeline);
      const playbackRate = 2;
      animation.playbackRate = playbackRate;
      animation.play();
      animationRef.play();

      // wait until local times are synced back to the main thread.
      await waitForAnimationFrameWithCondition(_ => {
        return getComputedStyle(target).opacity != '1';
      });

      assert_times_equal(
        keyframeEffect.getComputedTiming().localTime,
        keyframeEffectRef.getComputedTiming().localTime * playbackRate,
        'When playback rate is set on WorkletAnimation, the underlying ' +
        'effect\'s timing should be properly updated.');

      assert_approx_equals(
        1 - Number(getComputedStyle(target).opacity),
        (1 - Number(getComputedStyle(targetRef).opacity)) * playbackRate,
        0.001,
        'When playback rate is set on WorkletAnimation, the underlying effect' +
        ' should produce correct visual result.');
    }, 'When playback rate is updated, the underlying effect is properly ' +
       'updated with the current time of its WorkletAnimation and produces ' +
       'correct visual result.');

    promise_test(async t => {
      const animation = createScrollLinkedWorkletAnimation(t);
      const scroller = animation.timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      scroller.scrollTop = 0.2 * maxScroll;

      animation.playbackRate = 0.5;
      animation.play();
      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      assert_percents_equal(animation.currentTime, 10,
        'Initial current time is scaled by playbackRate.');
    }, 'Initial current time is scaled by playbackRate set while ' +
       'scroll-linked animation is in idle state.');

    promise_test(async t => {
      const animation = createScrollLinkedWorkletAnimation(t);
      const scroller = animation.timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      scroller.scrollTop = 0.2 * maxScroll;

      animation.play();
      animation.playbackRate = 0.5;

      assert_percents_equal(animation.currentTime, 20,
        'Initial current time is not affected by playbackRate.');
    }, 'Initial current time is not affected by playbackRate set while '+
       'scroll-linked animation is in play-pending state.');

    promise_test(async t => {
      const animation = createScrollLinkedWorkletAnimation(t);
      const scroller = animation.timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      const playbackRate = 2;

      animation.play();
      scroller.scrollTop = 0.2 * maxScroll;
      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      // Set playback rate while the animation is playing.
      animation.playbackRate = playbackRate;
      assert_percents_equal(animation.currentTime, 20,
        'The current time should stay unaffected by setting playback rate.');
    }, 'The current time is not affected by playbackRate set while the ' +
       'scroll-linked animation is in play state.');

    promise_test(async t => {
      const animation = createScrollLinkedWorkletAnimation(t);
      const scroller = animation.timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      const playbackRate = 2;

      animation.play();
      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      scroller.scrollTop = 0.1 * maxScroll;

      // Set playback rate while the animation is playing.
      animation.playbackRate = playbackRate;

      scroller.scrollTop = 0.2 * maxScroll;

      assert_equals(
        animation.currentTime.value - 10, 10 * playbackRate,
        'The current time should increase twice faster than scroll timeline.');
    }, 'Scroll-linked animation playback rate affects the rate of progress ' +
       'of the current time.');

    promise_test(async t => {
      const animation = createScrollLinkedWorkletAnimation(t);
      const scroller = animation.timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      const playbackRate = 2;

      // Set playback rate while the animation is in 'idle' state.
      animation.playbackRate = playbackRate;
      animation.play();
      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });
      scroller.scrollTop = 0.2 * maxScroll;

      assert_percents_equal(animation.currentTime, 20 * playbackRate,
        'The current time should increase two times faster than timeline.');
    }, 'The playback rate set before scroll-linked animation started playing ' +
       'affects the rate of progress of the current time');

    promise_test(async t => {
      const scroller = createScroller(t);
      const timeline = new ScrollTimeline({
        scrollSource: scroller
      });
      const timing = { duration: 1000,
                      easing: 'linear',
                      fill: 'none',
                      iterations: 1
                    };
      const target = createDiv(t);
      const keyframeEffect = new KeyframeEffect(
        target, { opacity: [1, 0] }, timing);
      const animation = new WorkletAnimation(
        'passthrough', keyframeEffect, timeline);
      const playbackRate = 2;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;

      animation.play();
      animation.playbackRate = playbackRate;
      await waitForAnimationFrameWithCondition(_=> {
        return animation.playState == "running"
      });

      scroller.scrollTop = 0.2 * maxScroll;
      // wait until local times are synced back to the main thread.
      await waitForAnimationFrameWithCondition(_ => {
        return getComputedStyle(target).opacity != '1';
      });

      assert_percents_equal(
        keyframeEffect.getComputedTiming().localTime,
        20 * playbackRate,
        'When playback rate is set on WorkletAnimation, the underlying ' +
        'effect\'s timing should be properly updated.');
      assert_approx_equals(
        Number(getComputedStyle(target).opacity),
        1 - 20 * playbackRate / 1000, 0.001,
        'When playback rate is set on WorkletAnimation, the underlying ' +
        'effect should produce correct visual result.');
    }, 'When playback rate is updated, the underlying effect is properly ' +
       'updated with the current time of its scroll-linked WorkletAnimation ' +
       'and produces correct visual result.');
    done();
  });
}
</script>
</body>