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:
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.