CSS Animation Worklet API

Draft Community Group Report,

This version:
https://wicg.github.io/animation-worklet/
Issue Tracking:
GitHub
Inline In Spec
Editors:

Abstract

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This document introduces a new primitive for creating scroll-linked and other high performance procedural animations on the web. For details on the rationale and motivation see [explainer].

The Animation Worklet API provides a method to create scripted animations that control a set of animation effects. The API is designed to make it possible for user agents to run such animations in their own dedicated thread to provide a degree of performance isolation from main thread.

1.1. Relationship to the Web Animations API

Animations running inside an Animation Worklet execution context expose the Animation interface from the Web Animations specification on the main javascript execution context. This means they can be controlled and inspected from main thread using many of the Web Animation APIs. However Animation Worklet animations follow a different timing model that enables them to be script-driven, stateful, and runnable in a parallel worklet execution context. As such Web Animation APIs that seek or alter the input time (reverse, finish, etc.) have different semantics for Animation Worklet animations.

2. Threading Model

Animation Worklet is designed to be thread-agnostic. Rendering engines may create one or more parallel worklet execution contexts separate from the main javascript execution context, e.g., on their own dedicated threads. Rendering engines may then choose to assign Animation Worklet animations to run in such contexts. Doing so allows Animation Worklet animations to avoid being impacted by main thread jank.

Rendering engines may wish to make a best-effort attempt to execute animate callbacks synchronously with visual frame production to ensure smooth animation. However it is legal for rendering engines to produce visual frames without blocking to receive animation updates from a worklet (i.e., letting the effects slip behind). For example, this could occur when the animate function callback is unable to complete before the frame deadline.

We believe that scripted animations which are run in a parallel execution environment and which limit themselves to animating properties which do not require the user agent to consult main thread will have a much better chance of meeting the strict frame budgets required for smooth playback.

If a Worklet Animation animation is executing in a parallel worklet execution context, the last known state of its animation effects must be periodically synced back to the main javascript execution context. The synchronization of effect values from the parallel worklet execution context to the main javascript execution context must occur before running the animation frame callbacks as part of the document lifecycle. Note that due to the asynchronous nature of this animation model a script running in the main javascript execution context may see a stale value when reading a target property that is being animated in a Worklet Animation, compared to the value currently being used to produce the visual frame that is visible to the user. This is similar to the effect of asynchronous scrolling when reading scroll offsets in the main javascript execution context.

Overview of the animation worklet threading model.
Overview of the animation worklet threading model.

A simplified visualization of how animators running in a parallel execution environment can sync their update to main thread while remaining in sync with visual frame production.

3. Animation Worklet

Animation Worklet is a Worklet responsible for all classes related to custom animations. The worklet can be accessed via animationWorklet attribute.

The animationWorklet's worklet global scope type is AnimationWorkletGlobalScope.

AnimationWorkletGlobalScope represents a global execution context of animationWorklet.

partial interface Window {
    [SameObject] readonly attribute Worklet animationWorklet;
};
callback VoidFunction = void ();

[Global=Worklet]
interface AnimationWorkletGlobalScope : WorkletGlobalScope {
    void registerAnimator(DOMString name, VoidFunction animatorCtor);
};
Note: This is how the class should look.
class FooAnimator {
    constructor(options) {
        // Called when a new animator is instantiated.
    }
    animate(currentTime, effect) {
        // Animation frame logic goes here.
    }
}

4. Animator Definition

An animator definition is a struct which describes the author defined custom animation as needed by AnimationWorkletGlobalScope. It consists of:

4.1. Registering an Animator Definition

An AnimationWorkletGlobalScope has a animator name to animator definition map. The map gets populated when registerAnimator(name, animatorCtor) is called.

When the registerAnimator(name, animatorCtor) method is called in a AnimationWorkletGlobalScope, the user agent must run the following steps:

  1. If name is not a valid <ident>, throw a TypeError and abort all these steps.

  2. If name exists as a key in the animator name to animator definition map, throw a NotSupportedError and abort all these steps.

  3. If the result of IsConstructor(animatorCtor) is false, throw a TypeError and abort all these steps.

  4. Let prototype be the result of Get(animatorCtor, "prototype").

  5. If the result of Type(prototype) is not Object, throw a TypeError and abort all these steps.

  6. Let animate be the result of Get(prototype, "animate").

  7. If the result of IsCallable(animate) is false, throw a TypeError and abort all these steps.

  8. Let definition be a new animator definition with:

  9. Add the key-value pair (name - definition) to the animator name to animator definition map.

