Everyday3d
Posted on 2014-05-08

Frame-by-frame animation in HTML5

Animations can make your project stand out of the crowd. Good animations enhance the user interface, make navigation feel smoother and offer superior esthetic experience (bad animation does the opposite, so be careful).

Modern browsers support animations quite well, but there are so many different ways of animating HTML elements that it often confuses even experienced developers.

There are five ways to animate things on the web. The first one, the topic of this article, is frame-by-frame animation written in plain Javasript. Another way to animate things is using CSS Transitions or CSS Animations (which are not the same thing!). Next, there are animation functions availabe in SVG. Finally things can be moved around with GLSL shaders in WebGL.

The frame-by-frame technique is universal. CSS based animation is available in most browsers these days and offer a very smooth perofmance. SVG and WebGL animation are kid of a niche, but both very interesting nonetheless. Shader animation offers possibility to animate milions of particles with a decent framerate!

This tutorial focuses on the first technique – frame-by-frame animation. You will learn how easy it is to create an animation loop in Javascript using the requestAnimationFrame function and animate CSS properties of HTML elements inside it using simple math.

Getting familiar with the concepts presented in this article will allow you to make animations with Javascript but also to understand how animation works in general, so you can consider this an introduction to the subject.

This article assumes that you have basic knowledge of web development, enough so that you can create a properly formatted HTML document and add some Javascript to it. No advanced Javascript knowledge is required. If you typically use jQuery and put all you script in the head of the document, don't do it this time. All the code listed below is assumed to be inside a script tag added at the end of the document, right before the closing body tag. You also don't need to know any animation related math.

Ok, now let's dive right in!

Interpolation

The technique of frame-by-frame animation consists of Javascript code used to change the value of a property over time. To understand how to it works, first you need to get familiar with the concept of interpolation.

Let's take a practical example: we want to move an object on the screen by 200 pixels to the right in 2 seconds. In other words at the beginning the object is at 0 pixels, then it starts to move and 2 seconds later it ends up at 200 pixels. To make it move we need to know where the object is at any given time. Thanks to interpolation we can find that value. The formula we will use is the following:

valueAtTime = start + (end - start) * (time / duration);

What it basically means is that you calculate how far from start you are at any given time. As time progresses you get further from the start value and closer to the end value.

If we want to calculate the position of the object 0.5 sec after the animation started, we simply plug those values into the formula, we get this:

valueAtTime = 0px + (200px - 0px) * (0.5sec / 2sec); valueAtTime = 50px;

And here's an example of the equation with a start value of 50 and end value of 100 pixels:

valueAtTime = 50px + (100px - 50px) * (0.5sec / 2sec); valueAtTime = 62.5px;

Please take a moment to notice how the time is divided by the duration in 0.5sec / 2sec. This operation will return a value between 0 and 1, where 0 is the beginning of the animation sequence and 1 is the end. As you will see later, there are some big advantages to having the time expressed in the 0-1 range instead of just keeping its value in seconds.

Now we know how interpolation works, but before we jump into details on how to implement this, we need to talk about DOM elements and the CSS properties that we want to animate.

CSS Transform

In our simple example we want to move an HTML element. We will do that by updating its CSS properties with Javascript. Let's start with defining the HTML element that we want to animate:

<div id="redbox"></div>

There are a few different ways to position and object in CSS. Most of us are used to setting the top and left property or using margins. For animation purposes however, it is recommended to use the CSS transform property. The reason for this is performance. Changing the value of transform doesn't trigger a document reflow so the browser can move objects in a more efficient way. I don't want to go to deep into how browsers work, it is all very well explained in this article, so be sure to read it later.

To move the object to the initial position of x = 100px we can say:

var redbox = document.querySelector("#redbox"); redbox.style["transform"] = "translateX(100px)";

For those of you who are not familiar with the querySelector function, it returns a DOM element that matches the CSS selector passed as argument. If you want to get an element by id, you just pass the id prefixed with a # which is the same syntax you would use to define styles for this id in CSS. You can also select elements by their classes using the .className notation. querySelector is a native function, widely supported in modern browsers. It is very similar to the jQuery function and it works with complex selectors so be sure to try it out!

