Animations are cool, especially on large screens, so Firefox OS on TV features a lot of smooth, subtle animations to enhance user experience. This article provides guidance for creating effective animations that work well on large screens.
Graphics performance basics
Before starting to code, lets think about graphics performance at a basic level. According to App performance validation, a UI change may trigger a reflow (relayout), a repaint, and a composition. If we want graphics performance to be better, we should trigger them as little as possible.
There are various ways to create animations. For example, when moving a box from left (100px) to right (300px), we may use left
, margin
, padding
, border
, or transform
to do so. According to CSS Triggers, we should just use transform
because:
left
,margin
, andpadding
trigger reflow, repaint, and composition;border
triggers repaint and composition;transform
only triggers composition.
The following Codepen is available if you want to perform this test yourself: CSS animation with different properties. try turning on the Firefox FPS meter before running each animation (enabled via the layers.acceleration.draw-fps
pref in Firefox's about:config.
Layers
This is cool if we only want to use CSS animations or CSS transitions. When implementing animations with JavaScript however, we need to know about Layers.
A layer is a basic memory block for compositing. All layers will be merged by the compositor before sending them to the graphics buffer. If we have a hardware accelerated compositor (e.g. a GPU), the composition may be done there, which makes performance better.
Since there are many posts discussing how to create a layer (see Accelerated Rendering in Chrome and Layers: Cross-Platform Acceleration for example), here we will give a simple general guide for creating a layer. You'll need a setup along the lines of:
- An element to animate, such as
<video>
or<canvas>
. transform: translateZ(0.01px)
applied to your element, to separate it onto its own layer.- CSS animations or CSS transitions to perform the actual animation.
We can enable the nglayout.debug.paint_flashing
pref in about:config to get a guide to which HTML element are layers (see Layout paint flashing in Firefox for more details.) The LayerScope also provides a way to determine the layer structure in Firefox.
A layer needs a block of memory. A layer 100x100px in size may use 100 * 100 * 4 (color depth) = 40KB memory size. Therefore, you should use layers carefully with a source-limited device.
CSS-based animations
The most common ways to create animations on the Web are CSS animations or CSS transitions.
CSS transitions are used to transition between two different states. Unfortunately, we cannot pause or resume a transition without JavaScript because CSS transitions are not designed for this situation. However, we can change states to affect transitions as they run. CSS animations are used to loop animations, allowing for pausing and resuming as required. See our CSS-based animation example for a demonstration of the difference.
JavaScript-based animations
For JavaScript based animations, we should use window.requestAnimationFrame (RAF), designed to provide precise timing for animations, in the same fashion as CSS transitions and CSS animations.
Note: Don't use setTimeout for animations if you can avoid it. It is not as precise as RAF, and has a host of other problems.
Depending on browser implementations, RAF may be triggered before or after CSS transitions/CSS animations. A good animation, whether made by CSS transitions or CSS animations or RAF, should run at 60 fps. RAF will start to slow down however if to many animations are run on it concurrently. Please note that RAF stops running if the frame it is acting on is set to be invisible in some way.
Some libraries claim that JavaScript-based animations may have better performance than CSS animations and CSS transitions. This isn't exactly true, although they can get close, depending on browser implementation. In Firefox for example we have off main-thread animations (OMTA.) When we use CSS animations or CSS transitions, the OMTA moves all calculations to the GPU to offer better performance. Without OMTA, we can have better performance with RAF, which still depends on CSS attributes.
Building a "Menu Group" web component with CSS Transitions
In this section, we briefly describe how a series of CSS transitions is utilised in the Menu Group web component we built for the Homescreen app on Firefox OS TVs. It is located on the upper left of the screen as a gearwheel icon and expands when focused. To see it in action for yourself, check out our Menu Group usage example: to run this successfully you need Firefox Nightly with the dom.webcomponents.enabled
pref enabled in about:config.
Why use CSS transitions to animate “Menu Group”? Users should be able to open or close the Menu Group at any time, and CSS transitions can achieve this easily. The transition from close to open can be divided into 3 steps: enlarging, shrinking and opening. First, the icon becomes a little bit bigger and its background color changes. Second, it returns to the original state. Finally, the icon rotates, and the Menu Group expands to show the menu items it contains.
Building this with CSS transitions is straight forward. We defined 5 CSS classes for each state: enlarging
, shrinking
, and opening
are for the opening transition, while closing
and closed
are for the closing transition. To chain the transitions, the transitionend
event handler works as a state machine, which changes the applied CSS class accordingly. Here is the event handler source code:
proto.handleEvent = function(evt) { switch(evt.type) { // Like System app, the transition is our state machine. case 'transitionend': // We only process 'background-color' because all states have this // change. if ((evt.propertyName !== 'background-color' && evt.propertyName !== 'width') || evt.target !== this) { break; } if (this.classList.contains('enlarging')) { this.classList.remove('enlarging'); this.classList.add('shrinking'); // change to shrinking } else if (this.classList.contains('shrinking')) { // XXX: this is a workaround of CSS transform. We cannot have a // rotation right after resizing without any settimeout??? setTimeout((function() { if (!this.classList.contains('shrinking')) { // If we don't have shrinking class here, that means this group // is changing to another state and we don't need to opening // state. return; } this.classList.remove('shrinking'); this.classList.remove('closed'); this.classList.add('opening'); // change to opening this.style.width = this.calculateChildWidth() + 'px'; }).bind(this)); } else if (this.classList.contains('opening')) { this.classList.remove('opening'); // final state: opened this.fireEvent('opened'); } else if (this.classList.contains('closing')) { this.classList.remove('closing'); this.classList.add('closed'); // final state: closed this.fireEvent('closed'); } break; } };
Disadvantages of using CSS transitions
Although CSS transitions are easy to implement, they are not without problems:
- Performing actions following a CSS transition: No event will be fired after cancelling a transition. When we build a web app with actions following transitions, it’s possible that after transitioning element(s) are removed, these actions won't happen, which sometimes stalls the application. In this case, we usually need a timer to monitor whether a transition is cancelled or not. This happens mostly in web apps with rich animated UI components.
- Making sure CSS transitions are triggered: CSS transitions sometimes will not be triggered with a change of CSS class alone. For newly appended elements, as well-know workaround is to manually trigger a reflow and the transition for the elements using getComputedStyle() or setTimeout().
- Pausing or changing the CSS transition playback rate: Although changing pause and playback rates are possible with CSS transitions, it is not easy to make it 100% precise if the
transition-timing-function
is notlinear
.
Issues like these make chaining a series of transitions together more complicated. In addition, we need to pay more attention to handling unexpected user interactions. Even with a proper animation library it's still difficult to match the flexibility of JavaScript-based animations.
Getting started with Web Animations
The emerging Web Animations spec provides a great way to control CSS Transitions and Animations. To demonstrate usage of Web Animations in Firefox, we rewrote a part of the animation code in the last example — see Menu Group using Web Animations API. Please use Firefox Nightly 42 or later to run this example code.
The major difference in this version of Menu Group is that instead of handling the transitionend
event to chain or cancel CSS transition state changes, here we use the ES6 Promise object returned from the Web Animations API. Take the animateEnlarge()
method for example. When called, the function first sets the CSS class for the enlarging animation, and then gets the CSSTransition
object we want to handle.
proto.animateEnlarge = function() { this.classList.remove('shrinking'); this.classList.remove('opening'); this.classList.remove('closing'); this.classList.add('enlarging'); return this.getTransition('background-color').finished; };
The getAnimations()
method returns an array of the CSSTransition
s/CSSAnimation
s currently applied to the element. The finished
property is a Promise
object that is either resolved when the CSSTransition
is finished or rejected when the CSSTransition
is cancelled. Since CSSTransition
and CSSAnimation
objects are now much easier to provide with corresponding actions, we can better control both when one of them gets cancelled.
proto.getTransition = function(attr) { var transition; // 'this' refers to the Menu Group element this.getAnimations().forEach( (animation) => { if (animation.transitionProperty === attr) { transition = animation; } }); return transition; };
Chaining a series of CSSTransition
s now becomes much easier to structure and to read as well. When focus/open
is called, we first cancel all the CSSTransition
s/CSSAnimation
s on this Menu Group element and then perform transitions that are chained using ES6 Promise
.
proto.focus = proto.open = function() { this.getAnimations().forEach((animation) => { animation.cancel(); }); this.fireEvent('will-open'); this.animateEnlarge() .then(this.animateShrink.bind(this), this.resetToClose.bind(this)) .then(this.animateOpen.bind(this), this.resetToClose.bind(this)) .then( () => { this.classList.remove('opening'); this.fireEvent('opened'); }, () => { this.resetToOpen(); this.fireEvent('opened'); }); };
Note: The Web Animations API is currently being implemented in Firefox. See Are we animated yet for the latest list of supported API objects.