A. M. Douglas

Meet the new Web Animations API

While browser support for the Web Animations API (WAAPI) may look rather bleak at the moment, when the other browsers catch up, it will be the sharpest and fastest way to animate anything without resorting to WebGL, since it enables us to programmatically create CSS3 animations, which are subject to a very healthy helping of browser optimisation.

But WAAPI is much, much more than that. It's not just an opportunity to throw out all your animation libraries like GSAP, Velocity.js, jQuery, etc. CSS3 Animations already gave you that opportunity for basic things like fading content in and out. WAAPI enables you to play, pause, rewind and cancel animations as though they were videos. You can even alter the speed at which the animation is playing. The potential is absolutely enormous.

So let's get our teeth into the thing.

Recap: CSS Animations

I think it's best to approach WAAPI with at least a little familiarity with CSS animations. The syntax looks something like this:

  
@keyframes my-animation {
  from, 0% {
    opacity: 0; visibility: hidden;
  }
  to, 100% {
    opacity: 1; visibility: visible;
  }
}
.my-element {
             animation-name: my-animation;
  animation-timing-function: cubic-bezier(0.4,0,0.2,1);
         animation-duration: 1s;
  animation-iteration-count: 1;
        animation-fill-mode: forwards;
        animation-direction: normal;
            animation-delay: 2s;
       animation-play-state: initial;
}

So, let's look at this bit by bit. The @keyframes statement is the definition of your animation. You use this to say precisely what will happen across the time specified in your element's animation-duration property. When you have two states, you can use from and to, but when you have multiple keyframes you need to specify a percentage value, representing at what percentage of the total duration that keyframe should fire.

The rest is pretty self-explanatory. The animation-name is required and must match a set of keyframes you specified. The animation-timing-function works exactly as it does for CSS transitions, and includes the usual predefined defaults like linear, ease, and so on. You can also define your own bezier curves, which I prefer to do because I like the 'fast in, slow out' curve I've given as an example above.

animation-play-state is a new property. CSS Animations have been available for developers to use for a long time now, with even browsers as old as Safari 5.1 supporting a prefixed version of the above syntax. animation-play-state is something new, which seems to have arrived in Chrome since the last beta update (I just updated to Chrome 54 and I have only just seen this property become available). This is of course landing as a result of the Web Animation API's play/pause functionality being realised, because you might as well provide access to that functionality through CSS so that animations can be paused and played using :hover or input:checked + .animated-element and so on.

animation-iteration-count is important: you can create an everlasting animation by setting the iterations to be infinite, and we'll be using that later when we build a demo of WAAPI.

So, with the code above, you could quite easily make this:

Blink

Lovely. But what if you wanted to make something like a carousel, or a scrolling marquee style product showcase? Now that CSS has animation-play-state, it's quite easy to do with even the CSS syntax, but you'll need to know:

  • how many elements will be in the carousel
  • how big they are going to be, expressed as percentages or precise pixel counts

and you'll also need all of the elements to be the same width. It seems like quite a lot, but actually it's fairly reasonable to expect that your products will be displayed as squares of equal width, and that your back-end is setup to display, say, 12 products with a 'view more' button nearby.

If, however, you really did get such a situation as my imaginary scrolling marquee/carousel featuring, say, photographs of different widths, how would you use animations then?

Enter WAAPI

Let's imagine our gallery. Our end result will look like this:

1
2
3
4
5
6
7
8

I've used CSS to create that marquee, which is easy enough with a known number of equal-width elements. 8 elements, 25% each, so that's a translation in X of -100%. Let's have a look at how to achieve this for an unknown number of random-width elements:

The CSS for that animated marquee looks like this:

  
@keyframes marquee1 {
  from, 0% {
    transform: translate3d(0,0,0);
  }
  to, 100% {
    transform: translate3d(-100%,0,0);
  }
}
.marquee {
  display: block;
  white-space: nowrap;
  padding-top: 16px;
  font-size: 0;
  animation-name: marquee1;
  animation-timing-function: linear;
  animation-duration: 8s;
  animation-iteration-count: infinite;
  animation-fill-mode: forwards;
  animation-direction: alternate;
}
.marquee:hover{
  animation-play-state:paused;
}
.marquee .cell {
  position: relative;
  display: inline-block;
  font-size: 16px;
  width: 25%;
  padding: 8px;
  height: 100px;
}
.marquee .cell .content {
  position: absolute;
  top: 8px;
  left: 8px;
  bottom: 8px;
  right: 8px;
  padding: 8px;
  color: #fff;
  background: crimson;
}