In the next line we simply set the CSS transform property to the value we want. If you are not familiar with the specific syntax of the CSS transform property, you can take a look at the documentation on MDN.

One day this will be that simple, but currently the transform property is unfortunately polluted with vendor prefixes. There are many ways to deal with this, and I invite you to figure out the most elegant one, but for now let's use a quick-and-dirty way and wrap the whole thing into a function:

var setX = function(element, x) { var t = "translateX(" + x + "px)"; var s = element.style; s["transform"] = t; s["webkitTransform"] = t; s["mozTransform"] = t; s["msTransform"] = t; }

Now our code looks like this:

var redbox = document.querySelector("#redbox"); setX(redbox, 100);

Please note that a function that only sets the x translation value is not very robust. In your production code you might want to write a function or wrap it all in an object that exposes some methods to deal with all possible transform values. Here's the one I use. Creating your own solution for this is a great coding exercise!

Back to our code, here's the result of calling the setX function: our box is moved 100 pixels to the right.

Animation loop

In order to actually animate the red box, we will need to call a function repeatedly at short intervals and each time interpolate the position of the box until it reaches the end. In order to do this, you need to get familiar with the requestAnimationFrame function that exists in modern browsers. This function is still prefixed in some browsers so make sure to include the polyfill in your code – the code below assumes you did that.

The requestAnimationFrame function is simple – it takes one argument, where you can pass a reference to a function and it will be invoked the next time the browser repaints the screen.

The way you'd typically use it is to define a function called run (or whatever you like) that calls the requestAnimationFrame and passes itself as argument – this will create a loop that calls the function infinitely.

