Recipe 1.6

Animating an Element with requestAnimationFrame

This demo shows an example of performing a basic animation using the requestAnimationFrame API.

Four boxes are animated, at different frame rates, over the same length of time. The requestAnimationFrame callback calculates how often to update the element based on the given frame rate.

Demo

5 FPS
10 FPS
30 FPS
60 FPS

Code

JavaScript
const button = document.querySelector('#toggle');
const box = document.querySelector('#box');

const ANIMATION_SECONDS = 1;

let isBoxVisible = true;
button.addEventListener('click', () => {
  button.disabled = true;
  Promise.all([
    animateBox('#box-5', 5),
    animateBox('#box-10', 10),
    animateBox('#box-30', 30),
    animateBox('#box-60', 60)
  ]).then(() => {
    isBoxVisible = !isBoxVisible;
    button.disabled = false;
  })
});

function animateBox(id, fps) {
  return new Promise(resolve => {
    const box = document.querySelector(id);

    // The time interval between each frame.
    const frameInterval = 1000 / fps;

    // The total number of frames.
    const frameCount = ANIMATION_SECONDS * fps;

    // The amount to adjust the opacity by in each frame.
    const opacityIncrement = 1 / frameCount;

    // The timestamp of the last frame.
    let lastTimestamp;

    // The starting opacity value and the direction to adjust it in.
    let opacity = isBoxVisible ? 1 : 0;
    const offset = isBoxVisible ? -opacityIncrement : opacityIncrement;

    /**
     * This callback is called by requestAnimationFrame.
     * @param timestamp The timestamp when requestAnimationFrame executed this function
     */
    function animate(timestamp) {
      // Set the last timestamp to now if there isn't an existing one.
      if (!lastTimestamp) {
        lastTimestamp = timestamp;
      }

      // Calculate how much time has elapsed since the last frame.
      // If not enough time has passed yet, schedule another call of this
      // function and return.
      const elapsed = timestamp - lastTimestamp;
      if (elapsed < frameInterval) {
        requestAnimationFrame(animate);
        return;
      }

      // Time for a new animation frame. Remember this timestamp.
      lastTimestamp = timestamp;

      // Adjust the opacity and make sure it doesn't overflow or underflow.
      opacity = Math.max(0, Math.min(1, opacity + offset));
      box.style.opacity = opacity;

      // If the opacity is still an intermediate value, the animation isn't done
      // yet. Schedule another run of this function.
      if (opacity > 0 && opacity < 1) {
        requestAnimationFrame(animate);
      } else {
        resolve();
      }
    }

    requestAnimationFrame(animate);
  });
}
HTML
<div class="flex flex-col items-center justify-center">
  <button class="btn btn-primary" id="toggle">Animate</button>
  <div class="container">
    <div class="row">
      <div class="col text-center m-4">
        <div class="text-lg">5 FPS</div>
        <div id="box-5" class="box text-bg-primary m-auto"></div>
      </div>
      <div class="col text-center m-4">
        <div class="text-lg">10 FPS</div>
        <div id="box-10" class="box text-bg-primary m-auto"></div>
      </div>
      <div class="col text-center m-4">
        <div class="text-lg">30 FPS</div>
        <div id="box-30" class="box text-bg-primary m-auto"></div>
      </div>
      <div class="col text-center m-4">
        <div class="text-lg">60 FPS</div>
        <div id="box-60" class="box text-bg-primary m-auto"></div>
      </div>
    </div>
  </div>
</div>

<style>
  .box {
    width: 5rem;
    height: 5rem;
  }
</style>
Web API Cookbook
Joe Attardi