Drawing interactive shapes with the Canvas API in a React application
Tags: react, canvas
Published: 2022-10-03 17:07:41

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/image_753cbf433885c7d22a1f17532a244573.png" alt="Stylized image of a wooden pyramid."/></figure>

In this blog post, we will build a React application through which we can draw interactive shapes with the help of the Canvas API.

We’ll also learn about Canvas HTML elements, the Canvas API, and drawing different shapes with mouse interactions.

Without further ado, let’s get started!

**Prerequisites**

To follow along with this blog post, I would highly recommend that you review the following topics:

- [Basics of React](https://reactjs.org/tutorial/tutorial.html) and[ useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect).
- [React refs](https://reactjs.org/docs/glossary.html#refs)
- [Basic Javascript](https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics)
- [Basics of typescript](https://www.typescriptlang.org/docs/handbook/2/basic-types.html)


## The Canvas HTML Element and the Canvas API

`<canvas>` is an [HTML element](https://code.pieces.app/blog/introduction-to-html) that helps you to draw shapes. You can draw shapes inside a canvas element using the Canvas API provided by your browser. This element acts as a container in which you can draw different shapes. To draw inside a `canvas` element, a context for the element is required. A context is an entity that we can draw shapes on. We can use `getContext` to get the[ CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) that returns the 2-D context.

Let’s look at a simple React `canvas` component that draws a rectangle inside an element.

```
import { useEffect, useRef } from "react";

export const Rectangle = () => {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
 if (canvasRef.current) {
 const ctx = canvasRef.current.getContext("2d");
      ctx?.strokeRect(200, 200, 40, 50);
    }
  }, []);

 return (
 <canvas
      ref={canvasRef}
      width="400"
      height="350"
      style={{ border: "2px solid black" }}
    />
  );
};
```

Here is what the `Rectangle` component will render:

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/image2_23b2664ac95e8b9cc93d84cf6dde5c3f.png" alt="A rendered rectangle drawn with the Canvas API."/></figure>

Let’s take a peek into the above `Rectangle` component:

- The `Rectangle` component returns a `canvas` element whose width and height are set to `400px` and `300px`, respectively.
- We need to make sure that we pass a `ref` to the `canvas` element. This will help us to access the different properties of the `canvas` element.
- Lastly, we want to draw inside the `canvas` element once the component is rendered. To do so, we’ll write a piece of code that gets the `canvas` element’s 2-D context using `getContext("2d")` and uses the above context to draw a rectangle with a function[ strokeRect](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeRect).


Now, with this understanding, let’s dive into drawing some shapes.

## Drawing a Rectangle on the Canvas

In this section, we’re going to talk about drawing a rectangle upon clicking the canvas, as in the gif below:

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/ezgif_com-gif-maker17_a33e09863908ec045743484815004b8d.gif" alt="Gif showing a new rectangle being created with every click."/></figure>

If we think about building an app like this in React or in JS, then we need to consider two important points: First, how we get the cursor’s x and y coordinates for drawing onto the canvas, and second, how we’re going to draw on the mouse click.

Let’s investigate these points further:

### How would we get the cursor’s x and y coordinates so that we can draw it onto the canvas?

- In this scenario, we can simply add an event listener on the `mousemove` event.
- The handler we will attach will simply set the x and y coordinates to the values that are relative to the edges of the given element, like this:


```
const handleCursorMovement = (event) => {
 let rect = event.target.getBoundingClientRect();
 return {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top
    };
  };
```

- The `x-coordinate` is the difference between the viewport's x-coordinate and the distance of the current object from the left edge of the viewport. This will give us the x-coordinate inside the rectangle. In this case, the current object is what we’re hovering over.
- Similarly, we have `y` where it is the difference between the viewport’s y-coordinate and the distance of the current object from the top edge of the viewport. In this case, the current object is what we’re hovering over.


You can read more about `clientX` and `getBoundingClientRect` [here](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) and[ here](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). This is a really useful scenario, and it will be used in most of our examples.

So, let’s wrap this logic into a hook so that we can use it in any component we want.

Copy-paste the below code into a file named `useMousePosition.tsx`

```
import { useEffect, useState } from "react";

const useMousePosition = (
  global: boolean = false
): [{ x: number; y: number }, (event: MouseEvent) => void] => {
 const [mouseCoords, setMouseCoords] = useState<{
    x: number;
    y: number;
  }>({
    x: 0,
    y: 0
  });

 const handleCursorMovement = (event: MouseEvent): void => {
 //@ts-ignore
 let rect = event.target.getBoundingClientRect();
    setMouseCoords({
      x: event.clientX - rect.left,
      y: event.clientY - rect.top
    });
  };
  useEffect(() => {
 if (global) {
 window.addEventListener("mousemove", handleCursorMovement);

 return () => {
 window.removeEventListener("mousemove", handleCursorMovement);
      };
    }
  }, [global]);

 return [mouseCoords, handleCursorMovement];
};

export default useMousePosition;
```

- This hook returns an array that consists of two things: the current cursor coordinates, and a function that handles the setting of these coordinates.
- This function also accepts a parameter called “global.” If set to `true`, this parameter makes sure that a listener is attached to the `mousemove` event over the entire window.
- The default value of the `global` parameter is set to false.


### How are we going to draw on the mouse click?

- In this scenario, we’re going to draw inside the `canvas` whenever the user clicks on the `canvas`.
- To do this, we are going to bind the `canvas` element with a function that executes itself on a click. It’s as simple as adding an `onClick` attribute to it.


To wrap up this scenario, place the following code inside a file named `Square.tsx`:

```
import { useRef } from "react";
import useMousePosition from "./hooks/useMousePosition";

export default function Sqaure() {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);
 const [coords, handleCoords] = useMousePosition(true);

 return (
 <>
 <h1>Sqaure on click</h1>
 <canvas
        ref={canvasRef}
        width="400"
        height="350"
        style={{ border: "2px solid black" }}
        onClick={(e) => {
          handleCoords((e as unknown) as MouseEvent);
          if (canvasRef.current) {
            const ctx = canvasRef.current.getContext("2d");
            ctx?.strokeRect(coords.x, coords.y, 40, 50);
          }
        }}
      ></canvas>
 <button
        onClick={() => {
          if (canvasRef.current) {
            const ctx = canvasRef.current.getContext("2d");
            ctx?.clearRect(0, 0, 400, 350);
          }
        }}
      >
        CLEAR
 </button>
 </>
  );
}
```

The output will look something like below:

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/image4_359a92c552d59aacbd648a8d35cd7f24.jpeg" alt="Several rectangles on a canvas."/></figure>

In this, we also make sure to provide a “clear” button so that the Canvas can be cleared.

As you can see from the above code, we have attached a function on the `onClick` attribute of the canvas:

```
<canvas
        ref={canvasRef}
        width="400"
        height="350"
        style={{ border: "2px solid black" }}
        onClick={(e) => {
          handleCoords((e as unknown) as MouseEvent);
 if (canvasRef.current) {
 const ctx = canvasRef.current.getContext("2d");
            ctx?.strokeRect(coords.x, coords.y, 40, 50);
          }
        }}
      ></canvas>
```

This function will do the following things:

- It will execute the `handleCoords` function that sets the current mouse position coordinates.
- Then it gets the current canvas’s 2-D context.
- Finally, it draws a rectangle using the `strokeRect` function via passing the x and y coordinates of the mouse pointer.


Let’s go ahead and look at what else we can draw with React `canvas`.

## Animate Canvas Objects

In this section of the article, we’re going to talk about the interesting stuff: the animation of the objects present inside the canvas.

Things that we are going to do in this section are:

- Draw the shape on the canvas
- Move the shape on the canvas at a regular interval to get the animation effect


The animation that we are building here is a ball moving continuously in a horizontal direction. Here is a glimpse of it:

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/image1_c54d6c97521ed8487603836e82611a80.gif" alt="A ball moving across a rectangle."/></figure>

To draw a circle, we can use the below code:

```
export const drawCircle = (
  ctx: CanvasRenderingContext2D,
  circleDims: {
    radius: number;
    lineWidth: number;
    strokeStyle: string;
    colorFill?: string;
    startX: number;
    startY: number;
  },
  rectDims: { w: number; h: number } = { w: 400, h: 3500 }
) => {
 const {
    radius,
    strokeStyle,
    startX,
    startY,
    lineWidth,
    colorFill
  } = circleDims;
  ctx?.clearRect(0, 0, rectDims.w, rectDims.h);
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = strokeStyle;

  ctx?.beginPath();
  ctx?.arc(startX, startY, radius, 0, Math.PI * 2, true);
  ctx?.stroke();
 if (colorFill) {
    ctx.fillStyle = colorFill;
    ctx.fill();
  }
};
```

I have wrapped the logic of creating a circle in the function `drawCircle`. The logic of it is below:

```
ctx?.clearRect(0, 0, rectDims.w, rectDims.h);
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = strokeStyle;

  ctx?.beginPath();
  ctx?.arc(startX, startY, radius, 0, Math.PI * 2, true);
  ctx?.stroke();
 if (colorFill) {
    ctx.fillStyle = colorFill;
    ctx.fill();
  }
```

- First, we clear the canvas using the[ clearRect](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/clearRect) function.
- Next, we set the `lineWidth` of the circumference of the circle/ball.
- We also set the `strokeStyle` that sets the color of the border.
- Then, we start to create a new path using the[ beginPath](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/beginPath) function. We make use of the[ arc](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc) method that helps draw circular arcs with the given x-coordinate, y-coordinate, radius, start and end angle.
- Once the `arc` is added as a sub-path, we can draw the arc onto the canvas with the given stroke style using the[ stroke](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/stroke) function.
- Finally, if the `colorFill` is available, then it will fill the path with the available `fillStyle`.


Next, we will make use of this function to create a component that animates the shape.

Create a file named `Circle.tsx` file and place the below code within:

```
import { useEffect, useRef, useState } from "react";
import { drawCircle } from "./utilities";

const Circle = () => {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);
 const [startX, setStartX] = useState<number>(0);
  useEffect(() => {
 if (canvasRef?.current) {
 const ctx = canvasRef.current.getContext("2d");

      requestAnimationFrame(function ball() {
 //@ts-ignore
        drawCircle(ctx, {
          radius: 50,
          lineWidth: 3,
          strokeStyle: "#4F7CAC",
          colorFill: "#4F7CAC",
          startY: 150,
          startX
        });
        setStartX((prevStartX) => prevStartX + 5);
        ctx?.stroke();
 if (startX > 400) {
          setStartX(0);
        }
      });
    }
  }, [startX]);
 return (
    <>
      <h1>Moving Circle</h1>
      <canvas
        ref={canvasRef}
        width="400"
        height="350"
        style={{ border: "2px solid black" }}
      />
    </>
  );
};

export default Circle;
```

Let’s analyze this code a bit:

- The `Circle` component returns a `canvas` element. This is similar to the `Square` component that we saw in the earlier section.
- The aim of this animation is to move the circle continuously in the horizontal direction. So, in this case, we need to make sure we change the x-coordinate. For this, we created a new state variable called `startX`. We’ll look later at how we update `startX` continuously.
- Next, we have a `useEffect`. This `useEffect` gets the 2-D context of the canvas and then executes the function `requestAnimationFrame`.
- [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) is a function that tells the browser it needs to perform an animation. The provided function will be executed before the next repaint. In this case, we are going to pass a function, `ball`, that draws a new circle on every new x-coordinate with the help of `startX`.
- We make use of the `setStartX` update handler that updates the current `startX` by 5.
- Lastly, we check if the `startX` state is crossing the right edge of the canvas. If yes, then we set the value to 0. This ensures that whenever the ball hits the right edge, it can start the animation again from the left edge.


## Moving Shapes with Mouse Events

In this section, we’re going to talk about how we can make the shapes inside the canvas interact with mouse events.

The process that we are going to follow is very similar to what we had in the `Square` component. In the square component, we made use of the `onClick` attribute of the `canvas` element to draw a rectangle on the mouse click event. Similarly, we are going to make use of the `onMouseMove` attribute of the `canvas` element.

Create a file named `TrackingBall.tsx` and paste the below code:

```
import { useEffect, useRef } from "react";
import useMousePosition from "./hooks/useMousePosition";
import { drawCircle } from "./utilities";
const MouseBall = () => {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);
 const [coords, handleCoords] = useMousePosition();

  useEffect(() => {
 if (canvasRef?.current) {
 const ctx = canvasRef.current.getContext("2d");

      requestAnimationFrame(function ball() {
 //@ts-ignore
        drawCircle(ctx, {
          radius: 50,
          lineWidth: 3,
          strokeStyle: "#4F7CAC",
          colorFill: "#4F7CAC",
          startY: coords.y,
          startX: coords.x
        });
      });
    }
  }, [coords.x, coords.y]);
 return (
 <>
 <h1>Tracking ball</h1>
 <canvas
        id="canvas2"
        ref={canvasRef}
        width="400"
        height="350"
        onMouseMove={(e) => {
          handleCoords((e as unknown) as MouseEvent);
        }}
        style={{ border: "2px solid black" }}
      ></canvas>
 </>
  );
};

export default MouseBall;
```

The `MouseBall` component is pretty similar to the `Square` component. The only change is that we are executing the `handleCoords` function inside the `onMouseMove` event. Also, we are making use of the `requestAnimationFrame` function to execute the `ball` handler on the change of `coords.x` and `coords.y`.

The final output will look like the below:

<figure><img src="https://d37oebn0w9ir6a.cloudfront.net/account_32099/image3_a532abd02558e99f2d8e8b8c04186853.gif" alt="A circle that moves with your cursor."/></figure>

## Summary

In this article, we talked about the following things:

- What is a `canvas` HTML element?
- How is a Canvas API used to draw inside the canvas?
- How a Canvas API works with the help of a simple example.
- Drawing rectangles and circle shapes with the Canvas API.
- Building a custom hook to track the position of the cursor.
- How the `requestAnimationFrame` function can be used.
- Lastly, we also saw how we can make use of the mouse events to interact with shapes present inside the canvas.


So in this way, we can make use of the `canvas` HTML element, Canvas API, and React to draw different interactive shapes.