Part II: Running Multiple CSS3 and HTML5 Animations (Windows Store, IE)

In part I of my exploration of CSS3 for building a casual game, I talked about dynamically
setting the @keyframes rule
using JavaScript. In this, more introductory, post, I'll talk about some of the options for running animations, and provide some tips for starting animations and running multiple animations at the same time.

The game I'm working on is a board game with a few 3D elements and (mostly) 2D elements that mimic 3D. I decided to implement the game using CSS3 transforms. In addition to CSS3, I also used the animation timing function, requestAnimationFrame, which is associated with the HTML5 specs. At the moment, I'm rendering one set of 2D game piece elements in a parent DIV element, which overlays the game board, but after I finish code optimization and performance testing, I may move the overlay containing the game pieces to an HTML5 <canvas> object.

Updated info, 2/19/2014: Check out the completed sample (Windows Store version)

When I started coding the animated components, I tried to think about user experience (UX) first before choosing a methodology for a particular animation. For UX, the main considerations were:

  • Should touch/click or button click invoke an animation, or will the app initiate the animation itself?
  • Should the animation complete every time it runs, or can it be canceled?
  • Does the animation need to support different values for multiple DOM elements?

In your own coding, the answers to these questions can help you decide whether to start a particular animation using CSS pseudo-classes (focus, hover), by simply resetting animations in your JavaScript code, or by using requestAnimationFrame.

A few words about animation technologies

CSS3 allows you to perform animations using DOM element properties. If you use HTML5 or WebGL instead, your app will likely involve more development time but will typically result in the best performance. If your app requires lots of complex computations for positioning elements, plan on using HTML5 <canvas> or WebGL. In that scenario, you may want to skip to the last section, "Using requestAnimationFrame to start an animation."

If your animations follow predictable patterns, CSS3 may work great, and these animations will get hardware-accelerated. In addition, if you are moving lots of DOM elements around the screen using left and top CSS style properties (not CSS3), you will need to keep a close eye on perf.

Using focus and hover events to start an animation

You can initiate CSS3 animations by implementing pseudo-classes such as focus and hover on specific elements. hover isn't much use in a touch-centric app, so after a few tests I stopped using it. The focus event was more intriguing.

To use focus (or hover), create an animation in your CSS. The following example shows a rotate animation around the Z-axis.

  @keyframes rotate {
      from {
          transform: rotateZ(0deg);
      }
      to {
          transform: rotateZ(180deg);
      }
  }

To use this animation, set the animation properties on the associated DOM element in your CSS or in your JavaScript code. Here's an example of CSS:

  animation-duration: 6s;
animation-fill-mode: forwards;
animation-timing-function: ease-in-out

Then include the hover or focus event selector and the animation-name property. (I've shown both event selectors here.)

  #inner:focus {
animation-name: rotate;
}
#outer:hover {
animation-name: rotate;
}

You can let the user start the animation by touch or click, or just call someFocusableElement.focus() in your code.

There are a few UX considerations to manage when using focus.

  • Your animation gets canceled as soon as something else receives focus.
  • You may not always want the user to decide which element receives focus.

The easiest way to manage these issues is to enable and disable pointer events on different elements. If you start an animation using focus and you want it to finish, disable pointer events on the other UI elements until the animation completes.

To disable pointer events on a set of elements:

  var children = wrapperElem.children;
var len = children.length;
for (var i = 0; i < len; i++) {
children[i].style.pointerEvents = "none";
}

To re-enable pointer events, use this value:

  children[i].style.pointerEvents = "visiblePainted";

If you need user-initiated animations but don't want to disable events on the other elements, use handlers for events like mouseup, mousedown, or focus in your code, and then use a different method to start the animation (see the following sections). In this scenario, you will of course need to remove the focus rule from your CSS code.

Changing the animation values to start an animation

When you change the animation values for an element, you start the animation. Depending on the new values, this will appear either as a restart or an update. For your animation to appear smooth, the starting CSS3 transform properties (like translate, rotate, scale, and opacity) must match the end properties of the previous animation. To incrementally change animation values that are different for each element in a set of DOM elements, you can dynamically change the @keyframes rule.

A very simple way to control animation using JavaScript is to create two or more @keyframes rules for an element. Then, when you want to start a new animation, just change the animation-name property from one rule to the other (you first reset duration, etc. in JavaScript).

  elem.style.animationName = exitPiece;

