core/animation

This directory contains the main thread animation engine. This implements the Web Animations timing model that drives CSS Animations, Transitions and exposes the Web Animations API (e.g. element.animate()) to Javascript.

Contacts

Specifications implemented

Types of animations

There are 3 main types of animations that are supported in this directory subtree: CSS animations, CSS transitions, and web animations. These animation types are largely differentiated by their creation mechanism, but also have differences in behavior such as rules for composite ordering. Nonetheless, all three animation types share a common base class (Animation) with the majority of the code being common to all three animation types.

CSS animation

CSS animations are created via CSS rules, whereby an animation-name property (or animation shorthand property) refers to one or more keyframes rules. Keyframes specify property values at various positions along the animation curve. For example:

@keyframes fade {
  0% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes redshift {
  to { color: red; }
}
.fade-out {
  animation: fade 1s linear, redshift 400ms ease-in-out;
}

The labels ‘from’ and ‘to’ are aliases for ‘0%’ and ‘100%’, respectively. Keyframes may specify different property values, and the set of keyframes is not required to specify values at 0% or 100% (partial keyframes). If missing starting or ending values for a property, a neutral property values is used based on the underlying value. In the above example, the redshift animation will animate from the current color to red.

This example illustrates just a few of the options for generating keyframes. A more detailed explanation can be found on the MDN @keyframes page. A more detailed explanation of the animation shorthand property and its longhand counterparts can be found on the MDN > CSS > animation page.

CSS Animations are created during style update in a multi-stage process. Keyframe models for pending animations are created in CalculateAnimationUpdate, which in turn calls CreateKeyframeEffectModel. The algorithm for constructing keyframes for a CSS animation is outlined in css-animations-2/#keyframes. New animations are constructed for the pending animations in MaybeApplyPendingUpdate. This method also handles updating the timing model of existing CSS animations or canceling CSS animations.

Changes to the phase of a CSS animation may result in firing one or more AnimationEvents. Each CSS animation has an associated AnimationEventDelegate with an OnEventCondition method, which handles dispatching of these events.

The CSSAnimation interface extends the Animation interface to include an animationName property, which indicated the name of the keyframes rule associated with the animation. Note that this association may be broken by interacting with the animation via the web-animation API.

CSS transition

Like CSS animations, CSS transitions are triggered by style rules in CSS; however, unlike CSS animations, the keyframes are inferred. For example:

button {
  background: blue;
  transition: background-color 150ms ease-in;
}

button:hover {
  background: #05f
}

In this example, hovering over a button changes the background color. A transition animation is created from the previous background color to the new background color. A more detailed description of the transition property and its longhand counterparts can be found on the MDN > CSS > Transitions page.

CSS Transitions are created during style update in a multi-stage process. Keyframe models for pending animations are created in CalculateTransitionUpdate, which iterates over names in transition-property calling CalculateTransitionUpdateForProperty for each property. If there is already an active transition for the property and the value of the property changes, the transition is retargeted to the new end point. When retargeting an animation, the current position is used as a starting point, which is calculated by applying active animations with an updated timestamp to the underlying style in CalculateBeforeChangeStyle. The before-change style is lazy evaluated to avoid unnecessary work when no transition updates are required, and is computed at most once per element per non-animation style update. New transitions are constructed for the pending transitions in MaybeApplyPendingUpdate. This method handles constructing new transitions, retargeting active transitions, and canceling finished transitions.

Changes to the phase of a CSS transition may result in firing one or more TransitionEvents. Each CSS transition has an associated TransitionEventDelegate with an OnEventCondition method, which handles dispatching of these events.

The CSSTransition interface extends the Animation interface to include an transitionProperty property, which indicated the name of the property being transitioned. Note that this association may be broken by interacting with the transition via the web-animation API.

Web Animation

Web animations are programmatically created in JavaScript via calls to Element.animate. Similar to CSS animations, web-animations require a set of keyframes; however, the representation of the keyframes is expressed as a list or object in JavaScript. For example:

const slideAnimation = element.animate(
  [ { offset: 1, transform: 'translateX(200px)', easing: 'linear' } ],
  { duration: 1000,  easing: linear });
const gravityAnimation = element.animate(
  { transform: ['none', 'translateY(400px' ] },
  { duration: 1000, easing: 'ease-in', composite: 'add' });

This example illustrates two formats for expressing the list of keyframes. The keyframes argument to animate may be a list of keyframes with optional offsets. If offsets are not specified, then equal spacing is assumed. The first of the two animations illustrates the use of partial keyframes. For this animation, a neutral keyframe is inferred at offset 0.

If animating a single property, they keyframes may be expressed as a object with the property name as a key and a list of string/numeric values for the property. In this case, equally spaced keyframes are implied. This example also illustrates the use of composite modes to combine multiple effects on a single property. Since the easing functions are different for the two animations, they cannot be combined into a single animation to achieve the desired results.

A more detailed description of the animate method can be found on the MDN Element.animate page.

Web animation model

At a fundamental level, the web animation model converts a time value to one or more property values. This is true whether we are using a DocumentTimeline driven by an AnimationClock, or a ScrollTimeline that converts a scroll position to an abstract representation of time. The same rules also apply for CSSAnimations and CSSTransitions, which derive from the base Animation class.

The web animation model can be further broken down into two sub-models:

  1. A ‘timing model’ which converts time to an iteration index and progress (proportional value between 0 and 1).

  2. An ‘animation model’ which converts the progress to property values.

The division of responsibilities can best be illustrated through an example. Consider the following:

@keyframes fade {
  from { opacity: 1; animation-timing-function: ease-in; }
  50% { opacity: 0.5; animation-timing-function: ease-out; }
  to { opacity: 0; }
}
.pulse {
  animation-name: fade;
  animation-iteration-count: 3;
  animation-duration: 0.5s;
  animation-delay: 200ms;
  animation-direction: alternate-reverse;
  animation-fill: both;
}
element.classList.add('pulse');

Prior to the 200ms mark, the animation is in the ‘before’ phase. since the elapsed time is less than the start delay (200ms). Conversely, after the 1.7s mark (start delay + itertions * duration), the animation is in the ‘after’ phase. Between these time boundaries, we are in the ‘active’ phase, and the timing model is applied to calculate the iteration index and progress.

At the 1s mark of the animation, we are 0.8s into playing the animation (active time) due to the start delay. Each iteration takes 0.5s, putting us 0.3s into the second iteration, which is in the forward direction since alternating and initially playing backwards (alternate-reverse). The last step of the timing model is to convert the iteration time to an iteration progress. This is simply the ratio of the iteration time to the iteration duration (0.3s / 0.5s = 0.6).

Conversely, at the 0.5s mark, we are 0.3s into an iteration playing in the reverse direction. As the iteration progress measures progress in the forward direction, the iteration progress becomes 1 - directional progress, which is 1 - 0.3s / 0.5s = 0.4.

The animation model converts the iteration index and progress into property values. As the iteration-composite property is not supported at this time, only the iteration progress is a factor in our calculations. The iteration progress is fed into the keyframes model to compute property values.

Each keyframe has an offset, and for each property, we determine the keyframe pair that bounds the iteration progress. Returning to our example with an iteration progress of 0.6 at the 1s mark, we are iterating between the ‘50%’ and ‘to’ keyframes. The relative progress between these frames is (0.6 - 0.5) / (1.0 - 0.5) = 0.2. This value is the input to our animation-timing-function. The timing function for this keyframe pair is ‘ease-out’, which is equivalent to cubic-bezier(0, 0, 0.58, 1)). Plugging an input value of 0.2 into our cubic-bezier function, we get an output of roughly 0.31, which is the local progress value used for interpolation between the two keyframes. For any scalar-valued property, the interpolation function is:

