10 min read

I think that most developers who have not had much experience with Canvas will generally believe that it is unnecessary, non-performant, or difficult to work with. Canvas’s flexibility lends itself to be experimented with and compared against other solutions in the browser. You find comparison articles all over the web comparing things like animations, filter effects, GPU rendering, you name it with Canvas. The way that I look at it is that Canvas is best suited to creating custom shapes and effects that hook into events or actions in the DOM. Doing anything else should be left up to CSS considering it can handle most animations, transitions, and normal static shapes. Mastering Canvas opens a world of possibilities to create anything such as video games, smartwatch interfaces, and even more complex custom 3D graphics. The first step is understanding canvas in 2D and animating some elements before getting into more complex logic.

A simple idea that might demonstrate the abilities of Canvas is to create a Cortana- or HAL-like interface that changes shape according to audio. First we will create the interface, a simple glowing circle, then we will practice animating it. Before getting started, I assume you will be using a newer version of Chrome. If you are using a non-WebKit browser, some of the APIs used may be slightly different.

To get started, let us create a simple index.html page with the following markup:

<!doctype html>
<html>
  <head>
    <title>Demo</title>
    <style>
      body {
        padding: 0;
        margin: 0;
        background-color: #000;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <script></script>
  </body>
</html>

Obviously, this is nothing special yet, it just holds the simple HTML for us to begin coding our JavaScript. Next we will start to fill in the <script> block below the <canvas> element inside of the body. The first part of your script will have to get the reference to the <canvas> element, then set its size; for this experiment, let us just set the size to the entire window:

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
})();

Again, refreshing the page will not show much. This time let’s render a small circle to begin with:

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle

  context.beginPath(); // Start to create a shape
  context.arc(0, 0, 50, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
  context.fillStyle = '#8ED6FF'; // Set the color of the circle, make it a cool AI blue
  context.fill(); // Actually fill the shape in and close it
})();

Alright! Now we have something that we can really start to work with. If you refresh that HTML page you should now be staring at a blue dot in the center of your screen. I do not know about you, but blue dots are sort of boring, nothing that interesting. Let’s switch it up and create a more movie-esque AI-looking circle.

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  const color = '#8ED6FF';

  context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
  context.beginPath(); // Start to create a shape
  context.arc(0, 0, 50, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
  context.strokeStyle = color; // Set the stroke color
  context.shadowColor = color; // Set the "glow" color
  context.lineWidth = 10; // Set the circle/ring width
  context.shadowBlur = 60; // Set the amount of "glow"
  context.stroke(); // Draw the shape and close it
})();

That is looking way better now. Next, let’s start to animate this without hooking it up to anything. The standard way to do this is to create a function that alters the canvas each time that the DOM is able to render itself. We do that by creating a function which passes a reference to itself to window.requestAnimationFrame. requestAnimationFrame takes any function and executes it once the window is focused and has processing power to render another frame. This is a little confusing, but it is the best way to create smooth animations. Check out the example below:

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  const color = '#8ED6FF';

  const drawCircle = () => {
    context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
    context.beginPath(); // Start to create a shape
    context.arc(0, 0, 50, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
    context.strokeStyle = color; // Set the stroke color
    context.shadowColor = color; // Set the "glow" color
    context.lineWidth = 10; // Set the circle/ring width
    context.shadowBlur = 60; // Set the amount of "glow"
    context.stroke(); // Draw the shape and close it
    window.requestAnimationFrame(drawCircle); // Continue drawing circle
  };

  window.requestAnimationFrame(drawCircle); // Start animation
})();

This will not create any type of animation yet, but if you put a console.log inside the drawCircle function you would see a bunch of logs in the console, probably close to 60 times per second. The next step is to add some state to the this function and make it change size. We can do that by creating a size variable with an integer that we change up and down, plus another boolean variable called grow to keep track of the direction that we will change size.

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  const color = '#8ED6FF';

  let size = 50; // Default size is the minimum size
  let grow = true;

  const drawCircle = () => {
    context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
    context.beginPath(); // Start to create a shape
    context.arc(0, 0, size, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
    context.strokeStyle = color; // Set the stroke color
    context.shadowColor = color; // Set the "glow" color
    context.lineWidth = 10; // Set the circle/ring width
    context.shadowBlur = size + 10; // Set the amount of "glow"
    context.stroke(); // Draw the shape and close it

    // Check if the size needs to grow or shrink
    if (size <= 50) { // Minimum size
      grow = true;
    } else if (size >= 75) { // Maximum size
      grow = false;
    }

    // Grow or shrink the size
    size = size + (grow ? 1 : -1);

    window.requestAnimationFrame(drawCircle); // Continue drawing circle
  };

  window.requestAnimationFrame(drawCircle); // Start animation
})();

