Animations on the web

A long time ago...

  • CSS values could be changed, but not smoothly
.button { background-color: darkblue; }
.button:hover { background-color: deepskyblue; }
Button

Animations on the web

CSS Transitions

  • Simple way to change property values smoothly
.button { background-color: darkblue; transition: background-color 1s; }
.button:hover { background-color: deepskyblue; }
Button

Animations on the web

CSS Animations

  • A more powerful way to specify transitions between values, using keyframes
.button { background-color: darkblue; }
.button:hover { animation: pulsate 1s infinite alternate; }
@keyframes pulsate {
  0% { background-color: darkblue; }
  100% { background-color: deepskyblue; }
}
Button

Web Animations

  • A powerful JavaScript API for coordinating animations
  • A base upon which CSS and SVG animations can be implemented
element.animate([
  {backgroundColor: 'darkblue'},
  {backgroundColor: 'deepskyblue'},
  {backgroundColor: 'darkblue'}
], {duration: 1000});
Button

Additive animations

A motivating example

<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '100px'},
  {left: '500px'},
  {left: '100px'}
], 1000);
</script>

Additive animations

A motivating example

<style> #box { left: 200px; } </style>
<script>
box.animate([
  {left: '200px'},
  {left: '600px'},
  {left: '200px'}
], 1000);
</script>

Additive animations

A motivating example

<style> #box { left: 200px; } </style>
<script>
box.animate([
  {left: '0px', composite: 'add'},
  {left: '400px', composite: 'add'},
  {left: '0px', composite: 'add'}
], 1000);
</script>

Additive animations

A motivating example

<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '0px', composite: 'add'},
  {left: '400px', composite: 'add'},
  {left: '0px', composite: 'add'}
], 1000);
</script>

Additive animations

Responding to changes

<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '-50px', composite: 'add'},
  {left: '50px', composite: 'add'}
], {duration: 500, iterations: Infinity, direction: 'alternate',
    easing: 'ease-in-out'});
</script>

Additive animations

Mixing add and replace keyframes

<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '0px', composite: 'add'},
  {left: '800px', composite: 'replace'},
  {left: '0px', composite: 'add'}
], 1000);
</script>

Additive animations

Stacking animations

<style> #box { left: 0px; } </style>
<script>
box.animate([
  {left: '0px', composite: 'replace'},
  {left: '800px', composite: 'replace'},
], 5000);
</script>

Additive animations

Stacking animations

<style> #box { left: 0px; } </style>
<script>
box.animate([
  {left: '-50px', composite: 'add'},
  {left: '50px', composite: 'add'},
], {duration: 500, iterations: 10, direction: 'alternate',
    easing: 'ease-in-out'});
</script>

Additive animations

Stacking animations

+
=

Additive animations

Demo

