Building a Whiteboard with React and Canvas API. Part 1: First steps.
Tags: javascript, react, showdev, webdev
Published: 2023-05-10 15:25:02

---

series: Building a Whiteboard with React and Canvas API

---

_Before you start reading, you can check the result here: 
[demo](https://my-whiteboard.vercel.app/)_

Table of contents

- [__Overview__](#overview)
- [__First steps__](#first-steps)
- [__Move it__](#move-it)
- [__Writing text__](#writing-text)


## Overview<a name="overview"></a>
#### “Why do we fall? So that we can learn to pick ourselves up”

Hello there! In this series of articles, I'll walk you step by step and together we'll create a whiteboard!

No more words, let's code!

## First steps <a name="first-steps"></a>
#### § In this part we will learn basics about the Canvas API and how to draw.

First, let's create a base skeleton of our application. To make it faster, I'll use the [Create React App](https://github.com/facebook/create-react-app).

> 💡 Please note, that we're going to use the Typescript in this project. No worries if you don't have experience with the Typescript, you'll understand the code and probably will like it ❤️

```bash
yarn create react-app my-whiteboard --template typescript
```
> 💡 You also can use the npm

Now let's change the `App.tsx`, and add some code.

```typescript
// App.tsx

function App() {
	const [context, setContext] = useState<CanvasRenderingContext2D | null>(
		null
	);

	const setCanvasRef = useCallback((element: HTMLCanvasElement) => {
		element.width = window.innerWidth;
		element.height = window.innerHeight;
		const canvasContext = element.getContext('2d');

		if (canvasContext !== null) {
			setContext(canvasContext);
		}
	}, []);

	const onMouseDown = () => {
		console.log('context', context);
	};

	return (
		<div>
			<canvas ref={setCanvasRef} onMouseDown={onMouseDown} />
		</div>
	);
}

export default App;
```

Let's run it.

```bash
yarn start
```
Now if you click in your browser, you'll see the context in the console 🎉


But what it is the context? **Context** - this is the interface, that provides access to the Canvas API. Basically any manipulation that we are doing with the canvas, we are doing with context. In more details you can read about it here [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)

Now let's draw our first pixels ✨

Let's change the method `onMouseDown`.

```typescript
// App.tsx
const onMouseDown = (event: MouseEvent) => {
    if (context) {
        context.fillStyle = '#ffd670';
        context.fillRect(event.pageX, event.pageY, 200, 200);
    }
};
```

Now by clicking in your browser, you'll see the square, and you can draw as many as you can, isn't it awesome? 😎

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/de9aaymi35xqteaujlcq.png)

But let's brake down what's happening here:

1. We're using the `fillStyle` to specify the color to use inside shapes.
2. Method `fillRect` draws a rectangle that is filled according to the current `fillStyle`.

## Architecture.
#### § In this part, we will define the basic architecture of our whiteboard.

Drawing a square is already something we can be proud of, but we need to manipulate it somehow, move it, resize it and etc.

Here we need to understand an important concept of the canvas. Everything we draw on the canvas will stay on the canvas until we don't clean it. All canvas drawings are just drawings and not objects with properties, so we can't manipulate them independently.
Due to this, we need to define the following requirements:

1. Whenever we do changes to a canvas, we have to clear and then redraw the whole canvas.
2. We need to have a canvas object model of everything we have on canvas to be able to redraw it.

How can we implement that? We have a good example in front of us, in the browser.
When the browser renders a page, it creates different layers. A layer might contain a bunch of elements. that can be combined to be shown on the screen.

> 💡 This is not an accurate explanation of how layers work in the browser. But education-wise, let's keep thinking about it in this way.

Presuming this, we can consider that a square we're drawing should be presented as an `object` with properties. The `object` is needed to store information about canvas drawings. And whenever we create (_draw_) a new object(_square_) we should create a new `layer`. Then, as browsers, we should composite these `layers` in one big picture on the canvas.

Summarising all of these requirements, we can have this schema:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6i9ox18zcp8ry0wa3mc3.png)

Let's define the `LayerInterface`, `CanvasObjectInterface` and implement the `Layer` and `CanvasObject` classes.

```typescript
// canvas/layer/type.ts
import { CanvasObjectInterface } from 'canvas/canvas-object/type';

export interface LayerInterface {
	isPointInside(pointX: number, pointY: number): boolean;

	setActive(state: boolean): void;

	isActive(): boolean;

	addChild(child: CanvasObjectInterface<any>): void;

	getChildren(): CanvasObjectInterface<any>[];
}

```

```typescript
// canvas/canvas-object/type.ts

export interface CanvasObjectInterface<T> {
	getOptions(): T;
	setOptions(options: T): void;

	getXY(): number[];
	setXY(x: number, y: number): void;

	getWidthHeight(): number[];
	setWidthHeight(width: number, height: number): void;

	move(movementX: number, movementY: number): void;
}

```
**LayerInterface** - is intended to be a canvas object that contains width, height and coordinates of the layer. Also it contains other canvas objects.

**CanvasObjectInterface** - is intended to be a canvas object that contains width, height and will be drawn on the canvas.

Now we will implement these interfaces: 

```typescript
// canvas/layer/layer.ts

export class Layer extends CanvasObject implements LayerInterface {
	private active = false;
	private children: Array<CanvasObjectInterface> = [];

	setActive(state: boolean) {
		this.active = state;
	}

	isActive() {
		return this.active;
	}

	addChild(child: CanvasObjectInterface) {
		this.children.push(child);
	}

	getChildren(): CanvasObjectInterface[] {
		return this.children;
	}

	isPointInside(pointX: number, pointY: number, padding = 0) {
		const { x, y, w, h } = this.getOptions();

		return (
			pointX > x - padding &&
			pointX < x + w + padding &&
			pointY > y - padding &&
			pointY < y + h + padding
		);
	}
}

```

```typescript
// canvas/canvas-object/canvas-object.ts
import { CanvasObjectInterface } from './type';

export class CanvasObject<T extends BaseDrawOptions>
	implements CanvasObjectInterface<T>
{
	options: T = {
		x: 0,
		y: 0,
		w: 0,
		h: 0,
	} as T;

	constructor(options: T) {
		this.options = { ...options };
	}

	getOptions(): T {
		return this.options;
	}

	setOptions(options: T) {
		this.options = { ...this.options, ...options };
	}

	getXY(): number[] {
		return [this.options.x, this.options.y];
	}

	setXY(x: number, y: number) {
		this.options.x = x;
		this.options.y = y;
	}

	setWidthHeight(width: number, height: number) {
		this.options.w = width;
		this.options.h = height;
	}

	getWidthHeight(): number[] {
		return [this.options.w, this.options.h];
	}

	move(movementX: number, movementY: number) {
		const { x, y } = this.options;
		const layerX = x + movementX;
		const layerY = y + movementY;

		this.setXY(layerX, layerY);
	}
}

```

We've created `Layer` and `CanvasObject` classes, now let's see how they can help us.

Let's update the `App.tsx`.

```typescript
// App.tsx

function App() {
	const [isMousePressed, setMousePressed] = useState<boolean>(false);
	const [context, setContext] = useState<CanvasRenderingContext2D | null>(
		null
	);

	const [layers, setLayers] = useState<Array<Layer>>([]);
	const [selectedLayer, setSelectedLayer] = useState<Layer | null>(null);

	const setCanvasRef = useCallback((element: HTMLCanvasElement) => {
		element.width = window.innerWidth;
		element.height = window.innerHeight;
		const canvasContext = element.getContext('2d');

		if (canvasContext !== null) {
			setContext(canvasContext);
		}
	}, []);

	const reDraw = () => {
		if (context) {
			context?.clearRect(0, 0, window.innerWidth, window.innerHeight);
			layers.forEach((layer) => {
				const children = layer.getChildren();

				children.forEach((child) => {
					const options = child.getOptions();
					if (layer.isActive()) {
						context.fillStyle = '#70d6ff';
					} else {
						context.fillStyle = '#ffd670';
					}

					context.fillRect(
						options.x,
						options.y,
						options.w,
						options.h
					);
				});
			});
		}
	};

	useEffect(() => {
		reDraw();
	}, [layers]);

	const onMouseDown = (event: MouseEvent) => {
		setMousePressed(true);

		if (context) {
			const detectedLayer = layers.find((layer) =>
				layer.isPointInside(event.pageX, event.pageY)
			);

			if (detectedLayer) {
				if (selectedLayer) {
					selectedLayer.setActive(false);
				}

				detectedLayer.setActive(true);
				setSelectedLayer(detectedLayer);
				reDraw();
			} else {
				selectedLayer?.setActive(false);
				setSelectedLayer(null);

				const options = {
					x: event.pageX,
					y: event.pageY,
					w: 200,
					h: 200,
				};
				const rect = new CanvasObject(options);
				const layer = new Layer(options);
				layer.addChild(rect);
				setLayers([...layers, layer]);
			}
		}
	};

	return (
		<div>
			<canvas
				ref={setCanvasRef}
				onMouseDown={onMouseDown}
			/>
		</div>
	);
}
```

The code above will be triggered on the mouse down event and the method `onMouseDown` will search for a layer using the method `isPointInside`, which we implemented for the `Layer` class.


🎉 Now we can draw and select squares 🎉

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p5yf2boii69yv3m1aypg.gif)

## Move it <a name="#move-it"></a>
#### § In this part, we create a mechanism of moving canvas elements by mouse.

We can draw, we can select, next we'll add the movement functionality.

First, we will update the Layer class and add this code:

```typescript
// canvas/layer/layer.ts
export class Layer extends CanvasObject implements LayerInterface {

	// rest of the code ...

	move(movementX: number, movementY: number) {
		super.move(movementX, movementY);
		this.moveChildrenAccordingly(movementX, movementY);
	}

	moveChildrenAccordingly(movementX: number, movementY: number) {
		for (const child of this.children) {
			child.move(movementX, movementY);
		}
	}
}

```

Update the `App.tsx` file and add new methods, `onMouseUp` and `onMouseMove`.

```typescript
function App() {
	// rest of the code ...

	const onMouseMove = (event: MouseEvent) => {
		if (isMousePressed && selectedLayer) {
			selectedLayer.move(event.movementX, event.movementY);
			reDraw();
		}
	};

	const onMouseUp = () => {
		setMousePressed(false);
	};

	return (
		<div>
			<canvas
				ref={setCanvasRef}
				onMouseDown={onMouseDown}
				onMouseUp={onMouseUp}
				onMouseMove={onMouseMove}
			/>
		</div>
	);
}

```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8ztev93p8hxi5adddy9.gif)

🎉 We can draw, select and move objects on the canvas  🎉

## Writing text <a name="#writing-text"></a>
#### § In this part, we learn how to draw text in the canvas.

Let's make the stickers a bit more useful by adding text on them 😎

We're going to introduce a `CanvasText` object, it will contain text specific information: text, font size, color and etc.


```typescript
// canvas/canvas-text/type.ts
export interface TextDrawOptions extends BaseDrawOptions {
	text: string;
	color: string;
	fontSize: number;
}

// canvas/canvas-text/canvas-text.ts
import { CanvasObject } from 'canvas/canvas-object';
import { TextDrawOptions } from './type';

export class CanvasText extends CanvasObject<TextDrawOptions> {
	constructor(options: TextDrawOptions) {
		super(options);
	}
}

```

Now we need to draw text, for this purpose, the canvas rendering context provides the `fillText` method to render text.

We will change the `reDraw` method by adding the `fillText`.

Also, we need to mark the `CanvasText` and `CanvasObject` to be able to choose between the `fillRect` and `fillText` methods and understand what should be used to draw objects.

For that purpose, we will add a new property `type` to the `CanvasObjectInterface` and will update its implementation.

```typescript
// canvas/enums.ts
export enum TYPES {
	TEXT,
	RECT,
}

// canvas/canvas-object/type.ts
export interface CanvasObjectInterface<T> {
	// rest of the code ...

	getType(): string;
	setType(type: string): void;
}

// canvas/canvas-object/canvas-object.ts
import { TYPES } from 'canvas/enums';

export class CanvasObject<T extends BaseDrawOptions>
	implements CanvasObjectInterface<T>
{
	// rest of the code ...
	private type = TYPES.RECT;

	// rest of the code ...
	setType(type: TYPES) {
		this.type = type;
	}

	getType(): TYPES {
		return this.type;
	}
}



```

And small changes for the `CanvasObject`.

```typescript
export class CanvasText extends CanvasObject<TextDrawOptions> {
	constructor(options: TextDrawOptions) {
		super(options);
		this.setType(TYPES.TEXT); // we're setting the object type
	}
}
```


Next let's change the `reDraw` method.

```typescript
	const reDraw = () => {
		if (context) {
			context?.clearRect(0, 0, window.innerWidth, window.innerHeight);
			layers.forEach((layer) => {
				const children = layer.getChildren();

				children.forEach((child) => {
					if (layer.isActive()) {
						context.fillStyle = '#70d6ff';
					} else {
						context.fillStyle = '#ffd670';
					}

					const type = child.getType();

					if (type === TYPES.RECT) {
						const options: BaseDrawOptions = child.getOptions();

						context.fillRect(
							options.x,
							options.y,
							options.w,
							options.h
						);
					}

					if (type === TYPES.TEXT) {
						const options: TextDrawOptions = child.getOptions();

						context.save();
						context.fillStyle = options.color;
						context.font = `${options.fontSize}px monospace`;
						context.fillText(
							options.text,
							options.x,
							options.y,
							options.w
						);
						context.restore();
					}
				});
			});
		}
	};
```

Well, let's now focus on what is happening in the code snippet above. We're checking the current `type` if it equals to `TEXT` and we start rendering the text object. You can see new context methods that we didn't see before, `save()` and `restore()`.
We're using this method to **save** the current context state and **restore** it later. 
What does it mean? We're using the `fillStyle` to set the font's color, but by changing this property, we will change **NOT ONLY** the font color but also change the current canvas color.
For example, if we try to call `fillRect` right after the `fillText`, without using `save()` and `restore()`, it will render a square with the same color that we applied before for text.
Another property we've used here is `font`, it specifies the current text style. This string uses the same syntax as the CSS font specifier.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zpr9la0zhcaf991wby6q.gif)

🎉 Now we can draw text on the canvas! 🎉

We did our first steps in building our own white board! And it wasn't so difficult right?

In the next part we will improve it!

P.S. I'm working on a second part now. If you like this article, please share you feedback in the comments, thanks in advance!