Skip to content

Chapter 2 - Create a drawing board with Konva.js

In this chapter, you will install Konva.js as the drawing library and make your project a board for drawing.

Info

All the following steps will be executed in the frontend directory.

In a new terminal, you can switch to the frontend directory with the following command.

In a terminal, execute the following command(s).
cd frontend

Steps

Install Konva.js

Install Konva.js with the following command.

In a terminal, execute the following command(s).
npm install --save react-konva konva

Create the Sketch component

To create a canvas and draw in your web application, you could simply implement it in the index.tsx file. But the complexity of this drawing application will soon begin to grow bigger and bigger. So it's better to separate some parts of your code. And how convenient! React proposes something. Something called Components.

As Next.js documentation explains:

Quote

The user interface can be broken down into smaller building blocks called Components.

Components allow you to build self-contained, reusable snippets of code. If you think of components as LEGO bricks, you can take these individual bricks and combine them together to form larger structures. If you need to update a piece of the UI, you can update the specific component or brick.

This modularity allows your code to be more maintainable as it grows because you can easily add, update, and delete components without touching the rest of our application.

Before implementing the Sketch Component, we need to think the data structure of a drawing. What is a drawing? It's an array of array of points. We could represent it like that:

1
2
3
4
5
6
[
	[{ x: 21, y: 69 }, { x: 100, y: 98 }, { x: 82, y: 0 } ],
	[{ x: 1, y: 54 }, { x: 90, y: 80 } ],
	[{ x: 100, y: 98 }, {x: 34, y: 23} ],
	[{ x: 13, y: 38 }, { x: 44, y: 8 }, { x: 84, y: 90 }, { x: 6, y: 17 }]
]

But for Konva.js a line is not represented with x and y, it's just an array of number, and the one in odd indexes are x and the one in even indexes are y. So the data will be presented like that.

1
2
3
4
5
6
[
	[21, 69, 100, 98, 82, 0],
	[1, 54, 90, 80],
	[100, 98, 34, 23],
	[13, 38, 44, 8, 84, 90, 6, 17]
]

The Sketch component implements a State in which the data of line will be stored. Konva needs the data to be in a State to be responsive and react when a user move the mouse over the canvas.

As this project uses TypeScript, so you can define this kind of data with types to ensure code correctness:

  • The LineData type will be an array of number (number[])
  • The LinesData type will be an array of LineData (LineData[])
  • The State type is an alias for the LinesData type for terminology purposes
1
2
3
type LineData = number[];
type LinesData = LineData[];
type State = LinesData;

Now, implement the Sketch component.

frontend/src/components/sketch.tsx
import React from 'react';
import Konva from 'konva';
import { Stage, Layer, Line } from 'react-konva';

type Point = {
	x: number,
	y: number,
};

type LineData = number[];
type LinesData = LineData[];
type State = LinesData;