5. Animator Instance

An animator instance is a struct which describes a fully realized custom animation instance in an AnimationWorkletGlobalScope. It has a reference to an animator definition and owns the instance specific state such as animation effect and timelines. It consists of:

5.1. Creating an Animator Instance

Each animator instance lives in an AnimationWorkletGlobalScope.

Each AnimationWorkletGlobalScope has an animator instance set. The set is populated when the user agent constructs a new animator instance in the AnimationWorkletGlobalScope scope. Each animator instance corresponds to a worklet animation in the document scope.

To create a new animator instance given a name, timeline, effect, serializedOptions, serializedState, and workletGlobalScope, the user agent must run the following steps:

  1. Let the definition be the result of looking up name on the workletGlobalScope’s animator name to animator definition map.

    If definition does not exist abort the following steps.

  2. Let animatorCtor be the class constructor of definition.

  3. Let timelineList be a new list with timeline added to it.

  4. Let options be StructuredDeserialize(serializedOptions).

  5. Let state be StructuredDeserialize(serializedState).

  6. Let animatorInstance be the result of Construct(animatorCtor, [options, state]).

    If Construct throws an exception, abort the following steps.

  7. Set the following on animatorInstance with:

  8. Add animatorInstance to workletGlobalScope’s animator instance set.

5.2. Running Animators

When a user agent wants to produce a new animation frame, if for any animator instance the associated animation requested flag is frame-requested then the the user agent must run animators for the current frame.

Note: The user agent is not required to run animations on every visual frame. It is legal to defer generating an animation frame until a later frame. This allow the user agent to provide a different service level according to their policy.

When the user agent wants to run animators in a given workletGlobalScope, it must iterate over all animator instances in the workletGlobalScope’s animator instance set. For each such instance the user agent must perform the following steps:

  1. Let animatorName be instance’s animator name

  2. Let the definition be the result of looking up animatorName on the workletGlobalScope’s animator name to animator definition map.

    If definition does not exist then abort the following steps.

  3. If the animation requested flag for instance is frame-current or the effect belonging to the instance will not be visible within the visual viewport of the current frame the user agent may abort all the following steps.

    Consider giving user agents permission to skip running animator instances to throttle slow animators.

  4. Let animateFunction be definition’s animate function.

  5. Let currentTime be animator current time of instance.

  6. Let effect be animator effect of instance.

  7. Invoke animateFunction with arguments «currentTime, effect», and with instance as the callback this value.

Note: Although inefficient, it is legal for the user agent to run animators multiple times in the same frame.

5.3. Removing an Animator Instance

To remove an animator instance given instance and workletGlobalScope the user agent must run the following steps:

  1. Remove instance from workletGlobalScope’s animator instance set.

5.4. Migrating an Animator Instance

User agents are responsible for assigning an animator instance to a WorkletGlobalScope. There can be many such WorkletGlobalScopes, which may exist across different threads or processes. To give the most flexibility to user agents in this respect, we allow migration of an animator instance while it is running. The basic mechanism is to serialize the internal state of any author-defined effect, and restore it after migration.

To migrate an animator instance from one WorkletGlobalScope to another, given instance, sourceWorkletGlobalScope, destinationWorkletGlobalScope, the user agent must run the following steps :

  1. Let serializedState be undefined.

  2. Queue a task on sourceWorkletGlobalScope to run the following steps:

    1. Let animatorName be instance’s animator name

    2. Let definition be the result of looking up animatorName on sourceWorkletGlobalScope’s animator name to animator definition map.

      If definition does not exist then abort the following steps.

    3. Let prototype be the result of Get(animatorCtor, "prototype").

    4. Let onDestroyFunction be the result of Get(prototype, "onDestroy").

    5. If the result of IsCallable(onDestroyFunction) is false then abort the following steps.

    6. Invoke onDestroyFunction with instance as the callback this value and let state be the result of the invocation.

    7. Set serializedState to be the result of StructuredSerialize(state). If any exception is thrown, then abort the following steps.

    8. Run the procedure to remove an animator instance given instance, and sourceWorkletGlobalScope.

  3. Wait for the above task to complete. If the task is aborted, abort the following steps.

  4. Queue a task on destinationWorkletGlobalScope to run the following steps:

    1. Run the procedure to create a new animator instance given:

