Scripted effects (written in response to requestAnimationFrame
or async onscroll
events) are
rich but are subject to main thread jankiness. On the other hand, accelerated CSS transitions and
animations can be fast (for a subset of accelerated properties) but are not rich enough to enable
many common use cases and currently have no way to access scroll offset
and other user input. This is why scripted effects are still very popular for implementing common
effects such as hidey-bars, parallax, position:sticky, and etc. We believe (and others
agree) that there is a need for a new primitive for creating fast and rich visual
effects with the ability to respond to user input such as scroll.
We propose an API to animate a set of animatable properties (which include scroll offsets) inside an isolated execution environment, worklet. We believe this API hits a sweet spot, one that is currently missing in the platform, in balancing among performance, richness, and rationality for addressing our key use cases. Finally, it is possible to fine tune this trade-off in future iteration of this API by exposing additional options and without fundamentally reworking this design.
This design (explainer) supersedes our CompositorWorker proposal.
Motivating Use Cases
-
Scroll-linked effects:
- Parallax (demo)
- Animated scroll headers, eg. “hidey-bars” (demo, twitter, polyer paper-scroll-header-panel)
-
Animations with custom timing functions (particularly those that are not calculable a priori)
- Spring timing function (demo)
-
Location tracking and positioning:
- Position: sticky
-
Procedural animation of multiple elements in sync:
- Compositing growing / shrinking box with border (using 9 patch)
-
Animating scroll offsets:
- Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you scroll either (demo)
- Implementing smooth scroll animations (e.g., custom physic based fling curves)
Note: Demos work best in the latest Chrome Canary with the experimental
web platform features enabled (--enable-experimental-web-platform-features
flag) otherwise they fallback to using main thread rAF to emulate the behaviour.
#Syntax and Exmaples
For detailed explanation of syntax and examples please see this Explainer. Below is just two simple examples to showcase the proposed API. Note that this is the initial proposed syntax and it is likely to change as we collaborate with other interested parties on it.
Like other Houdini APIs they all rely on first importing a script into the scope of a worklet:
if (animationWorklet)
animationWorklet.import('my-animator.js');
else
// AnimationWorklet not supported, use legacy animation fallback or polyfill
Example 1. A fade-in animation with spring timing
Register the animator in AnimationWorklet scope:
registerAnimator('spring-fadein', class SpringAnimator {
static inputProperties = ['--spring-k'];
static outputProperties = ['opacity'];
static inputTime = true;
animate(root, children, timeline) {
children.forEach(elem => {
// read a custom css property.
const k = elem.styleMap.get('--spring-k') || 1;
// compute progress using a fancy spring timing function.
const effectiveValue = this.springTiming(timeline.currentTime, k);
// update opacity accordingly.
elem.styleMap.opacity = effectiveValue;
});
}
springTiming(timestamp, k) {
// calculate fancy spring timing curve and return a sample.
return 0.42;
}
});
Assign elements to the animator declaratively in CSS:
.myFadin {
animator:'spring-fadein';
}
<section class='myFadein'></section>
<section class='myFadein' style="--spring-k: 25;"></section></pre>
Example 2. Multiple Parallax animations
Register the animator in AnimationWorklet scope:
registerAnimator('parallax', class ParallaxAnimator {
static inputProperties = ['transform', '--parallax-rate'];
static outputProperties = ['transform'];
static rootInputScroll = true;
animate(root, children) {
// read scroller's vertical scroll offset.
const scrollTop = root.scrollOffsets.top;
children.forEach(background => {
// read parallax rate.
const rate = background.styleMap.get('--parallax-rate');
// update parallax transform.
let t = background.styleMap.transform;
t.m42 = rate * scrollTop;
background.styleMap.transform = t;
});
});
}
});
Assign elements to the animator declaratively in CSS:
<style>
:root {
animator-root: parallax;
}
.bg {
animator: parallax;
position: fixed;
opacity: 0.5;
z-index: -1;
}
</style>
<div class='bg' style='--parallax-rate: 0.2'></div>
<div class='bg' style='--parallax-rate: 0.5'></div>
Define Custom CSS properties in document Scope:
CSS.registerProperty({
name: '--parallax-rate',
inherits: false,
initial: 0.2,
syntax: '<number>'
});
Additional Details
The explainer has additional details for:
- Interaction between animator and document scope
- Syntax details of root and child elements, declared input and outputs
- CSS selector usage
- Advocated performance model
- Relationship with CSS transitions and web animations
You can also find a simple Web IDL for all the new APIs.
Open Questions
You may find a list of key open questions in Github. Feel free to continue discussion there or add new ones as well.