Fun with HTML5 canvas

This is the eighth project of WesBos's JS30 series. To see the whole 30 part series, click here We will be creating a drawing canvas as seen below!

You can find the minimal starter code below -

As you can see we've got nothing more than a simple canvas element! That is because all the magic happens in the JS bits we're going to code.

You can learn more about the <canvas/> element and the API on MDN Docs

There is also a fully blow out tutorial series for those who wish to dig deeper.

So let's get started. There are multiple things we need to do in order to obtain our end result.

  1. Initialisation - get a handle to the canvas, set basic settings
  2. Track the user input
  3. Draw lines on the canvas
  4. Change the color of the line
  5. Change the thickness of the line

Initialisation

The handle to manipulate the canvas is ctx, the 2D context of the canvas. You have a 3D context for drawing 3D objects.

const canvas = document.querySelector('#draw')
const ctx = canvas.getContext('2d')

The canvas width and height are set to that of the whole window

canvas.width = window.innerWidth
canvas.height = window.innerHeight

Think of the canvas element like a piece of canvas (duh!) which you can draw and paint on. ctx.strokeStyle sets the color to use, and ctx.lineWidth sets the width of the line. ctx.lineJoin property determines how two connecting segments (of lines, arcs or curves) in a shape are joined together, ctx.lineCap property determines how the end points of every line are drawn (three possible values for this property and those are: butt, round and square).

Learn more about strokeStyle, lineJoin, lineCap, lineWidth

ctx.strokeStyle = 'blue'
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
ctx.lineWidth = 10

Track user ineractions

We need to know when the user has actually clicked on the canvas and is dragging the cursor. For this purpose we'll have a isDrawing variable.

let isDrawing = false

function draw(e){
  if(!isDrawing) return;

  //rest of the logic in here
}

canvas.addEventListener('mousemove', draw);

canvas.addEventListener('mousedown', ()=> isDrawing = true);
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseout', () => isDrawing = false);

So we've done a bunch of things here! The draw() function is what actually does the drawing on a mousemove event on the canvas, but only if isDrawing is true. Now isDrawing is set to true when there is a mousedown event on the canvas (i.e. we click on the canvas). When we unclick the mouse (mouseup) or when we leave the canvas area (mouseout), then isDrawing is set to false again.

Drawing lines on the canvas

Here is how drawing a line on the canvas works, let's say you want to draw a line from (10,10) to (50,50)

ctx.moveTo(10, 10)
ctx.lineTo(50, 50)
ctx.stroke()

You start drawing by moving to the point of the desired location (ctx.moveTo(10, 10)) and then draw the line to the desired destination (ctx.lineTo(50, 50)). But until this point nothing is really drawn on the canvas. The drawing happens only after you call ctx.stroke().

Now when the user moves over the canvas with the mouse, a stream of mousemove events are triggered and draw(e) is called (e having the event data). (e.offsetX, e.offsetY) have the (x,y) coordinates of the mouse wrt to canvas. These events matter only if isDrawing is true.

Now to draw lines we need a start postion (where the user has clicked), and lineTo() the new postion and an immediate stroke(). So as we're dragging our mouse while clicking we leave a trail.

The following code moves the drawing position to where the user has clicked.

//modifying the mousedown event listener
canvas.addEventListener('mousedown', (e) => {
  isDrawing = true
  ctx.moveTo(e.offsetX, e.offsetY)
});

The draw() function has now been modified to draw the line. The call to ctx.lineTo() also updates the drawing postion to where the line was drawn (i.e. (e.offsetX, e.offsetY))

function draw(e){
  if(!isDrawing) return;

  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
}

At this point we have a simple drawing canvas! You can see the results below -

Now for the finishing touches! We need to change both the width of the strokes and the color as the user drags on.

Change the color of the line

This can be done by changing the strokeStyle incrementally inside the draw() function. We'll be setting the color in HSL format, that way we can change the color across the rgb spectrum just by adjusting the hue factor.

let hue = 0

function draw(e){

  // draw line

  // change color
  ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
  hue = ++hue % 360;
}

Change the thickness of the line

let widthDelta = 1

function draw(e){
  // draw line ...
  // change color ...

  // change width
  ctx.lineWidth += widthDelta
  if(ctx.lineWidth < 10 || ctx.lineWidth > 100){
    widthDelta = -widthDelta
  }

}

The final code is here -

As you can see, the final code is slightly different from WesBos's code! The difference in the output line being slightly "dotted" (not a smooth one). This is because Wes used a lastX and lastY variable to store the latest position, I relied on the fact that drawing postion would update automatically to (x,y) after a call to lineTo(x,y) was made. The line was smooth before I added ctx.beginPath() to draw() (see the above codepen). But then the color and the width of the lines already drawn would also change, hence I needed to add ctx.beginPath(). This in turn makes my line dotted whenever the cursor is dragged quickly! (Try removeing line 19 ctx.beginPath() in the codepen and see what happens!)

Anyway, I found this observation interesting so I let it be! Here is the final one which works like Wes' does!