5.5. Requesting Animation Frames

Each animator instance has an associated animation requested flag. It must be either frame-requested or frame-current. It is initially set to frame-current. Different circumstances can cause the animation requested flag to be set to frame-requested. These include the following:

§5.2 Running Animators resets the animation requested flag on animators to frame-current.

6. Web Animations Integration

6.1. Worklet Animation

Worklet animation is a kind of animation that delegates the animation playback to an animator instance. It controls the lifetime and playback state of its corresponding animator instance.

Being an animation, worklet animation has an animation effect and a timeline. However unlike other animations the worklet animation’s current time does not directly determine the animation effect’s local time (via its inherited time). Instead the associated animator instance controls the animation effect’s local time directly. Note that this means that the timeline’s current time does not fully determine the animation’s output.

Worklet animation has the following properties in addition to the Animation interface:

Overview of the WorkletAnimation timing model.
Overview of the WorkletAnimation timing model.

The animation current time is input to the animator instance, which produces a local time value for the animation effect. If the animator instance is running in a parallel global scope the implementation may also choose to use the local time value to produce the final effect value and update the visuals in parallel.

6.2. Creating a Worklet Animation

[Constructor (DOMString animatorName,
              optional (AnimationEffectReadOnly or array)? effects = null,
              AnimationTimeline? timeline,
              optional WorkletAnimationOptions)]
interface WorkletAnimation : Animation {
        readonly attribute DOMString animatorName;
};

WorkletAnimation(animatorName, effects, timeline, options)

Creates a new WorkletAnimation object using the following procedure.

  1. Let workletAnimation be a new WorkletAnimation object.

  2. Run the procedure to set the timeline of an animation on workletAnimation passing timeline as the new timeline or, if a timeline argument is not provided, passing the default document timeline of the Document associated with the Window that is the current global object.

  3. Let effect be the result corresponding to the first matching condition from below.

    If effects is a AnimationEffectReadOnly object,

    Let effect be effects.

    If effects is a list of AnimationEffectReadOnly objects,

    Let effect be a new WorkletGroupEffect with its children set to effects.

    Otherwise,

    Let effect be undefined.

  4. Run the procedure to set the target effect of an animation on workletAnimation passing effect as the new effect.

  5. Let serializedOptions be the result of StructuredSerialize(options). Rethrow any exceptions.

  6. Set the serialized options of workletAnimation to serializedOptions.

  7. Set the animation animator name of workletAnimation to animatorName.

6.3. Worklet Animation timing model

This section describes how worklet animation’s timing model differs from other animations.

In addition to the existing conditions on when the animation is considered ready, a worklet animation is only considered ready when the following condition is also true:

As described in §6.1 Worklet Animation, the worklet animation’s current time does not determine its animation effect’s local time. Instead the associated animator instance controls the animation effect’s local time directly. This means that the animation effect’s local time is controlled from a WorkletGlobalScope which may be in a parallel execution context.

Here are a few implications of the above semantics:

Come with appropriate mechanism’s for animator instance to get notified when its animation currentTime is changing e.g., via reverse(), finish() or playbackRate change. So that it can react appropriately. <https://github.com/wicg/animation-worklet/issues/63>

6.4. Interaction with Animator Instances

A worklet animation corresponds to at most one animator instance at any time, and may have no current corresponding animator instance. The correspondance of an animator instance for a worklet animation depends on the animation play state.

To associate animator instance of worklet animation given workletAnimation, the user agent must run the following steps:

  1. If workletAnimation has a corresponding animator instance, abort the following steps.

  2. Let workletGlobalScope be the AnimationWorkletGlobalScope associated with workletAnimation.

  3. Queue a task on workletGlobalScope to run the procedure to create a new animator instance, passing:

  4. If the procedure was successful, set the resulting animator instance as corresponding to workletAnimation.

