Canvas animation in React

In this blog post we will learn to create a simple animation in HTML Canvas element in React component. This is also applicable for Next.js projects.

ReactProgrammingWeb Dev

Welcome back to my another blog post. In today's tutorial we will learn to create a basic animation using HTML Canvas element in React component. This approach can also be used in Next.js projects.

Creating a Canvas element

We begin by creating a simple HTML Canvas element and add the width and height attributes.

export function Example() {
  return <canvas width={500} 
    height={300} 
    style={{border: '1px solid #333'}} />
}

Now, we create a reference to the canvas using the useRef hook. We will use this to get the canvas context.

import {useRef} from 'react';

export function Example() {
  const canvasRef = useRef(null);
  return (
    <canvas ref={canvasRef} 
      width={500} height={300} 
      style={{ border: '1px solid #333' }} />
  );
}

Trying to get context directly will throw error

If we try to get the context of the canvas while the component is not yet mounted, we will get error like the following.

import {useRef} from 'react';

export function Example() {
  const canvasRef = useRef(null);
  const canvas = canvasRef.current;
  const context = canvas.getContext('2d');
  return (
    <canvas ref={canvasRef} 
      width={500} 
      height={300} 
      style={{ border: '1px solid #333' }} />
  );
}

Error:

TypeError: Cannot read properties of null (reading 'getContext')

Get the canvas context using useEffect hook

To solve the above error we have to first let the component mount and then inside the useEffect hook we will get the canvas context. For this we will write the following code.

import { useRef, useEffect } from 'react';

export function Example() {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
  }, []);

  return (
    <canvas
      ref={canvasRef}
      width={500}
      height={300}
      style={{ border: '1px solid #333' }}
    />
  );
}

Draw circle and fill it with color

We will draw a circle at the center of the canvas. Our canvas width is 500px and height is 300px. So, our circle should have it's center at 250px along x-axis and 150px along y-axis inside our canvas.

Note! Origin is at the top left corner of the canvas. The x-axis moves from top left corner to top right corner. Similarly, the y-axis moves from top left corner to the bottom left corner.

We can extract the width and height of the canvas from the canvas variable we created above. And let's create a circle that has 30px radius. For this we will write the following lines inside the useEffect hook.

const { width, height } = canvas;
let radius = 30;

Now, to draw the circle we use the arc method. We pass 5 arguments to it.

arc(x, y, radius, startAngle, endAngle)

Where, x and y is for the center of the circle. The radius is for the radius of the circle and it must be a positive value. The arc start from the positive x-axis. And it is drawn clockwise.

Check MDN on arc method.

Write the following code in the useEffect hook.

import { useRef, useEffect } from 'react';

export function Example() {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');

    const { width, height } = canvas;
    let radius = 30;

    // draw circle and fill it
    context.clearRect(0, 0, width, height);
    context.beginPath();
    context.fillStyle = '#333';
    context.arc(width / 2, height / 2, radius, 0, 2 * Math.PI);
    context.fill();
  }, []);

  return (
    <canvas
      ref={canvasRef}
      width={500}
      height={300}
      style={{ border: '1px solid #333' }}
    />
  );
}

At this point if we open our page in the browser we will have a circle at the center of the canvas.

Creating animation loop

We can create an animation loop by writing the following code inside the useEffect hook.

const animate = () => {
  requestAnimationFrame(animate);
};
animate();

So, we are creating an animate function and inside it we are calling requestAnimationFrame function and passing the animate function name. This will create a loop.

And inside the useEffect hook, at the end, we are invoking the animate function which starts the loop. Now, we will move the logic to draw the circle inside the animate function.

const animate = () => {
  // draw circle and fill it
  context.clearRect(0, 0, width, height);
  context.beginPath();
  context.fillStyle = '#333';
  context.arc(width / 2, height / 2, radius, 0, 2 * Math.PI);
  context.fill();

  requestAnimationFrame(animate);
};
animate();

This will clear the canvas and draw the circle and repeat in loop. Since this is happening very fast so, it is not giving us the animation feel. Let us try to change the radius and oscillate it between 10px to 50px.

Changing the radius to create animation

Inside our animate function we will increase the radius of the circle as long as it is less than 50px. As so as it becomes 50px, we will start to decrease the radius as long as it is greater than 10px. Once the radius reaches 10px, we will start to increase the radius again and thus the radius will oscillate between 10px and 50px.

We will create a dr variable which controls the rate at which the radius increases or decreases. In our case, we will change our radius by 1px.

let dr = 1;

Now, let us write the logic to oscillate the radius between 10px and 50px.

if (radius >= 50 || radius <= 10) {
  dr *= -1;
}
radius += dr;

Final code

Following code will create a circle whose radius will oscillate between 10px to 50px.

import { useRef, useEffect } from 'react';

export function Example() {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');

    const { width, height } = canvas;
    let radius = 30;

    // this will control the speed of radius change
    let dr = 1;

    const animate = () => {
      if (radius >= 50 || radius <= 10) {
        dr *= -1;
      }

      radius += dr;

      // draw circle and fill it
      context.clearRect(0, 0, width, height);
      context.beginPath();
      context.fillStyle = '#333';
      context.arc(width / 2, height / 2, radius, 0, 2 * Math.PI);
      context.fill();

      requestAnimationFrame(animate);
    };
    animate();
  }, []);

  return (
    <canvas
      ref={canvasRef}
      width={500}
      height={300}
      style={{ border: '1px solid #333' }}
    />
  );
}

Following GitHub repository contains Next.js example.

canvas-animation-nextjs

This brings us to the end of this tutorial. Hope it was interesting. See you soon in my next videos and blog posts. Feel free to subscribe my YouTube channel @yusufshakeel. Have fun coding.