For example, I use the following rules for game elements in my board game app: enterPiece when a game piece is added, and exitPiece when a game piece is removed. Here is the CSS:

  @keyframes enterPiece {
      from {
          transform: translateY(-150px) scale(.5);
          opacity: 0;
      }
      to {
          transform: translateY(0px) scale(1);
          opacity: 1;
      }

  }

  @keyframes exitPiece {
      from {
          transform: translateY(0px) scale(1);
          opacity: 1;
      }
      to {
          transform: translateY(-150px) scale(.5);
          opacity: 0;
      }
  }

 

If you want to support additional behavior (for example when a game piece gets captured), you can create additional rules.

Using requestAnimationFrame to start an animation

The performance-optimized requestAnimationFrame function has replaced setTimeout and setInterval as the function of choice for timer-based animations. The callback function you pass to requestAnimationFrame gets called only when the browser is about to repaint the screen, so there are no wasted code cycles.

requestAnimationFrame is critical for independently adjusting values that need to be different for each member of a set of animated DOM elements. This function is used in loop-based HTML5 <canvas> apps, but you can also use it for simple manipulations of CSS properties like top and left. (If you want to benefit additionally from hardware acceleration, use requestAnimationFrame with HTML5 <canvas>, with WebGL, or potentially with some CSS3 properties values set in JavaScript.)Note: If you are resetting lots of DOM elements using left and top properties, you will need a tool to help test perf. In Windows Store apps and web apps, use the HTML UI Responsiveness profiler. The same profiler is available in F12 tools for IE11. The profiler is easy to use and provides good feedback on CPU utilization and frame rate. If there are perf issues, you may need to switch to a <canvas> implementation.

You call requestAnimationFrame like this:

  var requestId = window.requestAnimationFrame(animFrame);

You must be sure to cancel the animation callback when your animation is complete:

  window.cancelAnimationFrame(requestId);

For a loop-based game, you cancel the animation callback when the game ends. In my game, I cancel it in a setTimeout timer with a delay value that equals the animation-duration.

Note: To take action either at the beginning or at the end of an animation, you can use setTimeout or events like MSAnimationStart and MSAnimationEnd.

Here is the essential code for the animation frame callback loop. It includes a recursive call to requestAnimationFrame.

  function animFrame(args) {
      // Manipulate DOM elements

      requestId = window.requestAnimationFrame(animFrame);
  }

Within the animation frame callback function, I get the current top and left coordinates of my DOM elements using getBoundingClientRect() (a very handy API for games, at least for IE and Windows Store, especially when working with a transformed coordinate plane), and then reset those values. The code here is truncated for readability.

  function animFrame(args) {

      gameObjects.forEach(function(piece) {

          // Get and manipulate DOM elements, or use cached elements.
          // point may be an element on a transformed coordinate plane,
          // or you might use the current element (piece),
          // depending on your app.
          // NOTE: The WinJS call is Windows Store only, so for IE
          // you need to use a different method to get the position
          // of the parent element.
          var pos = WinJS.Utilities.getPosition(point.parentElement);

          var topRect = Math.round(point.getBoundingClientRect().top);
          var leftRect = Math.round(point.getBoundingClientRect().left);

          // pos.top and pos.left are offsets of the parent element.
          piece.style.top = topRect - pos.top - piece.clientHeight + "px";
          piece.style.left = leftRect - pos.left + "px";

      });

      requestId = window.requestAnimationFrame(animFrame);
  }

 

As noted previously, the style and rendering of top and left values here are not running on the GPU. To to do that, make <canvas> the parent element and use methods available in <canvas> to draw the pieces.

Although you can combine a CCS3 animation with the animations produced in the callback function, you typically just need to run the CSS3 animation once for each DOM element (not once for each frame) in a complete animation cycle. For example, I combine a CSS3 scale transform with the top/left animations in the previous code, but this code is not contained in the animation callback function. The important aspect here is the dynamic setting of an @keyframes rule to adjust scale individually for each game element. For more information, see my earlier post.

  // Get currentScale and targetScale values for each DOM element.
  cssRuleAdjustScale.deleteRule("0");
  cssRuleAdjustScale.deleteRule("1");

  cssRuleAdjustScale.appendRule("0% { transform: " + currentScale + "; }");
  cssRuleAdjustScale.appendRule("100% { transform: " + targetScale + "; }");

  piece.style.animationDuration = "5.5s";
  piece.style.animationName = "adjustScale";

When the app is finished, I'll upload the full sample and post a link here. If you have time to look at it then, please send me some feedback!