export const Sketch = () => {
	const [lines, setLines] = React.useState<State>([]); // (1)!
	const isDrawing = React.useRef(false); // (2)!

	const getPointFromMouseEvent = (// (3)!
		mouseEvent: Konva.KonvaEventObject<MouseEvent>
	) => {
		return mouseEvent.target.getStage()?.getPointerPosition() ?? { x: 0, y: 0 };
	};

	const addFirstPoint= (point: Point) => {// (4)!
		setLines([...lines, [point.x, point.y] ]);
	}

	const addPoint = (point: Point) => {// (5)!
		const linesCopy = [...lines];
		let [lastLine] = linesCopy.splice(-1);
		setLines([...linesCopy, [...lastLine, point.x, point.y]]);
	}

	const handleMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {// (6)!
		isDrawing.current = true;
		const point = getPointFromMouseEvent(e);
		addFirstPoint(point);
	};

	const handleMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {// (7)!
		if (!isDrawing.current) {
			return;
		}
		const point = getPointFromMouseEvent(e);
		addPoint(point);
	};

	const handleMouseUp = () => {
		isDrawing.current = false;
	};

	return (
		<div>
			<Stage // (8)!
				width={window.innerWidth}
				height={window.innerHeight}
				onMouseDown={handleMouseDown}
				onMousemove={handleMouseMove}
				onMouseup={handleMouseUp}
			>
				<Layer>
					{lines.map((line, i) => ( // (9)!
						<Line // (10)!
							key={i}
							points={line}
							stroke="#df4b26"
							strokeWidth={5}
							tension={0.5}
							lineCap="round"
							lineJoin="round"
						/>
					))}
				</Layer>
			</Stage>
		</div>
	);
};
  1. Initialize the State with an empty array [] and return a getter (to read the state) and a setter (to write in the state).
  2. Create a reference that will persist for the full lifetime of the component to know if we're currently drawing or not. It would also be possible to use a useState for this, but it would be less performant because it would trigger a re-render every time the value changes.
  3. Function that convert a mouse event into a point.
  4. Add a new line with the coordinates of the first point in the State.
  5. Add the coordinates of a new point in the last line of the State.
  6. When the user press the mouse down, it starts drawing a new line.
  7. When the user moves the mouse, and if it's already drawing, it continues to draw a line.
  8. This is the component where the drawing are displayed.
  9. Iterate over the lines array and get each line individually.
  10. This is the component that draws lines.

Konva.js can only be run on a browser. As Next.js can process data on the server side (without a browser) with SSR (Server Side Rendering), you need to import the Sketch component dynamically explicitly disabling SSR.

Update the main page to use the Sketch component without SSR.

frontend/src/pages/index.tsx
import dynamic from 'next/dynamic';

const Sketch = dynamic(
	() => import('../components/sketch').then((mod) => mod.Sketch),
	{
		ssr: false, // (1)!
	},
);

export default function Home() {
	return (
		<Sketch />
	)
}
  1. SSR is disabled for this component, it will only be available on the browser.

Check the results

Your working directory should looks like this.

.
├── .devcontainer
│   ├── devcontainer.json
│   ├── Dockerfile
│   └── npm-global-without-sudo.sh
└── frontend
    ├── node_modules
       └── ...
    ├── .next
    │   └── ...
    ├── public
    │   ├── favicon.ico
    │   ├── next.svg
    │   ├── thirteen.svg
    │   └── vercel.svg
    ├── src
    │   ├── components
    │   │   └── sketch.tsx
    │   ├── pages
    │   │   ├── api
    │   │   │   └── hello.ts
    │   │   ├── _app.tsx
    │   │   ├── _document.tsx
    │   │   └── index.tsx
    │   └── styles
    │       ├── globals.css
    │       └── Home.module.css
    ├── .eslintrc.json
    ├── .gitignore
    ├── next.config.js
    ├── next-env.d.ts
    ├── package.json
    ├── package-lock.json
    ├── README.md
    └── tsconfig.json

Start the application and access http://localhost:3000. You should see a blank page.

If you draw on the board with your mouse and left click, you should be able to draw red lines to your wish!

Summary

Congrats! You have a Next.js + Konva.js application you can draw on it!

Go further

Are you able to change the color and the size of the line? Expand the next component to see the answer!

Show me the answer!

Update the Sketch to set the color and the size of the lines. The color can be any valid Web colors.

frontend/src/components/sketch.tsx
return (
	<div>
		<Stage
			width={window.innerWidth}
			height={window.innerHeight}
			onMouseDown={handleMouseDown}
			onMousemove={handleMouseMove}
			onMouseup={handleMouseUp}
		>
			<Layer>
				{lines.map((line, i) => (
					<Line
						key={i}
						points={line}
						stroke="DarkOrchid"
						strokeWidth={8}
						tension={0.5}
						lineCap="round"
						lineJoin="round"
					/>
				))}
			</Layer>
		</Stage>
	</div>
);