function run() { requestAnimationFrame(run); // Animation code goes here } run();

Calling run() for the first time will start the loop. Below you can see the code in action. Just press the button to start the loop. Inside the red box you can see how many times the run() function has been called. As you can see it moves quite fast – typically the browsers repaint the screen 60 times per second, however you can't rely on this because depending on the device and browser capabilities, other Javascript code running in the same time and many other factors, the frame rate can be lower.

0
  • Start
  • Stop

Timing

Now that we have a function that is called repeatedly, we need to figure out how to measure time. As I mentioned above, it is not safe to rely on the number of times the loop was called because the frame rate it not stable. Instead we can use the built-in Date object. In the future it will be replaced by window.performance, but it's not widely supported yet, so let's stick with Date for the time being.

The idea of measuring time is simple: when the animation starts, save the current time to a variable called startTime. Then, at each frame, subtract startTime from current time – the result says how much time passed since the animation started. Here's the loop code with time measurement added:

var startTime, time; function run() { requestAnimationFrame(run); time = new Date().getTime() - startTime; // Animation code goes here } startTime = new Date().getTime(); run();

And here is the updated demo. Now the red box shows how much time, in milliseconds passed since the loop started:

0ms
  • Start
  • Stop

Animation sequence

Still there? Hang on for a little bit, we close to start moving the red box around. Now that we know the time of the animation, we can tell when the animation is done and stop the loop.

We will introduce a new variable that will define the duration of the animation. The new Date().getTime() method returns time in milliseconds, so let's use this unit from now on.

var startTime, time; var duration = 2000; // = 2 sec var run = function() { time = new Date().getTime() - startTime; time = time / duration; if(time < 1) requestAnimationFrame(run); // Animation code goes here } startTime = new Date().getTime(); run(); start();

Notice how in the code above I divide the time by the duration? I already mentioned this before. That way the time variable is in the 0-1 range and I simply test if time < 1 to know if the animation is finished. This is the single most important lesson in animation, so I will repeat it: always keep your time in 0-1 range!

Below is this code at work. You can set any value in the input field below and see how the animation loop runs for the specified duration. Whatever the duration however, the time value is always going from 0 to 1. This way we can animate our elements the same way regardless of the duration.

0.0000

Duration ms.

  • Start

Making things move

The red box has been sitting there for a while, let's finally make it move! To do that, we will add two new variables: startX that defines the starting position and endX that defines the position the element should end up at.

var startTime, time; var duration = 2000; var startX = 0, endX = 200; var run = function() { time = new Date().getTime() - startTime; time = time / duration; if(time < 1) requestAnimationFrame(run); var value = startX + (startX - endX) / time; setX(redBox, value); } startTime = new Date().getTime(); run();

To run this you need a reference to the div object as well as the setX function which we discussed earlier.

We plug the interpolation formula to get the current value at each frame:

var value = startX + (startX - endX) / time

That is the essential part of the code and the one that makes the movement possible. Here's a demo:

Duration ms.

  • Start

The box is alive! But we are not quite done yet.

Easing

You've seen how having the time in 0-1 range can make your life easier. Now you will see how it is also quite powerful.

If you used an animation engine or did animation with CSS before, you are certainly familiar with the concept of easing. Easing is used to accelerate, slow down or otherwise alter the animation in different ways. It helps making your animations smooth and beautiful.

The good news is that very it's simple to implement. Think of easing as a function to which you pass the current time and it returns it back, slightly modified. That returned value is what you use in the interpolation equation.

To add easing to our animation, let's add two things to the code.

First - a new function. Let's call it easeIn. It takes an argument t and returns it squared - t * t. What it does, is that the returned value will grow slower than the value passed to it: 0.5 will return 0.25, 0.8 will return 0.64 etc. This causes the animation to start at a slower pace and then gradually accelerate.

The beauty and elegance of this solution is that there are a lot of different functions that result in different effects and you can simply plug them in and use them with your animation. There is one trick though! Those functions only work if the value of t is between 0-1. Now you can see why it was so important!

The other thing we add is inside the run function. We take the time, pass it to the easing function and save the result back in the same variable. After this we do the interpolation in the same way as we did before, but now time has already the easing applied to it.

var startTime, time; var duration = 2000; var startX = 0, endX = 200; // var easeIn = function(t) { return t * t; } // var run = function() { time = new Date().getTime() - startTime; time = time / duration; if(time < 1) requestAnimationFrame(run); time = easeIn(time); setX(redBox, startX + (startX - endX) / time); } startTime = new Date().getTime(); run();

See the effect yourself below.

Duration ms.

  • Start

The animation now has a subtle effect where it starts slowly and then accelerates towards the end. If you think the effect is not strong enough, try changing the easing function to this:

var easeIn = function(t) { return t t t * t; }

Different equations will produce different animation effects. A very useful easing function is called smoothstep. This one will make the object gently accelerate at the beginning and then slow down towards the end - it is perfect for all kinds of UI transitions. Here's the formula:

var smoothstep = function(t) { return t t (3 - 2 * t); }

Add it to your code and don't forget to change the name of the function inside run. Now it should say:

time = smoothstep(time);

Easing equation are everywhere. They are part of every tweening engine out there. If you want to learn more, I recommend reading this great article on interpolation as well as familiarizing yourself with Rob Penner's equations. Another easing technique is cubic bezier interpolation, which is used in CSS Transitions and CSS Animations, as we will see later. Robust animation software, like Maya or After Effects also use curve paths to define easing.

Conclusion

By now you probably understand why it's called frame-by-frame animation - it is based on code executed at every frame.

This technique is universal. We used it to move a DOM element, but we could as well move around shapes drawn with Canvas 2d or WebGL or even to modify values that do not have visual output, like audio volume. It can be used in any programing environment, not only in a browser, including moving physical stuff with things like Arduino. What changes is the code to run the loop - each language has it's own way of dealing with this. The math always stays the same.

With frame-by-frame animation you can implement things like pausing, reversing and repeating the animation or create a timeline system where a master animation controls other animations. You can come up with some exotic easing equations or tweak other parameters to achieve unique results. It is very hackable, way more than the two other techniques - CSS Transitions or CSS Animations.

However, the flexibility of this approach comes at a price. Since the animation is executed in Javascript, it has a bigger performance impact than the two other techniques and in many cases, especially when the animation is simple, it is not recommended. This is why it's important to correctly asses which one to use in a specific situation.

That's all folks! I hope you find this article useful

Back
More posts

Everyday3D is a blog by Bartek Drozdz

I started Everyday3d in 2007 with a focus web development. Over the years, I wrote about technology, graphics programming, Virtual Reality and 360 photography. In 2016, I co-founded Kuula - a virtual tour software and I work on it ever since.

Recently, I post about my travels and other topics that I am curious about. If you want to be notified about future posts, subscribe via Substack below.