The code we'll use for our JavaScript WAAPI example will look like this:

The HTML:


<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
  </head>
  <body>
    <div class="grid">
      <div class="cell w33">
        <div class="content">
          1
        </div>
      </div>
      <div class="cell w66">
        <div class="content">
          2
        </div>
      </div>
      <div class="cell w100">
        <div class="content">
          3
        </div>
      </div>
      <div class="cell w83">
        <div class="content">
          4
        </div>
      </div>
      <div class="cell w16">
        <div class="content">
          5
        </div>
      </div>
      <div class="cell w25">
        <div class="content">
          6
        </div>
      </div>
      <div class="cell w25">
        <div class="content">
          7
        </div>
      </div>
      <div class="cell w50">
        <div class="content">
          8
        </div>
      </div>
      <div class="cell w100">
        <div class="content">
          9
        </div>
      </div>
      <div class="cell w75">
        <div class="content">
          10
        </div>
      </div>
      <div class="cell w25">
        <div class="content">
          Finish
        </div>
      </div>
    </div>
  </body>
</html>

The CSS:


* { box-sizing: border-box }

body  { margin: 0; overflow: hidden; background: #08f }
.grid { font-size: 0 }
.cell {
  display: inline-block;
  font-size: 2rem;
  font-weight: 700;
  position: relative;
  width: 100%;
  height: 30vh;
  padding: 1rem .5rem;
  text-align: center;
  cursor: pointer;
}
.cell .content {
  display: block;
  position: absolute;
  top: .5rem;
  bottom: .5rem;
  left: .5rem;
  right: .5rem;
  padding: 1rem;
  color: #555;
  background: #fff;
}

.cell.w16  { width: calc(100% / 6) }
.cell.w25  { width: 25% }
.cell.w33  { width: calc(100% / 3) }
.cell.w50  { width: 50% }
.cell.w66  { width: calc(100% / (3 / 2)) }
.cell.w75  { width: 75% }
.cell.w83  { width: calc(100% / (6 / 5)) }
.cell.w100 { width: 100% }

So that's a grid and 10 grid cells, each serving as containers for an image or something like that. I'm just using numbers for now. To turn this into something which we can scroll horizontally, we need to first set the .grid element's white-space property to nowrap. We'll do this using JavaScript, so that the items appear as a normal grid if JavaScript is turned off or if WAAPI is not supported.

We need to find our grid element and its children, get the combined width of the children, subtract from that the width of the containing element, and then programmatically animate the grid to translate itself by the number of pixels we calculated earlier, before returning to its original position.

If that sounds complicated, it's not about to get any simpler:


// First, create a variable to be a reference to our animation later, 
// so by using a closure, we can start, stop and cancel the animation.
var marquee;

// Then let's grab the element we're going to move around
var marquee_el = document.querySelector( '.grid.marquee' );

// and its children, so we know how much we have to move it around by
var children = marquee_el.querySelectorAll( '.cell');

function createMarquee(){

  /*
  We're going to recreate the marquee animation 
  when the viewport is resized, so get rid of 
  any existing animation first
  */
  if( typeof marquee !== 'undefined' ) marquee.cancel();

  // We set this dynamically, so the thing will 
  // gracefully degrade to a typical grid of items
  marquee_el.style.whiteSpace = 'nowrap';

  // Create a variable for the distance by which
  // the grid element will be transformed
  var displacement = 0;

  // Add up the width of all the elements in the marquee
  for ( var j = 0; j < children.length; ++j ) {
    displacement += children[j].clientWidth;
  }
  
  /*
  Crucial: subtract the width of the container;
  Optional: take the opportunity to round the displacement 
  value down to the nearest pixel. The browser may thank
  you for this by not blurring the shit out of your text.
  */
  displacement = (displacement - marquee_el.clientWidth) << 0;

  /* 
  Now for the juicy part. The WAAPI.
  By using the variable 'marquee' we created in the parent scope,
  we can easily use the reference to pause/cancel the animation later
  */
  marquee = marquee_el.animate([
    /*
    element.animate() accepts two arguments: an array of keyframes 
    and a sort of configuration object, much like the CSS syntax.
      
    First are your keyframes: so your 'from' or '0%' keyframe
    translates to 'offset: 0', '100%' translates to 'offset: 1', 
    and anything in betwen like '54%' will be 'offset: .54'. One
    object per keyframe. The ability to define a dynamic 
    translateX value already gives you an idea of why WAAPI is useful:
    */
    {transform:'matrix(1,0.00,0.00,1, 0, 0)', offset: 0},
    {transform:'matrix(1,0.00,0.00,1,'+ -displacement +', 0)', offset: 1}
    // you don't have to use matrix, I just like it.
  ],
  {
    // 1 second for each element in marquee
    // Entirely arbitrary decision
    duration: children.length * 1e3,

    /*
    Can be 'ease', 'cubic-bezier(.4,0,.2,1)', etc. 
    can also be a stepping function, like 'steps(4)', 'steps(10, end)'
    see 'MDN: Using the Web Animations API' (https://goo.gl/PtVEkQ)
    */
    easing: 'linear',

    // Useful if you don't want the animation to start until your content
    // has loaded from, say, a REST API and you want to speculate a 
    // reasonable time for that to take
    delay: 0,

    // Kind of crucial for what we want to make...
    // NB: 'Infinity' not 'infinite'
    iterations: Infinity,

    // Invert animation after completion, so it scrolls backwards */
    direction: 'alternate',

    /* 
    You would use this if your animation is set to occur only a 
    finite number of times, and you wanted the animated element to finish at 
    the end keyframe, rather than the first keyframe
    */
    fill: 'forwards'
  });
}

This might look hideously complicated, but this kind of brevity to unlock the kind of performance and fluidity of animation is unprecedented. With jQuery and $(element).animate(), you'd have plenty of framedropping and jankiness to enjoy. With GSAP/Velocity.js, the story is a good deal better, but they both still represent an additional JavaScript library and HTTP request at the end of the day.

This is just a function.

Now, maybe you don't think it's particularly brief/terse. But if I just remove my comments...


function createMarquee(){
  if (typeof marquee !== 'undefined') marquee.cancel();
  
  var displacement = 0;
  
  marquee_el.style.whiteSpace = 'nowrap';
  
  for (var j = 0; j < children.length; ++j) displacement += children[j].clientWidth;
  
  displacement = (displacement-marquee_el.clientWidth) << 0;
  
  marquee = marquee_el.animate([
    { transform: 'matrix(1,0.00,0.00,1, 0, 0)', offset: 0 },
    { transform: 'matrix(1,0.00,0.00,1,'+-displacement+',0)', offset: 1 }
  ],{
    duration: children.length * 1e3,
    easing: 'linear',
    delay: 0,
    iterations: Infinity,
    direction: 'alternate',
    fill: 'forwards'
  });
}

Yeah, pretty brief. Of course, you still need to check to see if your user's browser supports WAAPI and then you ought to plan for things like a browser viewport resize, and add some event handlers to take care of the user hovering on one of the scrolling items for a closer look:


// quick check for the WAAPI method
if ('animate' in marquee_el && typeof marquee_el.animate === 'function') {

  // okay, let's fire up the marquee!
  createMarquee();

  // now for the playing/pausing
  marquee_el.addEventListener('mouseenter', pauseMarquee, false);
  marquee_el.addEventListener('mouseleave', playMarquee, false);

  // and resizing
  window.addEventListener('resize', debounce( createMarquee ), false);

} else {
  /*
  Let's say hello to those using Safari
  or indeed users of IE, not-recently-updated FF, old Chrome, old Opera, etc.
  They will see a standard grid of items
  */
  console.warn('Your browser does not support the Web Animation API');
}

// pretty self-explanatory
// though this is why we needed that closure I mentioned
function playMarquee(){
  if ( marquee.playState && marquee.playState === 'paused' ) 
    marquee.play();
}

// again, pretty self-explanatory
function pauseMarquee(){
  if ( marquee.playState && marquee.playState === 'running' )
    marquee.pause();
}

// a debouncing function using requestAnimationFrame
// this is just an easy-to-use wrapper I like to use for event handlers
function debounce(func){
  var scheduled, context, args;
  return function(){
    context = this; 
    args = [];
    for (var i = 0; i < arguments.length; ++i){
      args[i] = arguments[i];
    }
    !!scheduled && window.cancelAnimationFrame(scheduled);
    scheduled = window.requestAnimationFrame(function(){
      func.apply(context, args);
      scheduled = null;
    });
  }
}

(Open in Codepen) · (View live demo)

This still might not seem that great: now that browsers will support the animation-play-state as a CSS property, open to manipulation from hover effects and so on, the value of the WAAPI may be constrained to edge cases. But now at least, when there is an edge case, there is a simple and clean way to approach it, without libraries or horribly inaccurate setTimeouts/Intervals.

Labels: ,

1 Comments:
At 8:51 pm, May 19, 2017 , Blogger lois atwood said...

good article, but it doesn't seem as powerful as velocity.js

how well does it perform compared to velocity?

 

Post a Comment