Creating animations in Javascript using requestAnimationFrame

digi0ps

Sriram

Posted on July 4, 2020

Creating animations in Javascript using requestAnimationFrame

Animations in Javascript? I bet keyframes are better._ I have almost entirely used CSS to create animations for my websites. In fact, I rarely use Javascript directly to touch the styles of an element. But I recently stumbled upon a tutorial using requestAnimationFrame for animations. I expected the usage of it to be complex, but it was very simple yet powerful.

Animations in JavaScript before requestAnimationFrame

Before requestAnimationFrame, it was common to see people use setInterval to update styles of elements so that it was animated similar to the given code below. For a smooth animation we need to match the display framerate, which is 60fps, so we need to execute our function for 60 times per second which translates to an interval of 17ms.

const progressBar = document.querySelector('#progress-bar')
let width = 0

const updateProgressBar = () => {
  progressBar.style.width = width + '%'
  width += 1
  if (width > 100){
    clearInterval(id)
  }
}

const id = setInterval(updateProgressBar, 17)

A function, which updated the style of the element, was executed repeatedly on set intervals. Though this did the job, it wasn’t an effective way. There were many disadvantages for this approach. The timer wasn’t accurate i.e the browser would give priority to other UI tasks over this. The function would keep executing even if the user is in some other tab and depending on the execution it meant higher CPU usage.

Now what is requestAnimationFrame?

requestAnimationFrame was developed to overcome the shortcomings of the setInterval/setTimeout approach providing a native API to run any type of animation. It takes in a function as it’s argument and tells the browser to execute that function before the next repaint.

It is very similar to setInterval except we are requesting the browser to execute the animation at the next available opportunity instead of predefined interval.

Browsers generally optimize execution based on load, battery and element visibility on screen. All the animations are grouped into a single repaint, thus reducing the number of CPU cycles required. The animations are also halted when the user switches to a different tab. Yay!

Now doing the same progress bar with requestAnimationFrame gives us

const updateProgressBar = () => {
  progressBar.style.width = width + '%'
  width += 1
  if (width < 100){
    requestAnimationFrame(updateProgressBar) 
  }
}

requestAnimationFrame(updateProgressBar)

As we can see, we call the requestAnimationFrame initially and then recursively keep calling it until the required width level is reached. Clearly, this syntax is much better than setInterval.

Gravity using requestAnimationFrame

Now let’s try to use requestAnimationFrame to implement (a more complex) animation for a Ball experiencing free-fall under gravity.

We will be building a page which creates a ball everytime the user clicks on the page and the ball falls to the bottom. We will be trying to create mimic gravity for the fall.

First we will write a function to create a ball-like element. The ball will be div (with border-radius 50%) positioned absolutely. We will pass this function the x, y coordinates received from the click event.

const createBall = (top, left) => {
  const ball = document.createElement("div");
  ball.classList.add("ball");
  ball.style.top = top + "px";
  ball.style.left = left - 25 + "px";
  document.body.appendChild($ball);
  return ball;
}

Here we are creating a div, adding the class ball and sets the top/left values before returning the created element.

Next we write a function for initialising the fall animation which creates a function to deal with styles and initialises the animation frame request.

const initFalling = ball => {
  const ballHeight = 100
  const acceleration = 9.8 / 60;
  const { innerHeight } = window;

  let fallingSpeed = 0;

  const animateFall = () => {
    const top = parseInt(ball.style.top);
    const newTop = `${top + fallingSpeed}px`;

    // To break the fall, when the ball is near the surface
    if (parseInt(newTop) >= innerHeight - ballHeight) {
      ball.style.top = this.innerHeight - ballHeight + "px";
      ball.style.background = "red";
      return null;
    }

    // Else set the top to the new value
    ball.style.top = newTop;
    fallingSpeed = fallingSpeed + acceleration;
    requestAnimationFrame(animateFall);
  };

  requestAnimationFrame(animateFall);
};

Let’s breakdown this function.

Every ball starts with a fallingSpeed of 0 and is accelerated with every execution. Since acceleration due to gravity is 9.8m/s per second and the browser executes our function 60 times each second (general display framerate), so the acceleration per execution is 9.8/60.

We write another function inside function and call it animateFall. This will be the main function which will be passed to requestAnimationFrame. Pretty straight forward stuff inside. We retrieve the top of the ball and add falling speed to it. We check if this newTop value is greater the window’s innerHeight. If it is not, then we set it to the new value and increment fallingSpeed before requesting an animation frame again. If the newTop is greater, then the ball has hit the surface so we return from the function.

We are nearly done, all we have to do now is create a event handler and chain these two functions together.

const onClickListener = event => {
  const { x, y } = event;
  const ball = createBall(y, x);
  initFalling(ball)
};

document.addEventListener("click", onClickListener, false);

The event coordinates and the positioning coordinates are inverted. That is x here is equivalent to the left in positioning and y is equivalent to the top. And skadoosh we have created a free falling portal.

We can still make this better

  • Dying Animation; added a dying animation based on the impact of the collision. The falling speed is an indicator of how long the ball has travelled in air. So we can add a fading animation in which balls which have travelled more fade faster and those which have travelled less fade slowly.
  • Randomness; added a random factor for the color and size of the ball.
  • Organize; orgainize the code into components using Typescript + Webpack.

When should I use this instead of keyframes?

Though the performance of both requestAnimationFrame and keyframes/transitions are very close, we can stick to CSS animations for most of the cases.

Animating using JavaScript becomes super useful if the animation depends on some state or data, like the example above where we managed the fall of the ball using JS. We can also use it to create more complex animations like staggering animations or trailing animations (cue: I will be making a tutorial on trailing animations next).

References

And…

Thank you for reading!

💖 💪 🙅 🚩
digi0ps
Sriram

Posted on July 4, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related