Create a drawing app with HTML and JavaScript
Do you want to learn how to create a drawing app with HTML and JavaScript? In this post, we will do exactly that, and we will build upon the basics that we discussed in my last post (here).
The drawing app consists of a color selection and multiple interesting tools, like the polygon or rectangle tool!
To create the app, we will follow the following steps:
- Setup the project
- Create the interface for the drawing app
- Create the tools for the drawing app
- Create the colors for the drawing app
So let’s get started!
Setup the project
For this project, we will use vite and tailwind. You do not need them, but they ease the process of developing the application. Vite is used as a development environment and a bundler for the final application. Tailwind CSS is a CSS framework that allows you to create User Interfaces faster than basic CSS, as I showed in this post here.
I also created a script that I use to set up my projects. You can download that for subscribing to my newsletter π
Need help or want to share feedback? Join my discord community!
If you decide to do that, you can skip to this section.
[convertkit form=3187272]
If this guide is helpful to you and you like what I do, please support me with a coffee!
Setup without Script
npm init vite drawing-app
#select vanillacd drawing-app
npm i
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
- Open
tailwind.config.js
and update content to this:
module.exports = {
content: [
"./index.html"
],
theme: {
extend: {},
},
plugins: [],
}
- Open
style.css
and update content to this:
@tailwind base;
@tailwind components;
@tailwind utilities;
npm run dev
- Open http://localhost:3000 in your browser to see the site
With that, we set up Tailwind CSS. Now we will make some more changes that are project-specific. First, we create multiple folders. The folder assets
for assets, css
for CSS files and lastly js
for JavaScript files. According to that, we move the style.css file into the folder css
and the file main.js into the folder js
. Additionally, we will move the favicon.svg file into the assets
folder. Lastly, we will update the contents of index.html
to:
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drawing App</title>
</head>
<body>
<script type="module" src="./js/main.js"></script>
</body>
</html>
and the contents of css/style.css
to:
import '../css/style.css'
With that, we have the following final structure:
drawing-app
βββ node_modules
βββ assets
β βββ favicon.svg
βββ css
β βββ style.css
βββ js
β βββ main.js
βββ .gitignore
βββ index.html
βββ package.json
βββ package-lock.json
βββ postcss.config.js
βββ tailwind.config.js
To finish the setup, continue reading here.
Setup with Script
To set up the project with the script, do the following steps:
- Execute
sh setup-tw-vite.sh drawing-app
cd drawing app && echo -e "import '../css/style.css'" >> js/main.js
- Update index.html to:
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drawing App</title>
</head>
<body>
<script type="module" src="./js/main.js"></script>
</body>
</html>
For both
For the project, we need multiple different graphics. Therefore download the following zip and unzip it inside the folder assets.
The resulting structure should look like this:
assets
βββ big.svg
βββ clear.svg
βββ favicon.svg
βββ medium.svg
βββ path.svg
βββ pen.svg
βββ polygon.svg
βββ small.svg
βββ square.svg
Create the interface for the drawing app
Now that we finished setting up our project, we can start creating the drawing app! For that, we first have to create the basic HTML structure:
<body>
<canvas id="canvas"></canvas>
<div>
<div id="colors">
<button id="black"></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
</div>
<div id="settings">
<button id="pen">
<img src="./assets/pen.svg" alt="pen">
</button>
<button id="small">
<img src="./assets/small.svg" alt="small">
</button>
<button id="medium">
<img src="./assets/medium.svg" alt="medium">
</button>
<button id="big">
<img src="./assets/big.svg" alt="big">
</button>
<button id="path">
<img src="./assets/path.svg" alt="path">
</button>
<button id="polygon">
<img src="./assets/polygon.svg" alt="polygon">
</button>
<button id="rect">
<img src="./assets/square.svg" alt="square">
</button>
<button id="clear">
<img src="./assets/clear.svg" alt="clear">
</button>
</div>
</div>
<script type="module" src="./js/main.js"></script>
</body>
and then add Tailwind CSS classes to the markup to achieve the wanted result:
<body class="flex flex-col items-center justify-center h-full py-8 px-8 w-full lg:w-4/5 xl:w-3/4 2xl:w-2/3 mx-auto">
<canvas id="canvas" class="w-full max-h-full aspect-video border-2 border-black border-solid mb-8"></canvas>
<div class="w-full flex flex-row justify-between">
<div id="colors" class="grid grid-rows-2 grid-cols-4 gap-1 justify-self-start">
<button id="black" class="bg-black setting"></button>
<button class="bg-red-500 setting"></button>
<button class="bg-green-500 setting"></button>
<button class="bg-blue-500 setting"></button>
<button class="bg-white setting"></button>
<button class="bg-yellow-500 setting"></button>
<button class="bg-orange-500 setting"></button>
<button class="bg-violet-500 setting"></button>
</div>
<div id="settings" class="grid grid-rows-2 grid-cols-4 gap-1 justify-self-end">
<button id="pen" class="setting tool">
<img src="./assets/pen.svg" alt="pen">
</button>
<button id="small" class="setting size">
<img src="./assets/small.svg" alt="small">
</button>
<button id="medium" class="setting size">
<img src="./assets/medium.svg" alt="medium">
</button>
<button id="big" class="setting size">
<img src="./assets/big.svg" alt="big">
</button>
<button id="path" class="setting tool">
<img src="./assets/path.svg" alt="path">
</button>
<button id="polygon" class="setting tool">
<img src="./assets/polygon.svg" alt="polygon">
</button>
<button id="rect" class="setting tool">
<img src="./assets/square.svg" alt="square">
</button>
<button id="clear" class="setting">
<img src="./assets/clear.svg" alt="clear">
</button>
</div>
</div>
<script type="module" src="./js/main.js"></script>
</body>
As you may see, we added the class setting
to all the different colors and tools to make them look the same. But to make the class do something, we need to add custom classes to css/style.css
like this:
@layer components {
.setting {
@apply h-8 lg:h-12 aspect-square border-2 border-solid border-black;
}
.setting > img {
@apply h-full w-full;
}
.setting.selected {
@apply border-4;
}
#black.setting.selected {
@apply border-4 border-white outline outline-1 outline-black;
}
.setting.selected.hide-select {
@apply border-2;
}
}
With that, our drawing app already looks like we want it to look, but there is no functionality yet. That is what we will cover in the next section!
Create the tools for the drawing app
For the drawing app, we will create multiple different tools. A pen, a path, a polygon, and a rectangle tool! I orient myself on the HTML Canvas basics we discussed in this post here to create them.
Each tool is based on different mouse events. To change between the different settings, we will create a mode switcher in the last part of this section.
But before we can create the tools, we need to initialize the canvas with the following lines (add them to main.js):
import '../css/style.css'
const container = document.getElementById("canvas-container");
const canvas = document.getElementById("canvas");
const width = 1920;
const height = 1080;
// context of the canvas
const context = canvas.getContext("2d");
context.imageSmoothingEnabled = true;
// resize canvas (CSS does scale it up or down)
canvas.height = height;
canvas.width = width;
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect(),
scaleX = canvas.width / rect.width,
scaleY = canvas.height / rect.height;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY
}
}
Pen Tool
As we already saw in the basics post, we need three different events for the pen tool to work. These are mousedown
, mouseup
, and mousemove
. For each of the different events, we need a function that can be executed.
// --- Pen ---
let drawing = false;
function startDraw(e) {
drawing = true;
context.beginPath();
draw(e)
}
function endDraw(e) {
drawing = false;
}
function draw(e) {
if (!drawing) return;
let { x, y } = getMousePos(canvas, e);
context.lineTo(x, y);
context.stroke();
// for smoother drawing
context.beginPath();
context.moveTo(x, y);
}
To test if the pen tool works, you can add the following lines:
window.addEventListener("mousedown", startDraw);
window.addEventListener("mouseup", endDraw);
window.addEventListener("mousemove", draw);
After you see that it worked, I suggest you have to remove it.
Sizes
With this functionality, you can switch between the three different sizes (small, medium, and big). We will first set the new size and then handle the class of the selected size to show that it is selected!
const sizes = {
'small': 5,
'medium': 10,
'big': 15
}
function setSize(e, size) {
context.lineWidth = size;
selectSize(e);
}
function selectSize(e) {
if (mode === 'rect')
return;
const sizes = document.getElementsByClassName("size");
for (const size of sizes) {
size.classList.remove('selected');
}
if (e === undefined)
return;
e.target.parentElement.classList.add('selected');
}
The changed size affects the pen, path, and polygon tool!
Path Tool
The path tool is used to draw a straight line from point a to point b. Thus we only need two functions for the mousedown and mouseup event. When you press your mouse down, you set the start position, and when you lift your finger, you set the end position and draw a line between the two!
// --- Path ---
function startPath(e) {
drawing = true;
context.beginPath();
draw(e)
}
function endPath(e) {
drawing = false;
let { x, y } = getMousePos(canvas, e);
context.lineTo(x, y);
context.stroke();
}
To test if the path tool works, you can add the following lines (remove it afterward):
window.addEventListener("mousedown", startPath);
window.addEventListener("mouseup", endPath);
Polygon Tool
With the polygon tool, you create a closed polygon with multiple edges. It works as follows:
- Click to set the start position
- Click to set a node position (repeat as often as you want)
- After 1 second of no click, the polygon closes
Therefore we again need only two functions for mousedown and mouseup.
// --- Polygon ---
let poly = false;
let polyTimeout = undefined;
function startPolygon(e) {
if (e.target.id !== 'canvas')
return;
drawing = true;
if (poly) {
polygon(e);
}
else {
context.beginPath();
draw(e);
}
poly = true;
}
function endPolygon(e) {
if (!poly)
return;
polyTimeout = setTimeout(() => {
drawing = false;
context.closePath();
context.stroke();
poly = false;
}, 1000);
}
function polygon(e) {
if (!drawing) return;
clearTimeout(polyTimeout);
let { x, y } = getMousePos(canvas, e);
context.lineTo(x, y);
context.stroke();
}
To test if the polygon tool works, you can add the following lines (remove them afterward):
window.addEventListener("mousedown", startPolygon);
window.addEventListener("mouseup", endPolygon);
Rectangle Tool
Lastly, we have the rectangle tool. It is used to create rectangles on the canvas, and it works similarly to the path tool. But instead of a straight line between the two points, it creates a rectangle between them.
// --- Rect ---
let start = {}
function startRect(e) {
start = getMousePos(canvas, e);
}
function endRect(e) {
let { x, y } = getMousePos(canvas, e);
context.fillRect(start.x, start.y, x - start.x, y - start.y);
}
To test if the polygon tool works, you can add the following lines (remove them afterward):
window.addEventListener("mousedown", startRect);
window.addEventListener("mouseup", endRect);
Clear the canvas
The last tool in our toolbar is clear. It is used to reset the canvas.
// --- Clear ---
function clearCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
Select Tool/Mode
Lastly, let’s add functionality to swap between the different tools. Therefore we need to remove the events of the current tool and assign the ones of the new tool. In addition to that, we also need to change some of the classes to visualize the selected tools!
let mode = 'draw';
function selectMode(e, newMode) {
const tools = document.getElementsByClassName("tool");
for (const tool of tools) {
tool.classList.remove('selected');
}
const size = document.querySelector(".size.selected");
if (size !== null)
{
size.classList.remove('hide-select');
if (newMode === 'rect')
size.classList.add('hide-select');
}
e.target.parentElement.classList.add('selected');
mode = newMode;
}
const activeEvents = {
"mousedown": undefined,
"mouseup": undefined,
"mousemove": undefined
};
function setMode(e, mode) {
for (const event in activeEvents) {
window.removeEventListener(event, activeEvents[event]);
activeEvents[event] = undefined;
}
switch (mode) {
case 'pen':
window.addEventListener("mousedown", startDraw);
window.addEventListener("mouseup", endDraw);
window.addEventListener("mousemove", draw);
activeEvents['mousedown'] = startDraw;
activeEvents['mouseup'] = endDraw;
activeEvents['mousemove'] = draw;
break;
case 'path':
window.addEventListener("mousedown", startPath);
window.addEventListener("mouseup", endPath);
activeEvents['mousedown'] = startPath;
activeEvents['mouseup'] = endPath;
break;
case 'polygon':
window.addEventListener("mousedown", startPolygon);
window.addEventListener("mouseup", endPolygon);
activeEvents['mousedown'] = startPolygon;
activeEvents['mouseup'] = endPolygon;
break;
case 'rect':
window.addEventListener("mousedown", startRect);
window.addEventListener("mouseup", endRect);
activeEvents['mousedown'] = startRect;
activeEvents['mouseup'] = endRect;
break;
default:
break;
}
selectMode(e, mode);
}
The created function does not yet work with the buttons. We will take care of that after creating the color selection!
Create the colors for the drawing app
The next thing we need is to create the color selection of the app. We create a colors.js file containing a JSON with the different color codes needed (they represent the tailwind -500 colors that we also assigned the buttons).
export const colors = {
"black": "#000000",
"white": "#ffffff",
"red": "#ef4444",
"green": "#22c55e",
"blue": "#3b82f6",
"yellow": "#eab308",
"orange": "#f97316",
"violet": "#8b5cf6"
}
Next, we need to import this into our main.js file like this:
//...
import { colors } from "./colors";
//...
And with that, we can create the color selection. For that, we first change the drawing color and then apply the selected style to the selected color:
function setColor(e, color) {
context.strokeStyle = colors[color];
context.fillStyle = colors[color];
selectColor(e);
}
function selectColor(e) {
const colors = document.getElementById("colors").children;
for (const color of colors) {
color.classList.remove('selected');
}
e.target.classList.add('selected');
}
We created all the functions with that, and there is only one step left. To initialize the whole thing.
Initialize the drawing app
Inside this step, we will assign the different functions to the correct buttons and set the default settings for the drawing app.
function initialize() {
const colorButtons = document.getElementById('colors').children;
for (const colorButton of colorButtons) {
colorButton.addEventListener('click', (e) => { setColor(e, colorButton.classList.value.replace(/bg-(\w*).*/, '$1'))} );
}
const tools = document.getElementsByClassName('tool');
for (const tool of tools) {
tool.addEventListener('click', (e) => { setMode(e, tool.id)} );
}
const sizeButtons = document.getElementsByClassName('size');
for (const sizeButton of sizeButtons) {
sizeButton.addEventListener('click', (e) => { setSize(e, sizes[sizeButton.id])} );
}
document.getElementById('clear').addEventListener('click', clearCanvas);
// set default settings
context.lineCap = 'round';
document.getElementById('small').firstElementChild.click();
document.getElementById('pen').firstElementChild.click();
document.getElementById('black').click();
}
initialize();
With that, we completed the drawing app, and you can create a final version of it by running:
npm run build
Conclusion
We created a drawing app with basic HTML and JavaScript from scratch in this post! For that, we learned how to change settings like the size or color, and we learned how to create different tools based on the functionality the HTML Canvas provides!
You can find the final app in this repository here.
I hope you enjoyed this post and it was helpful for you! If you have any questions feel free to ask through the chat window or send me a message on Twitter @programonaut (or email etc.).
In case you are interested in more posts like this, consider subscribing to my newsletter, where I publish a monthly update on all the new posts!
[convertkit form=2303042]