<!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>