Refreshing the page and seeing nothing happen might be a little disheartening, but do not worry you are not the only one! Canvases require being cleared before being drawn upon again. Now we have created a way to change and update the canvas, but we need to introduce a way to clear it.

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  const color = '#8ED6FF';

  let size = 50; // Default size is the minimum size
  let grow = true;

  const drawCircle = () => {
    context.clearRect(0, 0, canvas.width, canvas.height); // Clear the contents of the canvas starting from [0, 0] all the way to the [totalWidth, totalHeight]

    context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
    context.beginPath(); // Start to create a shape
    context.arc(0, 0, size, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
    context.strokeStyle = color; // Set the stroke color
    context.shadowColor = color; // Set the "glow" color
    context.lineWidth = 10; // Set the circle/ring width
    context.shadowBlur = size + 10; // Set the amount of "glow"
    context.stroke(); // Draw the shape and close it

    // Check if the size needs to grow or shrink
    if (size <= 50) { // Minimum size
      grow = true;
    } else if (size >= 75) { // Maximum size
      grow = false;
    }

    // Grow or shrink the size
    size = size + (grow ? 1 : -1);

    window.requestAnimationFrame(drawCircle); // Continue drawing circle
  };

  window.requestAnimationFrame(drawCircle); // Start animation
})();

Now things may seems even more broken. Remember that context.translate function? That is setting the origin of all the commands to the middle of the canvas. To prevent that from messing up our canvas when we try to clear it, we will need to save the state of the canvas and restore before hand.

(() => {
  const canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
  const color = '#8ED6FF';

  let size = 50; // Default size is the minimum size
  let grow = true;

  const drawCircle = () => {
    context.restore(); // Restore previous canvas state, does nothing if it wasn't saved before
    context.save(); // Saves it until the next time `drawCircle` is called

    context.clearRect(0, 0, canvas.width, canvas.height); // Clear the contents of the canvas starting from [0, 0] all the way to the [totalWidth, totalHeight]

    context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
    context.beginPath(); // Start to create a shape
    context.arc(0, 0, size, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
    context.strokeStyle = color; // Set the stroke color
    context.shadowColor = color; // Set the "glow" color
    context.lineWidth = 10; // Set the circle/ring width
    context.shadowBlur = size + 10; // Set the amount of "glow"
    context.stroke(); // Draw the shape and close it

    // Check if the size needs to grow or shrink
    if (size <= 50) { // Minimum size
      grow = true;
    } else if (size >= 75) { // Maximum size
      grow = false;
    }

    // Grow or shrink the size
    size = size + (grow ? 1 : -1);

    window.requestAnimationFrame(drawCircle); // Continue drawing circle
  };

  window.requestAnimationFrame(drawCircle); // Start animation
})();

Oh snap! Now you have got some seriously cool canvas animation going on. The final code should look like this:

<!doctype html>
<html>
  <head>
    <title>Demo</title>
    <style>
      body {
        padding: 0;
        margin: 0;
        background-color: #000;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <script>
      (() => {
        const canvas = document.querySelector('canvas');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        const context = canvas.getContext('2d'); // Set the context to create a 2D graphic
        const color = '#8ED6FF';

        let size = 50; // Default size is the minimum size
        let grow = true;

        const drawCircle = () => {
          context.restore();
          context.save();
          context.clearRect(0, 0, canvas.width, canvas.height); // Clear the contents of the canvas starting from [0, 0] all the way to the [totalWidth, totalHeight]

          context.translate(canvas.width / 2, canvas.height / 2); // The starting point for the graphic to be in the middle
          context.beginPath(); // Start to create a shape
          context.arc(0, 0, size, 0, 2 * Math.PI, false); // Draw a circle, at point [0, 0] with a radius of 50, and make it 360 degrees (aka 2PI)
          context.strokeStyle = color; // Set the stroke color
          context.shadowColor = color; // Set the "glow" color
          context.lineWidth = 10; // Set the circle/ring width
          context.shadowBlur = size + 10; // Set the amount of "glow"
          context.stroke(); // Draw the shape and close it

          // Check if the size needs to grow or shrink
          if (size <= 50) { // Minimum size
            grow = true;
          } else if (size >= 75) { // Maximum size
            grow = false;
          }

          // Grow or shrink the size
          size = size + (grow ? 1 : -1);

          window.requestAnimationFrame(drawCircle); // Continue drawing circle
        };

        window.requestAnimationFrame(drawCircle); // Start animation
      })();
    </script>
  </body>
</html>

Hopefully, this should give you a great idea on how to get started and create some really cool animations. The next logical step to this example is to hook it up to an audio so that it can react to voice like your own personal AI.

Author

Dylan Frankland is a frontend engineer at Narvar. He is an agile web developer with over 4 years of experience in developing and designing for start-ups and medium-sized businesses to create functional, fast, and beautiful web experiences.

LEAVE A REPLY

Please enter your comment!
Please enter your name here