To disassociate animator instance of worklet animation given workletAnimation, the user age must run the following steps:

  1. If workletAnimation does not have a corresponding animator instance, abort the following steps.

  2. Let workletGlobalScope be the AnimationWorkletGlobalScope associated with workletAnimation.

  3. Let animatorInstance be workletAnimation’s corresponding animator instance.

  4. Queue a task on the workletGlobalScope to run the procedure to remove an animator instance, passing animatorInstance as instance and workletGlobalScope as workletGlobalScope.

  5. Set workletAnimation as having no corresponding animator instance.

To set animator instance of worklet animation given workletAnimation, the user age must run the following steps:

  1. disassociate animator instance of worklet animation given workletAnimation.

  2. associate animator instance of worklet animation given workletAnimation.

When a given workletAnimation’s play state changes to pending, running, or paused, run the procedure to associate a worklet animation with its animator instance given workletAnimation.

When a given workletAnimation’s play state changes to idle or finished, run the procedure to disassociate a worklet animation with its animator instance given workletAnimation.

When the procedure to set the target effect of an animation for a given workletAnimation is called, then set animator instance of worklet animation given workletAnimation.

When the procedure to set the timeline of an animation for a given workletAnimation is called, then set animator instance of worklet animation given workletAnimation.

6.5. Timeline Attachment

Define semantics of attachment and detachment. <https://github.com/wicg/animation-worklet/issues/61>

6.6. ScrollTimeline

ScrollTimeline is a new concept being proposed for addition to web animation API. It defines an animation timeline whose time value depends on the scroll position of a scroll container. Worklet animations can have a scroll timeline and thus drive their scripted effects based on a scroll offset.

Note: Access to input: We are interested on exposing additional user input beside scrolling (e.g., touch/pointer input) to these animations so that authors can create jank-free input driven animations which are not really possible today. We are still trying to figure out the right abstractions and mechanisms to do this.

6.7. WorkletGroupEffect

WorkletGroupEffect is a type of group effect that allows its child effect’s local times to be mutated individually.

When a WorkletGroupEffect is set as the animation effect of a WorkletAnimation, the corresponding animator instance can directly control the child effects' local times. This allows a single worklet animation to coordinate multiple effects - see §9.2 Example 2: Twitter header. for an example of such a use-case.

interface WorkletGroupEffectReadOnly :  GroupEffectReadOnly {};

interface WorkletGroupEffect :  WorkletGroupEffectReadOnly {};
WorkletGroupEffect implements AnimationEffectMutable;
WorkletGroupEffect implements GroupEffectMutable;

[Exposed=Worklet]
partial interface AnimationEffectReadOnly {
    // Intended for use inside Animation Worklet scope to drive the effect.
    attribute double localTime;
};

To set the localTime property on a effect to value t, the user agent should perform the action that corresponds to the first matching condition from the following:

If the effect does not have a parent group,

Set the effect local time to t.

If the effect has a parent group and it is of WorkletGroupEffect type,

Set the effect start time to (parent’s transformed time - t). Note this effectively set’s the effect’s local time to t.

Otherwise

Throw an exception indicating that the child effect time can only be controlled by its parent group.

The above algorithm should probably only apply to within the AnimationWorkletGlobalScope?

[Issue](https://github.com/w3c/web-animations/issues/191)

6.8. Effect Stack and Composite Order

As with other animations, worklet animations participate in the effect stack. A worklet animation does not have a specific animation class which means it has the same composite order as other Javascript created web animations.

7. Security Considerations

Need to decide what security considerations there are for this spec.

8. Privacy Considerations

Need to decide what privacy considerations there are for this spec.

9. Examples

9.1. Example 1: Hidey Bar.

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>
animationWorklet.addModule('hidey-bar-animator.js').then( _ => {
  const scrollTimeline = new ScrollTimeline($scrollingContainer, {timeRange: 100});
  const documentTimeline = document.timeline;

  // Note we pass in two timelines in the options bag which allows the animation to read their
  // currenTime values directly.
  const workletAnim = new WorkletAnimation('hidey-bar',
        new KeyFrameEffect($header,
                          [{transform: 'translateX(100px)'}, {transform: 'translateX(0px)'}],
                          {duration: 100, iterations: 1, fill: 'both' }]),
        scrollTimeline,
        {scrollTimeline, documentTimeline});
});


