[Proposal] AnimationWorklet - a primitive for scroll-linked and high performance procedural animated effects


#1

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:

  • 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.


[Proposal] Element scroll with ease options
Scroll-linked animations
#2

This proposal was discussed in the CSS Houdini meeting at TPAC, and interest was expressed by Microsoft (Rossen Atanassov), Mozilla (David Baron), Apple (Dean Jackson) and Google to explore this idea further via incubation in the WICG.


#3

Moved: https://github.com/WICG/animation-worklet


#4

Just wanted to mention the usecase of scroll-linked effects, at least for prepared animations, could be viable with the Web Animations API as well.

Since there is potential for extension of Timeline interface which can source time from values such as scroll positions.

For example in the future it might be possible to do the following:

// at 300px start animating foo's opacity over 100px
let effect = new KeyframeEffect(foo, {
	opacity: [0, 1]
}, {
	delay: 300,
	duration: 100
});

let timeline = new ScrollTimeline(window); // e.g. track scrollTop from window
let anim = new Animation(effect, timeline);
anim.play();

This could be then animated off-thread on a compositor thread or however the implementation chose to make high performance viable.

Of course this doesn’t solve the issue or more dynamic effects which a worklet could solve here really well! Such as hidey nav bars that show up (albeit annoyingly) when scrolling slightly up from any point.

I just wanted to point out that the more simple case of scroll-linked effects could be designed already with existing specs in progress with future potential spec extensions to it. That this might spark ideas, raise awareness for this path, and that maybe the AnimationWorklet could work with in a creative or collaborative way.

I think the idea of animation timing functions that existed in an isolated execution environment or otherwise deterministic was also thrown around there.


#5

Nexii, we at Mozilla have been developing precisely such a ScrollTimeline extension to the Web Animations API! The proposal can be found here.