v = progress * v_1 + (1 - progress) * v_0

For our example,

opacity = 0.31 * 0 + (1 - 0.31) * 0.5 = 0.35

The interpolation procedure is less straightforward for non-scalar values (especially for transform lists). Nonetheless, this example provides a good overview of the web animation model.

Points of interest in the Blink code base:

  • timing_calculations.h: contains static helper functions for various timing calculations used in the animation model such as determining the phase of the animation, iteration progress and transformed progress.
  • AnimationEffect::UpdateInheritedTime: applies the web animation model and dispatches animation/transition events.
  • InterpolationEffect::GetActiveInterpolations: creates a list of active interpolations by determining keyframe-pairs that bound the iteration progress and determining the ‘local’ progress between keyframes. If a timing function is specified for the interpolation, it is applied when computing the ‘local’ progress.
  • KeyframeEffect::ApplyEffects: Samples the keyframe effect, adding it to the effect stack if necessary and signaling that an animation style recalc is needed if the sampled value changed.
  • Animation::Update: Called in response to ticking the animation timeline or to revalidate outdated animations. In addition to applying the web animation model via UpdateInheritedTime, this method determines if the animation is finished.

Web-animation API

The Web-animation API provides a unified framework for interacting with animations regardless of the animation type (creation mechanism). CSS animations and transitions can also be manipulated via the API even though not created via the Element.animate method. The following sections cover each of the JavaScript extensions that build up the web-animation API.

Animatable

Objects that may be the target of a KeyframeEffect implement the Animatable interface. At present, this set of objects is restricted to Element and derived classes. Refer to the web-animation section under animation types for a description of Element.animate.

The animatable interface also includes a getAnimations method, which returns all active animations associated with the element in composite ordering. The rules for composite ordering are quite involved and presently span three specifications:

Style updates for CSS transitions, are applied before updates for CSS animations, which in turn are applied before generic web animations. Each affect, when applied, can replace or combine with the previous effects in the effect stack depending on the composite mode (refer to discussion of composite modes in the KeyframeEffect section). Additional rules apply for sorting CSS transitions and CSS animations.

Animation

The Animation interface contains methods and attributes for programmatically interacting with animations. Animation objects may be created via the constructor or the Element.animate method covered previously. Animation objects may be queried via a getAnimations call. The query retrieves all animations regardless of type and may be used to interact with CSS animations or transitions.