<style> #box { left: 400px; } </style>
<script>
box.animate([
  {left: '-200px', composite: 'add'},
  {left: '200px', composite: 'add'},
], {duration: 3000, iterations: Infinity, direction: 'alternate',
    easing: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'});
</script>

Additive animations

Demo

<style> #box { left: 400px; top: -200px; } </style>
<script>
box.animate([
  {left: '-200px', composite: 'add'},
  {left: '200px', composite: 'add'},
], {duration: 3000, iterations: Infinity, direction: 'alternate',
    easing: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'});
box.animate([
  {top: '-200px', composite: 'add'},
  {top: '200px', composite: 'add'},
], {duration: 3000, iterations: Infinity, direction: 'alternate', delay: -1500,
    easing: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'});
</script>

Additive animations

Demo

<style> #box { left: 400px; top: -200px; } </style>
<script>
...
box.animate([
  {left: '-50px', composite: 'add'},
  {left: '50px', composite: 'add'},
], {duration: 200, iterations: Infinity, direction: 'alternate',
    easing: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'});
box.animate([
  {top: '-50px', composite: 'add'},
  {top: '50px', composite: 'add'},
], {duration: 200, iterations: Infinity, direction: 'alternate', delay: -100,
    easing: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'});
</script>

Additive animations

Existing Web Animations implementation

// Note that composite: 'replace' is implicit
box.animate([{left: '100px'}, {left: '200px'}], 1000);
  • Each property in each pair of keyframes is represented by an Interpolation
  • Interpolations go through 3 stages:
    • Creation
    • Sampling
    • Application

Existing Web Animations implementation

Creation

box.animate([{left: '100px'}, {left: '200px'}], 1000);
box.animate([{left: '0px'}, {left: '300px'}], 1000);
  • An Interpolation is generated for each property in each pair of keyframes
    • The subclass of Interpolation that's used is dependent on the types of the values (here, it's LengthStyleInterpolation)
    • The from and to CSSValues are converted into InterpolableValues, a minimal representation that we can easily perform calculations on
    • Can be reused every time the animation is rendered

Existing Web Animations implementation

Sampling

  • A mapping from each property to a single Interpolation (the most recent one) is created
  • The timing fraction is calculated based on the current time
  • The result of interpolation is calculated and stored as an InterpolableValue

Existing Web Animations implementation

Application

  • The result of interpolation is converted from an InterpolableValue to a CSSValue and applied to the RenderStyle, so the element will be rendered with the new property value

Additive animations implementation

  • Things that need to change:
    • Since the effects of multiple animations will be taken into account, we should no longer discard all but the most recent Interpolation
    • InterpolableValues will need to be added together
    • The underlying value will need to be factored in so it can be added to

Additive animations implementation

Creation

<style> #box { left: '100px'; } </style>
<script>
box.animate([{left: '100px', composite: 'add'}, {left: '200px', composite: 'add'}], 1000);
box.animate([{left: '0px', composite: 'add'}, {left: '300px', composite: 'add'}], 1000);
</script>
  • Interpolations are still generated for each property in each pair of keyframes
    • The from and to composite modes are stored in the Interpolation

Additive animations implementation

Sampling

  • A mapping from each property to a list of InterpolationPipelineStages is created
    • Linked list nodes, each containing an Interpolation
    • This forms the basis of a pipeline, preserving the order in which the Interpolations will be evaluated and their values accumulated
  • An 'underlying fraction' is calculated: how much the previous value will contribute to the pipeline stage's result

Additive animations implementation

Application

  • If the first interpolation is additive, the underlying value must be added to the pipeline
    • The underlying value is pulled from the RenderStyle (as a CSSValue), and a constant Interpolation is created from it
    • Since an Interpolation is created from the underlying value, we get the benefits of its representation as an InterpolableValue, such as easy addition

Additive animations implementation

Application

  • InterpolableValues are passed from each pipeline stage to the next, and combined according to their underlying fraction

Additive pipeline examples

<style> #box { left: 100px; } </style>
<script>
box.animate([{left: '0px'}, {left: '800px'}], {duration: 5000, iterations: Infinity});
box.animate([
  {left: '-50px', composite: 'add'},
  {left: '50px', composite: 'add'},
], {duration: 500, direction: 'alternate',
    easing: 'ease-in-out', iterations: Infinity});
</script>

Additive pipeline examples

After 2500ms:

Additive pipeline examples

<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '0px', composite: 'add'},
  {left: '800px', composite: 'replace'}
], 1000);
</script>

Additive pipeline examples

After 250ms:

Challenges

  • Incompatible values
  • Cubic Bezier easing
  • inherit

Incompatible values

  • Some properties accept several different types of values, and we can't interpolate between them (nor can we add them)
    • For example, line-height accepts both lengths and numbers
<style> #text { line-height: 2; } </style>
<script>
text.animate([
  {lineHeight: '30px', composite: 'add'},
  {lineHeight: '40px', composite: 'add'}
], 1000);
</script>

Incompatible values

  • How should we handle such a case?
<style> #text { line-height: 2; } </style>
<script>
text.animate([
  {lineHeight: '30px', composite: 'add'},
  {lineHeight: '40px', composite: 'add'}
], 1000);
</script>

Incompatible values

  • We just ignore attempts to add on incompatible values
  • This behaviour was decided on through discussions with the spec authors, but isn't yet reflected in the spec

Cubic Bezier easing

  • Cubic Beziers give you a fine level of control over how values are interpolated between
<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '100px'},
  {left: '800px'}
], {duration: 1000, easing: 'cubic-bezier(.42, 0, .58, 1)'});
</script>

Cubic Bezier easing

  • Cubic Beziers can have 0-2 turning points
<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '100px'},
  {left: '800px'}
], {duration: 1000,
    easing: 'cubic-bezier(.41, 1.94, .72, -0.91)'});
</script>

Interpolating between incompatible values

  • The defined behaviour is using the first value when t < 0.5, and the second value when t ≥ 0.5
<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '100px', fontFamily: 'Verdana'},
  {left: '800px', fontFamily: 'Courier New'}
], 1000);
</script>
hi

Cubic Beziers and incompatible values

  • The first value is used when f(t) < 0.5, and the second value when f(t) ≥ 0.5
  • We ideally want to determine the corresponding values of t at creation time
<style> #box { left: 100px; } </style>
<script>
box.animate([
  {left: '100px', fontFamily: 'Verdana'},
  {left: '800px', fontFamily: 'Courier New'}
], {duration: 1000,
    easing: 'cubic-bezier(.41, 1.94, .72, -0.91)'});
</script>
hi

Cubic Beziers and incompatible values

  • The solution to this problem involved 'partitioning' the Bezier into regions where f(t) < 0.5 and f(t) ≥ 0.5
  • At create time, a constant Interpolation is created for each region
  • When sampling the animation, the appropriate constant Interpolation is chosen and added to the pipeline, enabling addition

Cubic Beziers and incompatible values

Example

<style> #element { width: 100px; } </style>
<script>
element.animate([
  {width: 'auto', composite: 'add'},
  {width: '50px', composite: 'add'}
], {duration: 1000,
    easing: 'cubic-bezier(.41, 1.94, .72, -0.91)'});
</script>

inherit

<style>
  #parent { line-height: 2; }
  #child { line-height: inherit; }
</style>
<div id="parent">
  <div id="child">
    Hello, World!
  </div>
</div>
  • The inherit keyword resolves to the element's parent's value
  • The value (and type) of inherit is only known at apply time
  • The InterpolableValue code path doesn't support inherit yet, so adding with inherit isn't yet possible
    • The old code path supports inherit by deferring the creation of 'interpolations' using the keyword until apply time

Status of additive animations

  • CLs landed
    • Support for adding and multiplying InterpolableValues
    • Refactoring of Interpolation creation for partitioning
    • Implementation of cubic Bezier and step timing function partitioning (behind flag)
    • Removal of partitioning flag
  • CLs up for review
    • Additive animations for lengths
  • What's next
    • Implement additive animations for other simple types
    • Benchmarking
    • Ship additive animations support
    • Implement composited additive animations?

Thanks!