Collaborative Drawing App: Drawing Shapes on the Canvas

Shawn
5 min readMay 28, 2022

--

In this section we add the ability to choose colors and draw lines, circles, and squares on the HTML5 canvas!

How the Drawing Process Works

In order to draw on the canvas, we’ll need to know

  • What color we’re drawing with
  • What shape we’re drawing
  • Whether we’re drawing the first or second point of the shape
  • Where the start and end points are

Add these properties to the constructor of the Canvas class:

this.activeColor = '#000000';
this.startPoint = null;
this.endPoint = null;
this.pointMode = 'start';
this.mode = 'Line';

So we default to drawing a black line. Now we add the function that lets us change what color we draw with, as a method on the Canvas class:

setColor(color) {
this.activeColor = color;
this.ctx.strokeStyle = color;
this.ctx.fillStyle = color;
}

Making the Color Palette Work

Now that we have the setColor method, we can finish the Palette.draw() method. We add the canvas object as a parameter, and attach an onclick event handler to each palette square:

draw(canvas) {
const row1 = document.querySelectorAll('#row-1 .palette');
const row2 = document.querySelectorAll('#row-2 .palette');
row1.forEach((div, idx) => {
div.style.backgroundColor = this.colors[0][idx];
div.onclick = e => canvas.setColor(this.colors[0][idx]);
});
row2.forEach((div, idx) => {
div.style.backgroundColor = this.colors[1][idx];
div.onclick = e => canvas.setColor(this.colors[1][idx]);
});
}

Since we added it as a parameter here, we need to set it in the index.html file:

palette.draw(canvas);

Choosing a Shape to Draw

This project allows 5 shapes to be drawn: line, hollow circle, filled circle, hollow rectangle, and filled rectangle. We default to a line by default. In order to change the shape (called mode in the code), first add this method to Canvas :

setMode(mode) {
this.mode = mode;
}

Then in the HTML, below the color palette, add a row of buttons that let us pick what shape we want to draw:

<div id="draw-methods">
<button onclick="canvas.setMode('Line')">Line</button>
<button onclick="canvas.setMode('Hollow Rectangle')">Hollow Rectangle</button>
<button onclick="canvas.setMode('Filled Rectangle')">Filled Rectangle</button>
<button onclick="canvas.setMode('Hollow Circle')">Hollow Circle</button>
<button onclick="canvas.setMode('Filled Circle')">Filled Circle</button>
</div>

Handling Clicks on the Canvas to Draw

Here’s the handleDraw method for Canvas . I’ll give you the code then explain what it’s doing, because it’s more complex than what we’ve seen so far.

handleDraw(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (this.pointMode == 'start') {
this.startPoint = [x, y];
this.pointMode = 'end';
} else if (this.pointMode == 'end') {
this.pointMode = 'start';
this.endPoint = [x, y];
// do the drawing
if (this.mode == 'Line') {
this.drawLine(this.startPoint, this.endPoint);
} else if (this.mode == 'Hollow Rectangle') {
this.drawHollowRectangle(this.startPoint, this.endPoint);
} else if (this.mode == 'Filled Rectangle') {
this.drawFilledRectangle(this.startPoint, this.endPoint);
} else if (this.mode == 'Hollow Circle') {
this.drawHollowCircle(this.startPoint, this.endPoint);
} else if (this.mode == 'Filled Circle') {
this.drawFilledCircle(this.startPoint, this.endPoint);
}
this.startPoint = null;
this.endPoint = null;
}
}

The first three lines are about determining where the user clicked. You might think when you click on a canvas, the mouse click event would simply have the coordinates of where in the canvas you clicked. But it’s not quite that easy. You have to calculate it yourself.

getBoundingRectClient returns values such as how far to the left and down the canvas is from that top left corner. The top left corner of the page is (0, 0), and the values get bigger the further right and down you go. e is the parameter passed to the function, representing the mouse click event. clientX and clientY represent where on the page you clicked. Subtracting the canvas element’s offset gives you the mouse’s position within the canvas element.

Once we have the location of the click, we need to know if this is the first (“start”) or second (“end”) click. Each shape is drawn with two clicks. For a line, just pick the start and end point. For circles, pick the center and the edge. For rectangles, pick two opposite corners. If it’s the first click, then store the location of the click but don’t do anything. If it’s the second click, store its location, draw the shape, then forget the locations.

The methods for drawing the shapes perhaps look more complicated than they really are. For drawLine , it’s straightforward:

drawLine(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.moveTo(startPoint[0], startPoint[1]);
this.ctx.lineTo(endPoint[0], endPoint[1]);
this.ctx.stroke();
}

Drawing the hollow and filled rectangles just requires calculating the two points that weren’t clicked:

drawHollowRectangle(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.strokeRect(
startPoint[0],
startPoint[1],
endPoint[0] - startPoint[0],
endPoint[1] - startPoint[1]
);
}
drawFilledRectangle(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.fillRect(
startPoint[0],
startPoint[1],
endPoint[0] - startPoint[0],
endPoint[1] - startPoint[1]
);
}

The methods for drawing the circles need to calculate the radius using the distance formula, then draws a 360 degree arc.

drawHollowCircle(startPoint, endPoint) {
const x = startPoint[0] - endPoint[0];
const y = startPoint[1] - endPoint[1];
const radius = Math.sqrt(x * x + y * y);
this.ctx.beginPath();
this.ctx.arc(startPoint[0], startPoint[1], radius, 0, 2 * Math.PI, false);
this.ctx.stroke();
}
drawFilledCircle(startPoint, endPoint) {
const x = startPoint[0] - endPoint[0];
const y = startPoint[1] - endPoint[1];
const radius = Math.sqrt(x * x + y * y);
this.ctx.beginPath();
this.ctx.arc(startPoint[0], startPoint[1], radius, 0, 2 * Math.PI, false);
this.ctx.fill();
}

Attaching the Canvas Event Listener

Getting the canvas to actually respond to being clicked on requires adding these two lines to the constructor:

this.handleDraw = this.handleDraw.bind(this);
this.canvas.addEventListener('click', this.handleDraw);

Those of you who have worked with React might recognize this pattern. But why is it needed? It has to do with how the event listener works. Without the bind, in handleDraw the this context variable would point to the HTML element that was clicked on. In this case, the canvas element. But that would mean we don’t have access to the Canvas object which has the methods we need. By using bind, we force this to refer to the object.

Putting it All Together

Here’s what the files changed in this step should look like at this point:

After refreshing the page, try changing the color, setting which shape to draw, and start clicking on the canvas. Here’s a little testing I’ve done to ensure it worked:

Playing with colors and shape types

In the next section, we’ll add some convenience functions that show which shape is selected, and short instruction on what the app is expecting the user to do next.

Collaborative Drawing App Series

--

--