The animation API consists of the following attributes and methods:

  • Animation constructor: This constructor is called regardless of the type of animation. The constructor may also be called directly from JavaScript to create a web animation, which is particularly useful when a timeline other than the default document timeline is being used.

    const keyframeEffect = new KeyframeEffect(...);
    const timeline = new ScrollTimeline(...);
    const animation = new Animation(keyframeEffect, timeline);
    // Unlike Element.animate, an animation created directly via the constructor
    // does not autoplay.
    animation.play();
    
  • id attribute: sets or gets an id that can be used to identify the animation. These ids are purely informational, but may be used by the developer to facilitate bookkeeping or debugging.

  • effect attribute: gets or sets the AnimationEffect for the animation. The algorithm for setting the effect of an animation is outlined in web-animations -- Setting the associated effect of an animation. A few extra steps are required for CSS animations and transitions, which have an AnimationEffect::EventDelegate that need to be reattached after the effect is updated. Special handling is also required if the new effect is null to cancel or finish the CSS animation / transition.

    // Clone an animation effect.
    const animationA = elementA.animate(...);
    const animationB = elementB.animate(animationA.effect.getKeyframes(),
                                        animationA.effect.getTiming());
    
  • readonly timeline attribute: At present Blink only supports fetching the associated timeline. Efforts are underway to support setting the timeline as well, which is useful for attaching a scroll timeline to an animation created via Element.animate. Refer to the Timelines section of the web animation spec for more details.

    const animation = element.animate(...);
    assert_equals(animation.timeline, document.timeline);
    
  • startTime attribute: gets or sets the start time of an animation. The start time is unresolved when the animation is paused, idle or play-pending (scheduled to play but not yet acked by the client). A running (if not play-pending) or finished animation has a resolved start time. The current time of an animation is determined from the timeline time, start time and playback rate if the animation is not paused or finished. Setting the start time of an animation is applied immediately rather than waiting on an ack from the client; however, a resulting change in the finished state will not resolve a finished promise or queue an onfinish handler until the next microtask checkpoint since the state change may be temporary. The setStartTime method is overridden for CSSAnimations since calling it may update the animation-play-state property by unpausing a CSS animation. Refer to Web-animations -- Setting the start time of an animation for more detail.

    const animation = element.animate(...);
    // The newly created animation is scheduled to play, but the start time is
    // unresolved until acked by the client agent.
    assert_true(animation.pending);
    assert_equals(animation.playState, 'running');
    assert_false(!!animation.startTime);
    animation.ready.then(() => {
      assert_false(animation.pending);
      assert_times_equal(animation.startTime, document.timeline.currentTime);
    });
    
    const animation = element.animate(...);
    aniamtion.pause();
    // The animation is in a pending-paused state.
    assert_true(animation.pending);
    assert_equals(animation.playState, 'paused');
    assert_false(!!animation.startTime);
    assert_equals(animation.currentTime, 0);
    // Explicitly setting the start time aborts the pending-pause, and starts the
    // animation.
    animation.startTime = document.timeline.currentTime;
    assert_false(animation.pending);
    assert_equals(animation.playState, 'running');
    assert_times_equal(animation.startTime, document.timeline.currentTime);
    assert_equals(animation.currentTime, 0);
    
  • currentTime attribute: gets or sets the current time of an animation. When setting the current time, either the start time or hold time of the animation is updated depending on the play state. The start time is updated for a running or finished animation, and the hold time is updated for an idle or paused animation. The change is applied immediately and does not wait on an ack from the client. Similar to setting the start time, updating the current time will not resolve a finished promise or queue an onfinished event until the next microtask checkpoint since the state change may be temporary. Refer to Web-animations -- The current time of an animation and Web-animations -- Setting the current time of an animation for more detail.

    const animation = element.animate(...);
    // Advance to 1000ms mark in the animation. Since the animation is
    // play-pending the hold time is updated. Once ready, the start time will
    // be set to align with the hold time.
    animation.currentTime = 1000;
    assert_true(animation.pending);
    assert_equals(animation.playState, 'running');
    animation.ready.then(() => {
      assert_false(animation.pending);
      assert_times_equal(animation.startTime,
                         document.timeline.currentTime - 1000);
      assert_times_equals(animation.currentTime, 10000);
    });
    
  • playbackRate attribute: gets or sets the playback rate of the animation. An animation with a negative value for the playback rate plays in the reverse direction. The setter is closely related to the updatePlaybackRate method with the difference that with this setter the change takes place immediately and does not require an ack from the client. The getter reports the active playback rate and not the pending playback rate. These values may differ for a brief time interval if using updatePlaybackRate to asynchronously change the playback rate. The algorithm for setting the playback rate is covered under web animations -- setting the playback rate on an animation in the spec. An additional step in required in Blink to ensure that a composited animation is kept in sync with a change to the playback rate and to prevent a discontinuity (jump) in the animation. Typically, using updatePlaybackRate is preferable to using the setter.

    const animation = element.animate(...);
    assert_equals(animation.playbackRate, 1);
    animation.playbackRate = -1;
    // The change to the playback rate is reflected immediately.
    assert_equals(animation.playbackRate, -1);
    
  • readonly playState attribute: gets the play state of an animation, which may be one of the following: ‘idle’, ‘paused’, ‘running’, or ‘finished’. At present, the play state in Blink has an extra state called ‘pending’, which is not used in web animations but is still externally referenced. The play state is forward looking insofar as a pending-play animation will report running and a pending-pause animation will report paused. The pending attribute can be checked to disambiguate whether the reported play state reflects the current or the scheduled state. The algorithm for determining the play state is outlined in web animations -- play states.

    const animation = element.animate(...);
    // play state is updated even though the animation has not ticked.
    assert_equals(animation.playState, 'running');
    animation.pause();
    assert_equals(animation.playState, 'paused');
    
  • readonly replaceState attribute: gets the replace state of an animation, which may be one of the following: ‘active’, ‘removed’, or ‘persisted’. A replaceState of ‘active’ indicates that the animation is in effect, but may be replaced if conditions are satisfied (finished and all affected properties are also affected by other finished animations higher in composite order). A ‘removed’ animation is an animation that is finished and marked for removal due to being replaceable. A persisted animation is an animation that has been explicitly marked for exclusion for the automated removal process via the persist method. The procedure for marking and removing animations is covered in web animations - replacing animations. In the Blink implementation, identifying which animations are replaceable is done in Animation::IsReplaceable. Removal of replaced animations is done in AnimationTimeline::RemoveReplacedAnimations, which calls Animation::RemoveReplacedAnimation. Removal is done during the timeline update cycle after animations have ticked and their finished states have been updated.

    function commitPersistedAnimations() {
      document.getAnimations().forEach((anim) => {
        if (anim.playState == finished &&
            anim.replaceState == persisted’) {
          anim.commitStyles();
          anim.cancel();
        }
      });
    };
    
  • readonly pending attribute: gets the pending status of an animation. Changes to the play state via the play, pause, and reverse methods do not take effect immediately, but instead schedule a task to execute once the animation is ready (acked by the client agent). An animation in the ‘running’ playState is pending until it receives a start time. Conversely, a pending-pause animation may still have a start-time until the client agent is ready to pause the animation. As the play state of a CSS animation may also be changed via the animation-play-state property, a style flush is required when querying the play state of a CSS animation.

    div.style.animation = `fade 1s linear`;
    const animation = div.getAnimations[0];
    assert_equals(animation.playState, 'running');
    assert_true(animation.pending);
    animation.ready.then(() => {
      assert_false(animation.pending);
      // Updating the play state via the animation-play-state property causes
      // the aniamtion to be pause-pending.
      div.style.animationPlayState = 'paused';
      assert_equals(animation.playState, 'paused');
      assert_true(animation.pending);
      animation.ready.then(() => {
        assert_false(animation.pending);
      });
    });
    
  • readonly ready attribute: The ready promise is resolved when pending play or paused operations are acked by the client agent. A change that requires an ack from the client agent must call Animation::SetCompositorPending. The method name is somewhat misleading as it applies whether or not the animation is actually run on the compositor. The method updates the list of pending animations if required. In turn, PendingAnimations::Update updates the list of animations that are waiting for a start time, and notifies that the animation is ready if not requiring a start time. If all animations needing a start time are main-thread animations, they are also marked as ready. If at least one animation is composited, all new animations created during the update cycle must wait on the compositor in order to properly synchronize the start times. Animations from a previous cycle are exempt from start synchronization to guard against plugging up of the animation pipeline. Composited animations call PendingAnimations::NotifyCompositorAnimationStarted with the start time, which in turn calls Animation::NotifyReady. If all animations for an element are running on the main thread, then NotifyCompositorAnimationStarted is called directly. Again, the method name is somewhat misleading.

    const slideAnimation = element.animate(...);
    const fadeAnimation = element.animate(...);
    slideAnimation.ready.then(() => {
      // Synchronize the fade animation to run 1s after the start of the slide
      // animations.
      fadeAnimation.startTime = slideAnimation + 1000;
    })
    
  • readonly finished attribute: The finished promise is resolved after an animation finishes. Since the finished state may be temporary, the resolution is deferred until the next microtask checkpoint. This delay facilitates getting consistent results when the ordering of API calls is changed. For example, setting the current time to the end time and reversing the direction of the animation should not resolve the finished promise. API calls that affect current time or play state must update the finished state of the animation. The algorithm is outlined in web animation -- updating the finished state. In Blink, updating the finished state and scheduling the microtask is performed in Animation::UpdateFinishedState. The microtask is handled in Animation::AsyncFinishMicrotask. The finished promise is convenient for chaining together animations and for applying style updates.

      const animation = element.animate(keyframes,
                                        { duration: 1000,
                                          fill: `forwards`
                                        });
      animation.finished.then(() => {
        animation.commitStyles();
        animation.cancel();
        // Follow up animation.
        element.animate(...);
      });
    
  • onfinished attribute: gets or sets the onfinished event handler that allows the web developer to attach customized JavaScript to run once an animation has finished.

    const animation = element.animate(keyframes,
                                      {
                                        duration: 1000
                                        fill: 'forwards'
                                      });
    animation`.onfinished = () => {
      animation.commitStyles();
      animation.cancel();
    };
    
  • oncancel attribute: gets or sets the oncancel event handler that allows the web developer to attach customized JavaScript to run once an animation has been canceled.

    const animation = element.animate(keyframes,
                                      {
                                        duration: 1000
                                        fill: 'forwards'
                                      });
    animation.oncancel = () => {
      myTrackedAnimations.remove(animation);
    };
    
  • onremove attribute: gets or sets the onremove event handler that allows the web developer to attach customized JavaScript to run once an animation has been removed.

    const animation = element.animate(keyframes,
                                      {
                                        duration: 1000
                                        fill: 'forwards'
                                      });
    animation.onremove = () => {
      animation.commitStyles();
    };
    
  • cancel nethod: synchronously cancels an animation. This operation will reject any pending read or finished promise, cancel any pending play or pause task, and queue a cancel event.

    const animation = element.animate(keyframes,
                                      { duration: 1000, fill: 'forwards' });
    animation.finished.then(() => {
      assert_unreached();
    });
    animation.ready.then(() => {
      assert_unreached();
    });
    animation.currentTime = 500;
    animation.cancel();
    assert_equals(animation.currentTime, null);
    assert_equals(animation.playState, 'idle');
    assert_false(animation.pending);
    
  • finish method: synchronously updates the current time of the animation to be the end time. If playing in the revsere direction, the current time will be set to zero. The finished promise is immediately resolved and finish event queued.

    const animation = element.animate(keyframes,
                                      { duration: 1000, fill: 'forwards' });
    animation.finish();
    assert_equals(animation.currentTime, 1000);
    assert_equals(animation.playState, 'finished');
    assert_false(animation.pending);
    
  • play method: schedules an animation to begin playing. The animation will resume playing from the hold time. If the hold time is unresolved at the time play is called, it will be set to the start or end time depending on the direction of the pending playback rate. The initialization procedure is altered slightly if a scroll timeline is used instead of a document timeline. In this case, the start time is set directly. Nonetheless, an ack is still required from the client agent. Animation::NotifyReady calls Animation::CommitPendingPlay to run the microtask for syncing the start time, resetting the hold time and resolving the ready promise.

    const animation = new Animation(kefyrameEffect, document.timeline);
    animation.play();
    
  • pause nethod: schedules an animation to pause. The pending state remains true until acked from the client agent. Animation::NotifyReady calls Animation::CommitPendingPause to run the microtask for updating the hold time, resetting the start time, and resolving the ready promise.

    const animation = element.animate(keyframes,
                                      { duration: 1000, fill: 'forwards' });
    button.onclick = () {
      if (animation.playState == 'running')
        animation.pause();
      else
        animation.play();
    }
    
  • updatePlaybackRate nethod: sets a pending playback rate for the animation. The change does not take effect until acked by the client agent. Animation::NotifyReady calls Animation::CommitPendingPlay or Animation::CommitPendingPause depending on the play state. The algorithm is specced in web animations -- seamlessly updating the playback rate....

    const animation = element.animate(...);
    // The playback rate is updated even though the animation has not ticked.
    assert_equals(animation.playbackRate, 1);
    aniamtion.playbackRate = -1;
    assert_equals(animation.playbackRate, -1);
    animation.updatePlayback(-2);
    // The playback rate is not updated until acked by the client agent.
    assert_equals(animation.playbackRate, -1);
    assert_true(animation.pending);
    animation.ready.then(() => {
      assert_equals(animation.playbackRate, -2);
      assert_false(animation.pending);
    });
    
  • reverse method: reverses the direction of a running animation. The change to playback rate does not take effect until acked by the client agent. An animation that is not running is started in the reverse direction.

    const animation = element.animate(keyframes, { duration: 1000 });
    animation.reverse();
    assert_true(animation.pending);
    // Change to playback rate has not yet taken effect.
    assert_equals(animation.playbackRate, 1);
    animation.ready.then(() => {
       // Snap to the end of the animation when playing in the reverse direction.
       assert_times_equal(animation.currentTime, 1000);
       assert_equals(aniamtion.playbackRate(1));
       assert_false(animation.pending);
    });
    
  • persist method: marks an animation as persistent such that it is exempt from being marked and removed as a replaceable animation.

    const animationA = element.animation({ transform: ['scale(1)', 'scale(2)'] },
                                         { duration: 1000, fill: 'forwards' });
    const animationB = element.animation({ transform: ['scale(1)', 'scale(2)'] },
                                         { duration: 1000,
                                           composite: 'accumulate',
                                           fill: 'forwards' });
    animationB.finished.then(() => {
      // The first animation will be automatically be removed since the second
      // animation is higher in the composite order and affects the same
      // property. Removal of the first animation will result in a visual change
      // since using composite mode 'accumulate' for the second animation. We
      // can prevent the removal by persisting the first animation.
      assert_equals(animationA.removeState, 'removed');
      aniamtionA.persist();
      // The first animation is once again active.
      assert_equals(aniamtionA.removeState, 'persist');
    });
    
  • commitStyles method: adds an inline style to the target element based on the current value of the effect stack up to an including the animation.

    const animationA = element.animation({ transform: ['scale(1)', 'scale(2)'] },
                                         { duration: 1000, fill: 'forwards' });
    const animationB = element.animation({ transform: ['scale(1)', 'scale(2)'] },
                                         { duration: 1000,
                                           composite: 'accumulate',
                                           fill: 'forwards' });
    animationA.finished.then(() => {
      // The first animation will be automatically be removed since the second
      // animation is higher in the composite order and affects the same
      // property. Removal of the first animation will result in a visual change
      // since using composite mode 'accumulate' for the second animation.
      // Rather than persisting the first animation, we can instead call
      // commitStyles to capture the current state of the animation effect stack
      // in an inline style.
      animationA.commitStyles();
      // element.style is now set to capture the finished state of the first
      // animation, and it can be safely removed without introducing a visual
      // change.
      animationA.cancel();
    });
    

AnimationEffect

The AnimationEffect interface contains methods for querying and updating the timing of an animation. Refer to the discussion on ‘timing model’ in the section titled ‘web animation model’ for a high level overview of animation timing.

  • getTiming method: returns an EffectTiming object that contains a dictionary of optional specified timing parameters. This object has methods of the form: hasParam(), param(), and setPararm() in C++. In JavaScript, each of the parameters has a getter/setter pair. If the effect is associated with a CSS animation, the style is flushed prior to fetching the timing since timimg parameters may be set via CSS properties. The properties are as follows:

    • delay: The start delay of the animation and equivalent to the CSS animation property animation-delay. An animation with a negative start delay will appear to have started ahead of when it was applied. The delay is expressed in milliseconds.
    • direction: The direction for playing the animation, which may be one of the following: ‘forward’, ‘reverse’, ‘alternate’ or ‘reverse-alternate’. The parameter is equivalent to the CSS animation property animation-direction.
    • duration: The duration for a single iteration of the animation, which may be expressed as a double or a string. If string valued, it contains the time unit as part of the value. Otherwise the value is a time expressed in milliseconds. The corresponding CSS parameter is animation-duration.
    • easing: The default timing function of the animation. This timing function is used for keyframes if not overridden within the keyframe properties. Refer to the sections titled ‘CSS animation’, ‘web animation model’, and ‘keyframeEffect’ for more details on keyframes. This parameter deviates from the normal naming convention when mapping to the CSS property name. The name of the equivalent CSS property is animation-timing-function.
    • endDelay: The end delay of the animation. There is no CSS property counterpart for this parameter. If set, the end delay specifies the number of milliseconds between the active phase and the end time (sounds ominous).
    • fill: The fill mode of an animation, which defines whether the animation persists once finished. The fill mode can be one of the following: ‘none’, ‘forwards’, ‘backwards’, ‘both’, or ‘auto’. An animation with fill ‘forwards’ will persist after finishing if playing in the forward direction. Conversely, an animation with fill ‘backwards’ will persist after finishing if playing in the reverse direction. An animation with fill ‘both’ will persist when playing in either direction. The persistence of a fill mode animation may be overruled if the animation is marked as replaceable as outlined in the section on the Animation API. The corresponding CSS property is animation-fill-mode.
    • iterationStart: If set, this parameter indicates where the animation starts within the iteration cycle. The animation will still complete the number of iterations specified in the ‘iterations’ parameter, but may start and end part way through an iteration cycle. There is no CSS property associated with this parameter. Unlike start delay, this parameter does not affect the runtime of the animation.
    • iterations: Specified the number of iterations to complete in the animation. The corresponding CSS property is animation-iteration-count.
  • getComputedTiming method: returns a ComputedEffectTiming object that contains a dictionary of computed timing parameters. As ComputedEffectTiming extends EffectTiming, this dictionary contains a superset of properties; however, values may differ from EffectTiming due to being evaluated. Default values for missing properties are resolved, the duration is expressed solely in milliseconds, and a fill mode of ‘auto’ is resolved to fill ‘none’. All values needed for the ‘timing model’ portion of the web animation model are included. The follow are the set of additional properties:

    • endTime: In the absence of a start or end delay, the endTime would be the total runtime of the animation (iteration duration * iteration count). A positive value for start or end delay extends the runtime of an animation; however, only the end delay factors into the calculation of endTime. The value for endTime is calculated as the duration of the active phase plus the end delay.
    • activeDuration: The active duration of the animation is simply the product of the iteration duration and iteration count.
    • localTime: The value for localTime is Animation.currentTime if the effect is associated with an animation, otherwise unresolved.
    • progress: Specifies the transformed progress. Steps in calculating the progress of an animation are also outlined in the section titled ‘web animation model’.
    • currentIteration: Indicates the current (zero-based) index of the animation.
  • updateTiming method: used to update one or more specified timing properties. Any property updated in this manner for a CSS animation is marked so that subsequent updates via CSS are ignored. In other words, explicit use of the web-animation API takes precedence over CSS.

    target.style.animation = 'spinner 1s infinite linear';
    const animation = target.getAnimations()[0];
    assert_equals(animation.effect.getTiming().duration, 1000);
    
    // Update via CSS property. Style changed properly flushed when fetching
    // timing properties.
    target.style.animationDuration = '3s';
    assert_equals(animation.effect.getTiming().duration, 3000);
    
    // Update via web animation API.
    animation.effect.updateTiming( {duration: 2000 });
    assert_equals(animation.effect.getTiming().duration, 2000);
    
    // Attempted update via CSS property is now ignored.
    target.style.animationDuration = '3s';
    assert_equals(animation.effect.getTiming().duration, 2000);
    

KeyframeEffect

The KeyframeEffect interface extends the AnimationEffect interface including attributes and methods specific to keyframes. A brief overview of keyframes is presented in the section on animation types and in the example for the web animation model.

  • KeyframeEffect constructor: creates a new KeyframeEffect object. The constructor takes an element, set of keyframes, and an optional set KeyframeEffectOptions or a numeric value (duration). All options available for AnimationEffects may also be used for KeyframeEffects. In addition, KeyframeEffectOptions contains ‘composite’ and ‘pseudoElement’ attributes that are described below.

  • target attribute: gets or sets the target element for the animation effect. If animating a CSS transition or CSS animation, transition/animation events after a target change are directed back to the original target element.

  • pseudoElement attribute: gets or sets the pseudo-element specifier for the target element. For example, an animation triggered by a “div::hover” CSS rule will have the parent element as the target element and “::hover” as the pseudoElement specification. The pseudoElement attribute is empty if not animating a pseudoElement.

  • composite attribute: gets or sets the composite mode for the animation effect. The composite mode is not to be confused with compositing (accelerated rendering off of the main-thread). The composite mode may be one of the following: ‘replace’, ‘add’, ‘accumulate’, or ‘auto’. The default mode is ‘replace’, where the effect replaces any underlying property value. The ‘add’ composite mode combines the effect with the underlying value. For list valued properties such as transforms, ‘add’ appends the effect to the list. For additive properties such as width, the output is the sum of the effect with the underlying value. The ‘accumulate’ option also combines the effect with the underlying value. In the case of a list valued property such transform, the combination is on a per transform basis, combining scales, translations, rotations etc. For additive properties such as translate and rotate, the result is the sum of the effect and underlying value. For multiplicative properties such as scale, the output is the addition of deltas. A scale(2) operation is a 100% size increase over the base. So ‘accumulating’ scale(3) on top of scale(2) = 200% increase + 100% increase = 300% increase = scale(4).

  • getKeyframes method: retrieves the list of kefyrames. Internally, Blink maintains two styles of keyframes: 1) TransitionKeyframes (used solely for CSS transitions), and 2) StringKeyframes. Values stored in transition keyframes are fairly low-level, being tightly coupled with Blinks implementation of the interpolation stack. Conversely, string keyframes can hold any parse-able value, and thus can represent the richness of expressions available to @keyframes rules. Each KeyframeEffect is associated with a KeyframeEffectModel that is templated based on the type of keyframe stored. Keyframes rules for CSS animations cannot be precisely represented in the dictionary form returned by the getkeyframes call. Various substitutes need to be made such as resolving variable references, shorthand property values, and filling in missing keyframe values. These substitutions cannot be made at parse time when creating the animation since they depend on the style cascade. Instead, the resolution is done on demand when getKeyframes is called. CSS animations use a specialized keyframe model (CssKeyframeEffectModel) which overrides getComputedKeyframes to perform the necessary resolution.

  • setKeyframes method: replaces the keyframes associated with the effect. The effect is updated to use a StringKefyrameModel (i.e. KeyframeEffectModel) regardless of the original format for the keyframes. Note that CSS transitions can be updated to animate properties other than the one specified in CssTransition.transitionProperty. In the event of transition retargeting, the transition's current time is used for computing the current progress even if no longer transitioning the property.

AnimationTimeline / DocumentTimeline

The AnimationTimeline interface provides access to the current time and phase of a timeline. A timeline provides a real (DocumentTimeline) or abstract (ScrollTimeline) notion of time and is used to synchronize timing updates to animations.

The DocumentTimeline interface extends the AnimationTimeline interface to add an originTime option for its constructor. The originTime is the time offset in milliseconds relative to the time origin (zero time) and may be used to synchronize animations across multiple document timelines.

In addition to supporting the JavaScript interfaces the AnimationTimeline class has a number of other responsibilities. These include: 1) maintaining a set of attached animations, 2) keeping track of which animations require an update during the next cycle, 3) flagging and removing replaced animations, and 4) retrieving a list of active animations in support of getAnimations calls.

  • currentTime attribute: gets the current time for the timeline or null if unresolved. The current time may be relative to a time origin in the case of a monotonically increasing timeline or proportional to the scroll position in the case of a non-monotonic timeline. The value of currentTime is updated each animation frame. Within the context of script execution, however, the value returned for currentTime must remain fixed. This restriction ensures consistent behavior if multiple calls to fetch the currentTime are performed within a script.

  • phase attribute: gets the phase of a timeline. A timeline that is not associated with the active document is in the ‘inactive’ phase. A DocumentTimeline has an additional constraint that the time origin needs to be initialized for the timeline to be active. A timeline in the ‘inactive’ phase has an unresolved value for currentTime.

TODO: Document ScrollTimeline

Document extension

Each document has an associated DocumentTimeline, which is the default document timeline that will be used if an element is started via element.animate or CSS rules. An alternate timeline may be used by calling the Animation constructor rather than Element.animate to create the animation.

  • timeline: the default document timeline.

DocumentOrShadowRoot extension

Documents and ShadowRoots support an API call to retrieve all animations associated with the document or shadow root.

  • getAnimations: retrieves the list of all active animations associated with the document in composite ordering. See Animatable.getAnimations for a brief overview of composite ordering. DocumentOrShadowRoot::getAnimations calls DocumentAnimations::getAnimations, which walks the timelines associated with the document and extracts the active ones.

The interpolation stack

Animation keyframes serve as mileposts indicating property values at specific points through the progress of the animation. Most commonly, keyframes are provided for the start and end of the animation; however, an arbitrary number of keyframes can be added for intermediate points. In fact, the start and ending points for the animation are not strictly required and if missing neutral keyframe values (based on the underlying property values) are used. Optionally, the keyframe can specify the timing function used to indicate the path for connecting the dots between keyframes. Details for interpolating a scalar valued property are covered in the section titled ‘web animation model’. This interpolation strategy extends to list valued properties. For example:

@keyframes pulse {
  0%   { filter: brightness(100%),
         animation-timing-function: cubic-bezier(0.2, 0.3, 0.7, 1.0) },
  25%  { filter: brightness(150%)
         animation-timing-function: cubic-bezier(0.2, 0.0, 0.8, 1.0) },
  75%  { filter: brightness(50%)
         animation-timing-function: cubic-bezier(0.3, 0.0, 0.8, 0.7) },
  100% { filter: brightness(100%) },
}
button:hover {
  animation: pulse 1s;
}

In this example, the brightness filter for the pulse animation follows roughly a sinusoidal path, approximated by piecewise cubic-bezier curves. Though the filter property supports a list of filter functions, the values are consistent between frames with only the brightness changing. Thus, the value can be interpolated following the same rules as for a scalar, i.e.:

value = (1 - p) A + p B

Each property that can be interpolated has an associated CSSInterpolationType that performs validation and constructs InterpolableValues. For our filter example, the InterpolableFilters are created via CSSFilterListInterpolationType, which requires a pairwise match between filter functions to be valid for continuous interpolation.

Various fallback mechanisms are used for interpolation of list-valued properties that are not pairwise compatible. In some cases, discrete interpolation is used where the output switch from A to B at a progress value of 0.5.

Transform lists are a particularly interesting example. If the lists are pairwise compatible between frames, they can be interpolated much like scalars. For example, transforming between ‘scale(1)’ and ‘scale(2)’. The notion of pairwise compatibility is extended to classes of functions with similar geometric properties. For example, ‘translateX’ and ‘translateY’ are both special cases of ‘translate’ and thus considered pairwise compatible. An interpolation between ‘translateX(100px)’ and ‘translateY(100px)’ is internally converted to an interpolation between ‘translate(100px, 0)’ and ‘translate(0, 100px)’. Now that the same transform function is being used, pairwise interpolation rules apply.

In the general case, incompatible transform list fall back to matrix interpolation. Any series of transform operations can be expressed as a 4x4 transformation matrix. Unfortunately, information is lost in the process as a no-op transform is indistinguishable from a 360 degree rotation once expressed in matrix form. For this reason, we use matrix interpolation as a last resort.

Interpolation of matrices is not as simple as interpolating the matrix elements. Instead, each matrix is decomposed into a set of transformations that produces the same matrix representation. Algorithms for decomposing and interpolating 2-D and 3-D matrices are covered in the spec. The 2-D algorithm is covered in css-transforms-1 - interpolations of matrices. The 3-D algorithm is covered in css-transforms-2 - interpolations of 3d matrices. Note that running the 3D decomposition algorithm on a 2D transformation is not guaranteed to provide a consistent decomposition as applying the 2D algorithm. The decomposition process in Blink uses the 2D algorithm if both matrices are 2D and the 3D algorithm otherwise.

To minimize the use of the matrix fallback, transform lists can be extended with neutral transform operations for the purpose of pairwise matching and the matrix fallback only applies to the residuals after pairwise matching. For example, interpolation between ‘translateX(100px) scale(2)’ and ‘translateY(100px)’ is treated as ‘translate(100px, 0) scale(2)’ to ‘translate((0, 100px) scale(1)’ establishing pairwise compatibility. An interpolation between ‘scale(1) translateX(100px)’ and ‘scale(2) rotate(90deg)’ is only partially pairwise compatible. The scale is interpolated pairwise, but the translate to rotate interpolation is handled via matrix decomposition. Rules for interpolations of transform lists are covered in css-transforms-1 - interpolations of transforms.

Multiple animations can be applied to a single element. There are strict rules for how to combine these effects based on composite ordering as well as composite mode. CSS transitions are applied before CSS animations, which in turn are applied before animations created via element.animate. Within each class of animation, there are also ordering rules that depend on the type. For example,

CSS transitions are ordered by ‘transition generation’, an index that increases with each style change. Within a single transition generation, transitions are sorted by property name.

div.style.top = '0px';
div.style.left = '0px';
div.style.transition = 'all 100ms';

div.style.top = '100px';
div.style.left = '100px';

// Styles are flushed when retrieving the list of animations. The left and
// top transitions are created within the same style update and thus sorted
// alphabetically.
const transitions = div.getAnimations();
assert_equals(transitions[0].transitionProperty, 'left');
assert_equals(transitions[1].transitionProperty, 'top');

div.style.opacity = 0.5;

// The opacity transition is at the end since created in a separate style
// update cycle.
const updated_transitions = div.getAnimations();
assert_equals(transitions[0].transitionProperty, 'left');
assert_equals(transitions[1].transitionProperty, 'top');
assert_equals(transitions[1].transitionProperty, 'opacity');

CSS animations that are applied to a single element are ordered by index within the animation-name property. Pseudo-element selectors have a strict ordering by selector name. CSS animations applying to different elements are sorted in DOM order. Note that the DOM order sort is only applied for getAnimations calls as it is too expensive too apply in general and does not affect rendering if internally sorted for style calculations in creation order instead.

  @keyframes fade { ... }
  @keyframes shrink { ... }
  @keyframes highlight { ... }
  button.dismiss { animation: shrink 600ms linear, fade 600ms ease-out; }
  button:hover::before { animation: highlight 300ms forwards; }
  button.onclick = (evt) => {
    const target = evt.target;
    target.classList.add('dismiss');
    const animations = document.getAnimations();
    // The fade and shrink animations are sorted by the ordering in which
    // they appear in the animation property.
    assert_equals(animations[0].animationName, 'shrink');
    assert_equals(animations[1].animationName, 'fade');
    // The pseudo-element animation appears after its parent.
    assert_equals(animations[1].animationName, 'highlight');
  };

It is also possible for a CSS transition or animation to get treated as a generic animation if it becomes disassociated from its owning element. The above example demonstrate that the getAnimations calls retrieve CSS animations and transitions as well as those generated via Element.animate. A finished CSS transition or animation is no longer associated with CSS rules that triggered the animation. If we retain a reference to one of these animations and replay it, it is treated like a generic animation, which in turn affects sort ordering.

Within the Blink implementation, EffectStack is used to store sampled effects and sort them for style update. These effects are collected as follows:

The method EffectStack::ActiveInterpolations assembles a set of interpolations for an element applying the effects in the correct order. This method is called separately for CSS transitions and animations since transitions are to be applied before animations. Effects are grouped by property, and since multiple effects can target the same property, the effects need to be combined in some fashion. The composite mode property determines how effects are combined. The default mode is ‘replace’, whereby a new effect for a property flushes all previous effects for the property. The other modes are ‘add’ and ‘accumulate’. If either of these options is used, both the old and new effect are included in the set.

Integration with Chromium

The Blink animation engine interacts with Blink/Chrome in the following ways:

  • Chromium's Compositor

    Chromium‘s compositor has a separate, more lightweight animation engine that runs separate to the main thread. Blink’s animation engine delegates animations to the compositor where possible for better performance and power utilisation.

    Compositable animations

    A subset of style properties (currently transform, opacity, filter, and backdrop-filter) can be mutated on the compositor thread. Animations that mutate only these properties are candidates for being accelerated and run on the compositor thread which ensures they are isolated from Blink's main thread work.

    Whether or not an animation can be accelerated is determined by CheckCanStartAnimationOnCompositor() which looks at several aspects such as the composite mode, other animations affecting same property, and whether the target element can be promoted and mutated in compositor. Reasons for not compositing animations are captured in FailureCodes.

    Lifetime of a compositor animation

    Animations that can be accelerated get added to the PendingAnimations list. The pending list is updated as part of document lifecycle and ensures each pending animation gets a corresponding cc::AnimationPlayer representing the animation on the compositor. The player is initialized with appropriate timing values and corresponding effects.

    Note that changing that animation playback rate, start time, or effect, simply adds the animation back on to the pending list and causes the compositor animation to be cancelled and a new one to be started. See Animation::PreCommit() for more details.

    An accelerated animation is still running on main thread ensuring that its effective output is reflected in the Element style. So while the compositor animation updates the visuals the main thread animation updates the computed style. There is a special case logic to ensure updates from such accelerated animations do not cause spurious commits from main to compositor (See CompositedLayerMapping::UpdateGraphicsLayerGeometry(), or FragmentPaintPropertyTreeBuilder::UpdateTransform(), FragmentPaintPropertyTreeBuilder::UpdateEffect(), and FragmentPaintPropertyTreeBuilder::UpdateFilter() for BlinkGenPropertyTrees mode)

    A compositor animation provides updates on its playback state changes (e.g., on start, finish, abort) to its blink counterpart via CompositorAnimationDelegate interface. Blink animation uses the start event callback to obtain an accurate start time for the animation which is important to ensure its output accurately reflects the compositor animation output.

  • Javascript

    EffectInput contains the helper functions that are used to process a keyframe argument which can take an argument of either object or array form.

  • DevTools

    The animations timeline uses InspectorAnimationAgent to track all active animations. This class has interfaces for pausing, adjusting DocumentTimeline playback rate, and seeking animations.

    InspectorAnimationAgent clones the inspected animation in order to avoid firing animation events, and suppresses the effects of the original animation. From this point on, modifications can be made to the cloned animation without having any effect on the underlying animation or its listeners.

  • SVG

    The element.animate() API supports targeting SVG attributes in its keyframes. This is an experimental implementation guarded by the WebAnimationsSVG flag and not exposed on the web.

    This feature should provide a high fidelity alternative to our SMIL implementation.

Architecture

Animation Timing Model

The animation engine is built around the timing model described in the Web Animations spec.

This describes a hierarchy of entities:

  • DocumentTimeline: Represents the wall clock time.
    • Animation: Represents an individual animation and when it started playing.
      • AnimationEffect: Represents the effect an animation has during the animation (e.g. updating an element's color property).

Time trickles down from the DocumentTimeline and is transformed at each stage to produce some progress fraction that can be used to apply the effects of the animations.

For example:

// Page was loaded at 2:00:00PM, the time is currently 2:00:10PM.
// document.timeline.currentTime is currently 10000 (10 seconds).

let animation = element.animate([
    {transform: 'none'},
    {transform: 'rotate(200deg)'},
  ], {
    duration: 20000,  // 20 seconds
  });

animation.startTime = 6000;  // 6 seconds
  • DocumentTimeline notifies that the time is 10 seconds.
    • Animation computes that its currentTime is 4 seconds due to its startTime being at 6 seconds.
      • AnimationEffect has a duration of 20 seconds and computes that it has a progress of 20% from the parent animation being 4 seconds into the animation.

        The effect is animating an element from transform: none to transform: rotate(200deg) so it computes the current effect to be transfrom: rotate(40deg).

Lifecycle of an Animation

Lifecycle

  1. An Animation is created via CSS1 or element.animate().
  2. At the start of the next frame the Animation and its AnimationEffect are updated with the currentTime of the DocumentTimeline.
  3. The AnimationEffect gets sampled with its computed localTime, pushes a SampledEffect into its target element's EffectStack and marks the elements style as dirty to ensure it gets updated later in the document lifecycle.
  4. During the next style resolve on the target element all the SampledEffects in its EffectStack are incorporated into building the element's ComputedStyle.

One key takeaway here is to note that timing updates are done in a separate phase to effect application. Effect application must occur during style resolution which is a highly complex process with a well defined place in the document lifecycle. Updates to animation timing will request style updates rather than invoke them directly.

1 CSS animations and transitions are actually created/destroyed during style resolve (step 4). There is special logic for forcing these animations to have their timing updated and their effects included in the same style resolve. An unfortunate side effect of this is that style resolution can cause style to get dirtied, this is currently a code health bug.

KeyframeEffect

Currently all animations use KeyframeEffect for their AnimationEffect. The generic AnimationEffect from which it inherits is an extention point in Web Animations where other kinds of animation effects can be defined later by other specs (for example Javascript callback based effects).

Structure of a KeyframeEffect

  • KeyframeEffect represents the effect an animation has (without any details of when it started or whether it's playing) and is comprised of three things:
    • Some Timing information (inherited from AnimationEffect). Example:

      {
        duration: 4000,
        easing: 'ease-in-out',
        iterations: 8,
        direction: 'alternate',
      }
      

      This is used to compute the percentage progress of the effect given the duration of time that the animation has been playing for.

    • The DOM Element that is being animated.

    • A KeyframeEffectModel that holds a sequence of keyframes to specify the properties being animated and what values they pass through. Example:

      [
        {backgroundColor: 'red', transform: 'rotate(0deg)'},
        {backgroundColor: 'yellow'},
        {backgroundColor: 'lime'},
        {backgroundColor: 'blue'},
        {backgroundColor: 'red', transform: 'rotate(360deg)'},
      ]
      

      These keyframes are used to compute:

      • A PropertySpecificKeyframe map that simply breaks up the input multi-property keyframes into per-property keyframe lists.
      • An InterpolationEffect which holds a set of Interpolations, each one representing the animated values between adjacent pairs of PropertySpecificKeyframes, and where in the percentage progress they are active. In the example keyframes above the [Interpolations][] generated would include, among the 5 different property specific keyframe pairs, one for backgroundColor: 'red' to backgroundColor: 'yellow' that applied from 0% to 25% and one for transform: 'rotate(0deg)' to transform: 'rotate(360deg)' that applied from 0% to 100%.

Lifecycle of an Interpolation

Interpolation is the data structure that style resolution uses to resolve what animated value to apply to an animated element's ComputedStyle.

  1. Interpolations are lazily instantiated prior to sampling.
  2. KeyframeEffectModels are sampled every frame (or as necessary) for a stack of Interpolations to apply to the associated Element and stashed away in the Element‘s ElementAnimationsEffectStack's SampledEffects.
  3. During style resolution on the target Element all the Interpolations are collected and organised by category according to whether it's a transition or not (transitions in Blink are suppressed in the presence of non-transition animations on the same property) and whether it affects custom properties or not (animated custom properties are animation-tainted and affect the processing of animation properties.
  4. TODO(alancutter): Describe what happens in processing a stack of interpolations.

Testing pointers

Test new animation features using end to end web-platform-tests to ensure cross-browser interoperability. Use unit testing when access to chrome internals is required. Test chrome specific features such as compositing of animation using web tests or unit tests.

End to end testing

Features in the Web Animations spec are tested in web-animations. Writing web platform tests has pointers for how to get started. If Chrome does not correctly implement the spec, add a corresponding -expected.txt file with your test listing the expected failure in Chrome.

Web tests are located in third_party/blink/web_tests. These should be written when needing end to end testing but either when testing chrome specific features (i.e. non-standardized) such as compositing or when the test requires access to chrome internal features not easily tested by web-platform-tests.

Unit testing

Unit testing of animations can range from extending Test when you will manually construct an instance of your object to extending RenderingTest where you can load HTML, enable compositing if necessary, and run assertions about the state.

Ongoing work

Properties And Values API

TODO: Summarize properties and values API.

Animation Worklet

AnimationWorklet is a new primitive for creating high performance procedural animations on the web. It is being incubated as part of the CSS Houdini task force, and if successful will be transferred to that task force for full standardization.

A WorkletAnimation behaves and exposes the same animation interface as other web animation but it allows the animation itself to be highly customized in Javascript by providing an animate callback. These animations run inside an isolated worklet global scope.