7 minute read
A quick tutorial on how to create a small web application to handle the userdrawing on HTML canvas
elements.
Introduction
The aim of this tutorial is to demonstrate how to work with canvas
elements. This will be done by creating a simple web page and usingtypescript to write the code that powers the logic of the page, which willallow the user to draw on the canvas
element like is possible in a trivialpaint application.
Why Typescript
I’m not an expert in web development, so I really appreciate the statictyping available in Typescript. Combined with VS Code’s Intellisense forTypescript, it makes it fairly easy to start writing new functionality anddiscovering new APIs while being reasonably certain of its correctness.
The files
There are three files needed for this demo:
tsconfig.json
- configures the typescript compilerindex.html
- simple HTML page with acanvas
andbutton
elementmain.ts
- the typescript file that we will write and compile
tsconfig.json
This file is relatively straightforward. It sets the target for the compiledJavascript code (ES5), turns on source map for easier debugging, and a fewother relatively inconsequential things. If you’d like to read more, a niceexplanation of tsconfig.json
is available here and a page explainingthe compiler options is available here.
{ "compilerOptions": { "target": "es5", "sourceMap": true, "lib": [ "es5", "dom" ], "noUnusedLocals": true, "module": "commonjs" }}
index.html
Another relatively straightforward file, the contents index.html
will serveas a foreshadowing of what we’re going to make.
<html><head> <title>Canvas demo</title></head><body> <canvas id="canvas" width="490" height="490" style="border: 1px solid black;"> </canvas> <p id="clear">clear</p> <script src="main.js"></script></body></html>
This creates a canvas
element (with the aptly-named id, canvas
). It alsocreates a text label (the p
element with id clear
) that will clear thedrawing area when clicked.
main.ts
Finally, we start writing the main file. We begin with the skeleton of theclass that is going to contain the functionality:
class DrawingApp {}new DrawingApp();
DrawingApp
is the name of the class we’re going to fill in. At the end, youcan see we instantiate a new class. Upon instantiation, we will find therelevant DOM elements and register event handlers for the events that we’reinterested in.
This is done by writing a constructor for the class. Add the followingsnippet to the body of the DrawingApp
class:
private canvas: HTMLCanvasElement;private context: CanvasRenderingContext2D;private paint: boolean;private clickX: number[] = [];private clickY: number[] = [];private clickDrag: boolean[] = [];constructor() { let canvas = document.getElementById('canvas') as HTMLCanvasElement; let context = canvas.getContext("2d"); context.lineCap = 'round'; context.lineJoin = 'round'; context.strokeStyle = 'black'; context.lineWidth = 1; this.canvas = canvas; this.context = context; this.redraw(); this.createUserEvents();}
The constructor
method is called automatically when instantiating theclass, the same as constructor methods in other OOP languages. In theconstructor, the first thing we do is get a handle to the element that hascanvas
as the id. We then get a 2D rendering context from the canvas. Othermodes are also available. For the 2D context, we then set some defaultsfor our drawing app. Finally, we store the handles in the class’s variablesand call a couple of methods that we will define next (add these to theclass):
private createUserEvents() { let canvas = this.canvas; canvas.addEventListener("mousedown", this.pressEventHandler); canvas.addEventListener("mousemove", this.dragEventHandler); canvas.addEventListener("mouseup", this.releaseEventHandler); canvas.addEventListener("mouseout", this.cancelEventHandler); canvas.addEventListener("touchstart", this.pressEventHandler); canvas.addEventListener("touchmove", this.dragEventHandler); canvas.addEventListener("touchend", this.releaseEventHandler); canvas.addEventListener("touchcancel", this.cancelEventHandler); document.getElementById('clear') .addEventListener("click", this.clearEventHandler);}private redraw() { let clickX = this.clickX; let context = this.context; let clickDrag = this.clickDrag; let clickY = this.clickY; for (let i = 0; i < clickX.length; ++i) { context.beginPath(); if (clickDrag[i] && i) { context.moveTo(clickX[i - 1], clickY[i - 1]); } else { context.moveTo(clickX[i] - 1, clickY[i]); } context.lineTo(clickX[i], clickY[i]); context.stroke(); } context.closePath();}
createUserEvents
does what the name says – it sets up the handlers for thecanvas events that we’re interested in. We register for both mouse and touchevents so that the app can work not only on normal computers but on mobiledevices as well. redraw
is a little bit more complicated. The method usesall the stored information about where the user clicks and drags the mouseand uses that to draw on the canvas
element. If the user was dragging(clicking and moving the cursor at the same time), we draw a line through allthe points one-by-one. However, if they just clicked in a spot, we draw apoint one pixel wide. The stored state of the user’s action is managed by theaddClick
and clearCanvas
methods:
private addClick(x: number, y: number, dragging: boolean) { this.clickX.push(x); this.clickY.push(y); this.clickDrag.push(dragging);}private clearCanvas() { this.context .clearRect(0, 0, this.canvas.width, this.canvas.height); this.clickX = []; this.clickY = []; this.clickDrag = [];}
These functions are used to add to and clear the state, respectively, of the user’s actions on the canvas
element. We store the X and Y coordinates along with a boolean
representing whether the user was in the middle of dragging the cursor.
When the user does perform an action on the canvas (other than simply moving the cursor over it), our event handlers will get triggered, which we registered with the browser with the createUserEvents
method. We’ll start with the three simple handlers:
private clearEventHandler = () => { this.clearCanvas();}private releaseEventHandler = () => { this.paint = false; this.redraw();}private cancelEventHandler = () => { this.paint = false;}
clearEventHandler
is registered to be called whenever the “clear” link isclicked. All we have to do is call clearCanvas
and the canvas is cleared.releaseEventHandler
is responsible for handling mouseup
or touchend
events. These occur when the user stops holding the mouse button over thecanvas
element. The event handler stores the state the user is no longerdrawing on the canvas and does a final redraw
call. cancelEventHandler
iscalled whenever the user moves his mouse or finger outside of the canvas
element. This handler method stops the processing of all future mouse eventsuntil the user performs another mousedown
event on the canvas
element.
Note the difference in syntax between these methods and the ones that werewritten earlier. This is because of the way Javascript (and thus Typescript)handle the binding of the this
variable. Creating a closure for the eventhandlers allows for the handler to keep a correct reference to the instancewhen it’s run as an event handler. There’s a pretty good write-up availablehere.
Finally, we write the two methods responsible for handling the initialclicking or touch event and the moving of the cursor while in a down state,respectively:
private pressEventHandler = (e: MouseEvent | TouchEvent) => { let mouseX = (e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0].pageX : (e as MouseEvent).pageX; let mouseY = (e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0].pageY : (e as MouseEvent).pageY; mouseX -= this.canvas.offsetLeft; mouseY -= this.canvas.offsetTop; this.paint = true; this.addClick(mouseX, mouseY, false); this.redraw();}private dragEventHandler = (e: MouseEvent | TouchEvent) => { let mouseX = (e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0].pageX : (e as MouseEvent).pageX; let mouseY = (e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0].pageY : (e as MouseEvent).pageY; mouseX -= this.canvas.offsetLeft; mouseY -= this.canvas.offsetTop; if (this.paint) { this.addClick(mouseX, mouseY, true); this.redraw(); } e.preventDefault();}
The methods essentially do the same thing with a couple of minor differences:
- get the X and Y coordinates of the event
- transform the coordinates to be relative to the
canvas
element itself - add the click to the stored state
- call
redraw
thereby updating the drawing surface
One difference is that dragEventHandler
makes sure the paint
variable istrue
. This is to prevent cases where the user performed a click eventoutside of the canvas
element but then moved the mouse inside of theelement. We also call preventDefault
(reference)in dragEventHandler
to improve the performance of the app. Without this,dragging the cursor can cause a degradation in responsiveness.
Note that both event handlers do a little bit of typepunning to work around the factthat the event handler is called for both mouse and touch events and thus thetype of e
can vary. We detect the type of the event argument by checkingfor the presence of the changedTouches
field; if it’s present, we have atouch event, otherwise it’s a mouse event.
Demo
You can see a live demo of this tutorial here.
Further reading
FAQs
What command is used to write the drawing to the canvas? ›
The print command is used to command the turtle to write something on the canvas.
Which JavaScript method is used to draw a circle on a canvas? ›To draw arcs or circles, we use the arc() or arcTo() methods.
Is canvas HTML or JavaScript? ›<canvas> is an HTML element which can be used to draw graphics via scripting (usually JavaScript).