In this tutorial we will create a simple drawing app in the browser. To do that we will use vanilla JS and the Canvas API.
After this tutorial you’ll have a great overview of the canvas API and event handling in javascript.
Video tutorial
If you would watch a detailed step-by-step video instead you can check out the video I made covering this project on my Youtube Channel:
HTML markup
We’ll start by wrapping the whole app into a section
with the class of container
. This will be used to align the toolbar and the drawing board.
Inside that we create a div
which will hold our toolbar. I also set an id of toolbar
for it so it will be easier to work with it in javascript.
Inside the toolbar we’ll add a title for our app in an h1
. Below that we’ll add two input fields: one for color and one for the with of the line. For the color input I add the id of stroke
as it will define the color of the stroke and for the number input I’ll add the id of lineWidth
. Don’t forget to add the corresponding labels for these input fields. Lastly I’ll add a button with the id of clear
and this will be used to clear the drawing board.
The next thing we have to add in our html is the actual drawing board. It will be a canvas
element, but for layouting purposes we’ll wrap it into a div
.
Lastly we need to add the script tag for our script at the bottom of the body
.
<section class="container">
<div id="toolbar">
<h1>Draw.</h1>
<label for="stroke">Stroke</label>
<input id="stroke" name='stroke' type="color">
<label for="lineWidth">Line Width</label>
<input id="lineWidth" name='lineWidth' type="number" value="5">
<button id="clear">Clear</button>
</div>
<div>
<canvas id="drawing-board"></canvas>
</div>
</section>
<script src="./index.js"></script>
Add styles with CSS
I’ll start by removing any browser defined paddings and margins. Also set the height of the body to 100% and remove the scrollbar with overflow: hidden.
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
color: white;
}
For the title I’ll add a gradient text color.
h1 {
background: #7F7FD5;
background: -webkit-linear-gradient(to right, #91EAE4, #86A8E7, #7F7FD5);
background: linear-gradient(to right, #91EAE4, #86A8E7, #7F7FD5);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
We’ll also make the container 100% height, set the display to flex.
.container {
height: 100%;
display: flex;
}
For the toolbar we will use flexbox with column direction. The width of the toolbar will have a fixed width of 70px. We’ll add some spacing with 5px of padding and set a dark background for it.
For the toolbar elements I apply some basic styling. Feel free to copy these styles 👇
#toolbar {
display: flex;
flex-direction: column;
padding: 5px;
width: 70px;
background-color: #202020;
}
#toolbar * {
margin-bottom: 6px;
}
#toolbar label {
font-size: 12px;
}
#toolbar input {
width: 100%;
}
#toolbar button {
background-color: #1565c0;
border: none;
border-radius: 4px;
color:white;
padding: 2px;
}
Implementing the javascript part.
First we’ll save references for the toolbar and the drawing board (canvas).
const canvas = document.getElementById('drawing-board');
const toolbar = document.getElementById('toolbar');
Next we have to get the context of the canvas. We’ll use this context to draw on the canvas. We can do that by calling the getContext
method of the canvas. We’ll draw in 2D so we have to provide that as a parameter.
const ctx = canvas.getContext('2d');
In the next step we’ll gather the offsets (distance between the canvas edges to the edge of the viewport) and save them. In this case the top offset will be 0px as the canvas takes the full height of the viewport and the left offset will be 70px as we have a fixed width sidebar on the left. Next we will calculate and set the height and the width of the canvas by subtracting the offsets from the viewport’s width and height.
const canvasOffsetX = canvas.offsetLeft;
const canvasOffsetY = canvas.offsetTop;
canvas.width = window.innerWidth - canvasOffsetX;
canvas.height = window.innerHeight - canvasOffsetY;
Now we will set some global variables. The isPainting
variable will reflect whether we are currently drawing or not. We’ll set a basic line width of 5 px. Lastly we’ll declare two variables (startX & startY) which will hold the coordinates of the point which we started the drawing from.
let isPainting = false;
let lineWidth = 5;
let startX;
let startY;
Let’s start to add event listeners now. First we will add a click event listener to the toolbar. If the e.target.id
is clear (so the clear button was clicked) then we’ll call the clearRect
function and provide it the width and height of the canvas. What this method will do is esentially set every pixel of the canvas to white inside the provided width and height values (so in this case the whole canvas).
toolbar.addEventListener('click', e => {
if (e.target.id === 'clear') {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
});
Next we will handle the input changes for the line width and the drawing color. We’ll use event delegation in this case. So instead of defining separate event handlers for each input field, we’ll add only one event listener to the parent element and handle the events from there. We can differentiate which input field was changed by checking the value of e.target
. If the color was changed we’ll set the strokeStyle
of the canvas context, if the lineWidth was changed then we update the value of the global lineWidth
variable with the new value.
toolbar.addEventListener('change', e => {
if(e.target.id === 'stroke') {
ctx.strokeStyle = e.target.value;
}
if(e.target.id === 'lineWidth') {
lineWidth = e.target.value;
}
});
Next we’ll implement the drawing controls. When the mousedown event happens (the user clicks and holds the mouse button down) we’ll set the isPainting
variable to true and set the current mouse position’s coordinates into startX
and startY
.
If the user releases the mouse button, then we’ll set isPainting
to false and call the stroke
method of the context to colorize the already drawn path. We also have to call the beginPath
method to close the path that the user drawn so far. We have to do this because if the user wants to draw another line it would start from this position, and this is not something that we want.
Lastly we’ll add an event listener to the mousemove event. When the user moves the mouse we’ll call the draw function which we’ll implement next.
canvas.addEventListener('mousedown', (e) => {
isPainting = true;
startX = e.clientX;
startY = e.clientY;
});
canvas.addEventListener('mouseup', e => {
isPainting = false;
ctx.stroke();
ctx.beginPath();
});
canvas.addEventListener('mousemove', draw);
In the draw
function we’ll first check the value of the isPainting
variable if it is false we are not drawing so we’ll just simply call return.
Next we’ll set the line width to take the value from the global variable and set the lineCap to round. After this we’ll draw a line by calling the lineTo
method with the current mouse position’s coordinates. One thing you have to be careful is to subtract the offset from the X coordinate because otherwise the drawn line would be offsetted with the width of the sidebar (70px). Lastly we only have to call the stroke
method to give the line the color that we selected.
const draw = (e) => {
if(!isPainting) {
return;
}
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineTo(e.clientX - canvasOffsetX, e.clientY);
ctx.stroke();
}
And this is it now you have a working drawing app!
If you stuck at any point you can watch the video or you can take a look at the source code on Codepen.
Where can you learn more from me?
I create education content covering web-development on several platforms, feel free to 👀 check them out.
I also create a newsletter where I share the week’s or 2 week’s educational content that I created. No bull💩 just educational content.
🔗 Links:
- 🍺 Support free education and buy me a beer
- 💬 Join our community on Discord
- 📧 Newsletter Subscribe here
- 🎥 YouTube Javascript Academy
- 🐦 Twitter: @dev_adamnagy
- 📷 Instagram @javascriptacademy