workletAnim.timeline == scrollTimeline; // true
</script>
// Inside AnimationWorkletGlobalScope

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;

    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;
  }
});

This example uses 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. A simple fallback can emulate this by detecting when timeline time (i.e. scroll offset) has not changed in the last few frames.

9.2. Example 2: Twitter header.

An example of twitter profile header effect where two elements (avatar, and header) are updated in sync with scroll offset.
// In document scope
<div id='scrollingContainer'>
  <div id='header' style='height: 150px'></div>
  <div id='avatar'><img></div>
</div>

<script>
animationWorklet.addModule('twitter-header-animator.js').then( _ => {
  const workletAnim = new WorkletAnimation('twitter-header',
    [new KeyFrameEffect($avatar,  /* scales down as we scroll up */
                        [{transform: 'scale(1)'}, {transform: 'scale(0.5)'}],
                        {duration: 1, iterations: 1}),
     new KeyFrameEffect($header, /* loses transparency as we scroll up */
                        [{opacity: 0}, {opacity: 0.8}],
                        {duration: 1, iterations: 1})],
     new ScrollTimeline($scrollingContainer, {timeRange: 1, startScrollOffset: 0, endScrollOffset: $header.clientHeight}),
  );

  // Same animation instance is accessible via different animation targets
  workletAnim == $avatarEl.getAnimations()[0] == $headerEl.getAnimations()[0];

});
</script>
// Ins AnimationWorkletGlobalScope.
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;  // scroll is in [0, 1] range

    // Drive the output group effect by setting its children local times individually.
    effect.children[0].localTime = scroll;
    effect.children[1].localTime = this.timing_(clamp(scroll, 0, 0.5));
  }
});

9.3. Example 3: Parallax backgrounds.

TODO

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-VALUES-3]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 3. URL: https://www.w3.org/TR/css-values-3/
[CSS3-TRANSITIONS]
Dean Jackson; et al. CSS Transitions. URL: https://www.w3.org/TR/css3-transitions/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[WEB-ANIMATIONS-1]
Brian Birtles; et al. Web Animations. URL: https://www.w3.org/TR/web-animations-1/
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. URL: https://heycam.github.io/webidl/
[WORKLETS-1]
Ian Kilpatrick. Worklets Level 1. URL: https://www.w3.org/TR/worklets-1/

Informative References

[EXPLAINER]
Animation Worklet Explainer. CR. URL: https://github.com/WICG/animation-worklet/blob/gh-pages/README.md

IDL Index

partial interface Window {
    [SameObject] readonly attribute Worklet animationWorklet;
};

callback VoidFunction = void ();

[Global=Worklet]
interface AnimationWorkletGlobalScope : WorkletGlobalScope {
    void registerAnimator(DOMString name, VoidFunction animatorCtor);
};

[Constructor (DOMString animatorName,
              optional (AnimationEffectReadOnly or array)? effects = null,
              AnimationTimeline? timeline,
              optional WorkletAnimationOptions)]
interface WorkletAnimation : Animation {
        readonly attribute DOMString animatorName;
};


interface WorkletGroupEffectReadOnly :  GroupEffectReadOnly {};

interface WorkletGroupEffect :  WorkletGroupEffectReadOnly {};
WorkletGroupEffect implements AnimationEffectMutable;
WorkletGroupEffect implements GroupEffectMutable;

[Exposed=Worklet]
partial interface AnimationEffectReadOnly {
    // Intended for use inside Animation Worklet scope to drive the effect.
    attribute double localTime;
};

Issues Index

Consider giving user agents permission to skip running animator instances to throttle slow animators.
Come with appropriate mechanism’s for animator instance to get notified when its animation currentTime is changing e.g., via reverse(), finish() or playbackRate change. So that it can react appropriately. <https://github.com/wicg/animation-worklet/issues/63>
Define semantics of attachment and detachment. <https://github.com/wicg/animation-worklet/issues/61>
The above algorithm should probably only apply to within the AnimationWorkletGlobalScope?
Need to decide what security considerations there are for this spec.
Need to decide what privacy considerations there are for this spec.
This example uses 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. A simple fallback can emulate this by detecting when timeline time (i.e. scroll offset) has not changed in the last few frames.