Animation Worklet is a new primitive that provides extensibility in web animations and enables high
performance procedural animations on the web. The feature is developed as part of the
CSS Houdini task force.
The Animation Worklet API provides a method to create scripted animations that control a set of animation effects. These animations are executed inside an isolated execution environment, worklet which makes it possible for user agents to run such animations in their own dedicated thread to provide a degree of performance isolation from main thread. The API is compatible with Web Animations and uses existing constructs as much as possible.
Scripted interactive effects (written in response to requestAnimationFrame, pointer events 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 key user input (pointer events, gestures, scroll). This is why scripted effects are
still very popular for implementing common effects such as hidey-bars, parallax, pull-to-refresh,
drag-and-drop, swipe to dismiss and etc. Animation Worklet provides is key building block for
enabling creation of smooth rich interactive visual effects on the web while also exposing an
extensibility hook in web animations.
See the Animation Worklet design principles and goals for a more extended overview of the motivations behind Animation Worklet and how the design will be evolved to support a growing set of use cases. Also see the status document for high level implementation status and timeline. Here you may find an earlier high level discussion on general approaches to address this problem.
-
Scroll driven effects:
- Hidey-bar: animation depends on both time and scroll input.
- Parallax: Simplest scroll-drive effect.
- Custom paginated slider.
- Pull-to-refresh: animation depends on both touch and time inputs.
- Custom scrollbars.
- High-fidelity location tracking and positioning
- More examples of scroll-driven effects.
-
Gesture driven effects:
- Image manipulator that scales, rotates etc.
- Swipe to dismiss.
- Drag-N-Drop.
- Tiled panning e.g., Google maps.
-
Stateful script driven effects:
- Spring timing emulations.
- Spring-Sticky effect.
- Touch-driven physical environments.
- Expando: Procedural animations with multiple elements.
-
Animated scroll offsets:
- Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you scroll either (demo)
- Custom smooth scroll animations (e.g., physic based fling curves)
-
Animation Extensibility:
- Custom timing functions (particularly those that are not calculable a priori)
- Custom animation sequencing which involves complex coordination across multiple effects.
Not all of these usecases are immediately enabled by the current proposed API. However Animation Worklet provides a powerfull primitive (off main-thread scriped animation) which when combined with other upcoming features (e.g., Event in Worklets, ScrollTimeline, GroupEffect) can address all these usecases and allows many of currently main-thread rAF-based animations to move off thread with significant improvement to their smoothness. See Animation Worklet design principles and goals for a more extended discussion of this.
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.
A worklet global scope that is created by Animation Worklet. Note that Animation Worklet creates multiple such scopes and uses them to execute user defined effects.
WorkletAnimation is a subclass of Animation that can be used to create an custom animation effect
that runs inside a standalone animation worklet scope. A worklet animation has a corresponding
animator instance in a animation worklet scope which is responsible to drive its keyframe effects.
Here are the key differences compared to a regular web animation:
- AnimationId should match a specific animator class registered in the animation worklet scope.
WorkletAnimationmay have multiple timelines (includingScrollTimelines).WorkletAnimationmay have a custom properties bag that can be cloned and provided to animator constructor when it is being instantiated.
Note that worklet animations expose same API surface as other web animations and thus they may be created, played, paused, inspected, and generally controlled from main document scope. Here is how various methods roughly translate:
cancel(): cancels the animation and the corresponding animator instance is removed.play(): starts the animation and the corresponding animator instance gets constructed and may get itsanimatefunction called periodically as a result of changes in its timelines.- pause(): pauses the animation and the corresponding animator instance no longer receives
animatecalls. - finish(), reverse() or mutating playbackRate: these affect the currentTime which is seens by
the animator instance. (We are considering possiblity of having a
onPlaybackRateChangedcallback)
ScrollTimeline is a concept introduced in
scroll-linked animation proposal. It defines an animation timeline whose time value depends on
scroll position of a scroll container. ScrollTimeline can be used an an input timeline for
worklet animations and it is the intended mechanisms to give read access to scroll position.
GroupEffect is a
concept introduced in Web Animation Level 2 specification. It provides a way to group multiple
effects in a tree structure. GroupEffect can be used as the output for worklet animations. It
makes it possible for worklet animation to drive effects spanning multiple elements.
TODO: At the moment, GroupEffect only supports just two different scheduling models (i.e.,
parallel, sequence). These models governs how the group effect time is translated to its children
effect times by modifying the child effect start time. Animation Worklet allows a much more
flexible scheduling model by making it possible to to set children effect's local time directly. In
other words we allow arbitrary start time for child effects. This is something that needs to be
added to level 2 spec.
Unlike typical animations, worklet animations can be attached to multiple timelines. This is necessary to implement key usecases where the effect needs to smoothly animate across different timelines (e.g., scroll and wall clock).
NOTE: We have decided to drop this piece in favor of alternative ideas. Most recent promising idea revolves around allowing worklet and workers to receive input events directly. (here are some earlier alternative design: 1, 2, 3
Sometimes animation effects require maintaining internal state (e.g., when animation needs to depend
on velocity). Such animators have to explicitly declare their statefulness but by inheritting from
StatefulAnimator superclass.
The animators are not guaranteed to run in the same global scope (or underlying thread) for their lifetime duration. For example user agents are free to initially run the animator on main thread but later decide to migrate it off main thread to get certain performance optimizations or to tear down scopes to save resources.
Animation Worklet helps stateful animators to maintain their state across such migration events. This is done through a state() function which is called and animator exposes its state. Here is an example:
// in document scope
new WorkletAnimation('animation-with-local-state', keyframes);registerAnimator('animation-with-local-state', class FoorAnimator extends StatefulAnimator {
constructor(options, state = {velocity: 0, acceleration: 0}) {
// state is either undefined (first time) or the state after an animator is migrated across
// global scope.
this.velocity = state.velocity;
this.acceleration = state.acceleration;
}
animate(time, effect) {
if (this.lastTime) {
this.velocity = time - this.prevTime;
this.acceleration = this.velocity - this.prevVelocity;
}
this.prevTime = time;
this.prevVelocity = velocity;
effect.localTime = curve(velocity, acceleration, currentTime);
}
state() {
// Invoked before any migration attempts. The returned object must be structure clonable
// and will be passed to constructor to help animator restore its state after migration to the
// new scope.
return {
this.velocity,
this.acceleration
};
}
curve(velocity, accerlation, t) {
return /* compute some physical movement curve */;
}
});TODO: Add gifs that visualize these effects
An example of header effect where a header is moved with scroll and as soon as finger is lifted it animates fully to close or open position depending on its current position.
<div id='scrollingContainer'>
<div id='header'>Some header</div>
<div>content</div>
</div>
<script>
await CSS.animationWorklet.addModule('hidey-bar-animator.js');
const scrollTimeline = new ScrollTimeline({
scrollSource: $scrollingContainer,
orientation: 'block',
timeRange: 100
});
const documentTimeline = document.timeline;
const animation = new WorkletAnimation('hidey-bar',
new KeyframeEffect($header,
[{transform: 'translateX(100px)'}, {transform: 'translateX(0px)'}],
{duration: 100, iterations: 1, fill: 'both' })
scrollTimeline,
{scrollTimeline, documentTimeline},
);
animation.play();
</script>hidey-bar-animator.js:
registerAnimator('hidey-bar', class {
constructor(options) {
this.scrollTimeline_ = options.scrollTimeline;
this.documentTimeline_ = options.documentTimeline;
}
animate(currentTime, effect) {
const scroll = this.scrollTimeline_.currentTime; // [0, 100]
const time = this.documentTimeline_.currentTime;
// **TODO**: use a hypothetical 'phase' property on timeline as a way to detect when user is no
// longer actively scrolling. This is a reasonable thing to have on scroll timeline but we can
// fallback to using a timeout based approach as well.
const activelyScrolling = this.scrollTimeline_.phase == 'active';
let localTime;
if (activelyScrolling) {
this.startTime_ = undefined;
localTime = scroll;
} else {
this.startTime_ = this.startTime_ || time;
// Decide on close/open direction depending on how far we have scrolled the header
// This can even do more sophisticated animation curve by computing the scroll velocity and
// using it.
this.direction_ = scroll >= 50 ? +1 : -1;
localTime = this.direction_ * (time - this.startTime_);
}
// Drive the output effects by setting its local time.
effect.localTime = localTime;
});An example of twitter profile header effect where two elements (avatar, and header) are updated in sync with scroll offset.
<div id='scrollingContainer'>
<div id='header' style='height: 150px'></div>
<div id='avatar'><img></div>
</div>
<script>
await CSS.animationWorklet.addModule('twitter-header-animator.js');
const animation = new WorkletAnimation('twitter-header',
[new KeyframeEffect($avatar, /* scales down as we scroll up */
[{transform: 'scale(1)'}, {transform: 'scale(0.5)'}],
{duration: 1000, iterations: 1}),
new KeyframeEffect($header, /* loses transparency as we scroll up */
[{opacity: 0}, {opacity: 0.8}],
{duration: 1000, iterations: 1})],
new ScrollTimeline({
scrollSource: $scrollingContainer,
timeRange: 1000,
orientation: 'block',
startScrollOffset: 0,
endScrollOffset: $header.clientHeight}),
);
animation.play();
</script>twitter-header-animator.js:
registerAnimator('twitter-header', class {
constructor(options) {
this.timing_ = new CubicBezier('ease-out');
}
clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
animate(currentTime, effect) {
const scroll = currentTime; // [0, 1]
// Drive the output group effect by setting its children local times.
effect.children[0].localTime = scroll;
// Can control the child effects individually
effect.children[1].localTime = this.timing_(this.clamp(scroll, 0, 1));
}
});<style>
.parallax {
position: fixed;
top: 0;
left: 0;
opacity: 0.5;
}
</style>
<div id='scrollingContainer'>
<div id="slow" class="parallax"></div>
<div id="fast" class="parallax"></div>
</div>
<script>
await CSS.animationWorklet.addModule('parallax-animator.js');
const scrollTimeline = new ScrollTimeline({
scrollSource: $scrollingContainer,
orientation: 'block',
timeRange: 1000
});
const scrollRange = $scrollingContainer.scrollHeight - $scrollingContainer.clientHeight;
const slowParallax = new WorkletAnimation(
'parallax',
new KeyframeEffect($parallax_slow, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}], scrollRange),
scrollTimeline,
{rate : 0.4}
);
slowParallax.play();
const fastParallax = new WorkletAnimation(
'parallax',
new KeyframeEffect($parallax_fast, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}], scrollRange),
scrollTimeline,
{rate : 0.8}
);
fastParallax.play();
</script>parallax-animator.js:
// Inside AnimationWorkletGlobalScope.
registerAnimator('parallax', class {
constructor(options) {
this.rate_ = options.rate;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.rate_;
}
});WorkletAnimation extends Animation and adds a getter for its timelines.
Its constructor takes:
animatiorIdwhich should match the id of an animator which is registered in the animation worklet scope.- A sequence of effects which are passed into a
GroupEffectconstructor. - A sequence of timelines, the first one of which is considered primary timeline and passed to
Animationconstructor.
[Constructor (DOMString animatorName,
optional (AnimationEffectReadOnly or array<AnimationEffectReadOnly>)? effects = null,
AnimationTimeline? timeline,
optional WorkletAnimationOptions)]
interface WorkletAnimation : Animation {
readonly attribute DOMString animatorName;
}TODO: At the moment GroupEffect constructor requires a timing but this seems unnecessary for
WorkletAnimation where it should be possible to directly control individual child effect local
times. We need to bring this up with web-animation spec.
AnimationEffectReadOnly gets a writable localTime attribute which may be used to drive the
effect from the worklet global scope.
partial interface AnimationEffectReadOnly {
[Exposed=Worklet]
// Intended for use inside Animation Worklet scope to drive the effect.
attribute double localTime;
};
The draft specification is the most recent version.