I’ve seen experienced devs crash and burn on the differences between working with the HTML5 canvas element and the HTML DOM. Here’s some short-cut learnings to help you refactor your brain the canvas way and keep you moving.
Preface
Please don’t despair as you read the next few paras – I’m just getting it out of the way so we can get on with better stuff. Developing with the canvas is actually straightforward, fun and rewarding. And, compared to CSS, at least the layout is exactly what you want it to be!
Here we go then…
Drawn then gone – there ain’t no DOM
The HTML5 canvas is a flat, 2-dimensional drawing surface composed of a grid of dots – or pixels. At it’s absolutely most fundamental, you could draw on this by setting some of the pixels to a color and leaving the remainder unchanged. Whatever you did would be seen in the canvas and might be pretty, but it would be very hard to work that way so the canvas has its own API that allows you to ‘draw’ various geometric shapes, text, and of course, images.
However, there’s a twist here…
All that experience you’ve built up working with the HTML DOM counts for zilch here, because there’s no DOM equivalent in the canvas.
That’s right – the canvas, without any libs in place, is a fire-and-forget deal. You draw, but you can’t edit/change what you drew. All you can do is clear the canvas and redraw. (On the plus side, the canvas element is very, very, very quick at doing that – its not all s**t)
No elements
Yep – there is no concept of child ‘elements’ of a canvas.
No CSS selectors
This shouldn’t come as a surprise since there’s no DOM, what use would selectors even be?
No reactivity
There’s no shadow dom, no set of change events to listen for, no reactive hooks for React or Vue.
Libs to the rescue
Why use a lib when working with the HTML5 canvas? Well, a decent lib such as Konva, will give you an object model and an API to drive it. You should get the fundamental shapes used in all 2D diagram building – the Rect, Circle, Line, etc. There will be attributes for size, position, stroke and fill colors, and a set of events that you can listen to. There’ll be baked-in drag & drop, grouping, custom shapes, and animation.
Using such a lib, you can leverage your JS skills to the max.
Which lib to select
Select your canvas lib like you’d select any other. Think about the risk between continuity, curation, support and resources of a commercial and open source products. Look at release and patch frequency – either in formal releases or on the Github pages. Watch out for projects that have gone stale and unsupported. Look at the resources around the product – that means documentation, tutorials & demos, forums, and StackOverflow activity. Look out for a lively and active user base. If you have a leaning toward a library like React or Vue, check out if there’s an extension in the lib. Then take the plunge.
I like Konva. I’ve used others over the years, both commercial and open source. I prefer to code using vanilla JS. I used the list of criteria above to evaluate where to focus my time and what to rely on. I’ve been using Konva for around 5 years now, including using it on 2 commercial products, and with more in the pipeline. The founding developer Anton Lavrenov, is very much involved with the project and is available for consulting.
Working with Konva
Declaring the canvas and instantiating shapes is straightforward as shown here. First the html – all we need for Konva is a container div to target. Yes a div and not a canvas – Konva will insert the canvas for us. The canvas background is transparent so if you want a background color set that on the container div via CSS.
<body>
<div id="container"></div>
</body>
Next the JS. First we define the variables for the stage, then the layer, then a rectangle. We then add the layer to the stage and the rect to the layer. This is essentially the construction of the document model.
const stage = new Konva.Stage({
container: 'container',
width: 800,
height: 400
}),
layer = new Konva.Layer(),
myRect = new Konva.Rect({
x: 50,
y: 60,
width: 20,
height: 40,
fill: 'red',
stroke: 'black',
strokeWidth: 4
};
stage.add(layer);
layer.add(rect);
We use the term ‘shapes’ for the rect and it’s fellow visible things. Importantly, the attributes of shapes aren’t set via CSS. In fact you can’t use CSS in any way on the canvas contents unless the lib you select has a CSS-like API.
Note: I would suggest caution if you are looking at a lib boasting a canvas CSS API as the implementation is unlikely to be as complete as the CSS capability we see in modern browsers and you might find you push on with a particular lib because of its CSS promise only to find later to your cost that the implementation is lacking.
With Konva, a shape’s attributes are defined at time of instantiation via a settings object as can be seen in the case of myRect above, and these can be changed later via various means. This overcomes the fire-and-forget issues of working with the canvas without a lib. There’s no magic though – what’s happening is that Konva is acting as a wrapper over the canvas, giving you the object model manipulation capability, but all the time erasing and re-drawing the canvas contents ridiculously quickly and flicker free, thus giving the impression of a living breathing 2D drawing surface.
Did you notice that position and dimension attributes don’t have the ‘px’ suffix? In fact ‘px’ is the only unit in canvas and all unit params are required to be numeric, therefore it is a syntax error to provide the unit type. For anyone coming from an intense CSS-based background, learning NOT to type ‘px’ is a hassle and a potential source of early bugs – expect to do this a few times.
Instead of CSS selectors, Konva has its own features that allow selection by name, type, etc. See my blog post Konva does not have css-type selectors, so what does it offer instead? for a thorough discussion and working examples. Note though that Konva doesn’t support the set-based operations you might find in jquery or similar libs where you can give a selector to return a list of matching elements and can then iterate directly over that set. Instead you execute the search which returns an array that you iterate through. So it’s not quite the same but entirely usable.
With Konva, the format for listening for events on a shape can be seen below. The argument passing into the function is a JS event augmented with Konva-specific attributes. And note that the ‘this’ inside the listener refers to the shape object. In this example, we are listening to a click on the ‘myRect’ shape meaning that ‘this’ represents myRect.
myRect.on("click", function(e){
// fill with lime
this.fill("lime");
})
In this example we can see an example of how to change the appearance of an existing shape. We are changing the fill color of the shape from whatever it was to “lime”. HTML5 canvas works with the same color settings as standard HTML elements so we can use RGB, RGBA colors and color names, etc.
There are events for all the things you would expect.
Bubbling does happen in Konva, meaning you can delegate listeners to the Stage. Konva is relatively fast at handling most things so delegation is not needed for a handful of shapes, but if you are coding up the next Asteriods reboot and you’re asteroid field runs to hundreds of spinning boulders then its good to know you can optimise performance by delegation.
Handling bubbling at the stage level would look something like this:
stage.on('some event', function (evt) {
if (evt.target.getClassName() === 'Stage') {
// ...do the stage event
}
else {
// optionally, handle any events bubbled up from other shape
}
}
To stop the event bubbling set evt.cancelBubble = true (oddly note the syntax here – mostly we use functions to set attribute values, but in this case it’s a direct assignment). Note also, that the JS event model has deprecated cancellBubble. But it’s alive ank kicking for Konva events – reason explained after next code sample.
circle.on('click', function (evt) {
alert('You clicked the circle!');
evt.cancelBubble = true; // note we set to true, not false!
});
While we are on the subject of events, you need to know that Konva operates its own events model. This is not as maverick as you might think so try to stop your toes curling too much. There’s no DOM in Konva, so the usual JS events model is not going to be of any use.
Instead the evt param you see in the event listeners for Konva is a Konva event object. In there you’ll find the evt.target which will be the Konva shape target of the event. There’s a bunch more useful attrs, including if you need it evt.evt which is the JavaScript event produced by the click, or whatever action you just did, on the canvas element. Generally, coding for Konva, you’ll want to use the plain evt parameter, as in the bubbling and cancelBubble code samples above.
The image loading trap
This was meant to be a very high level article but I just have to add this as it’s the number one trap that people new to canvas and Konva hit. It’s about loading images. You likely know that even in hard-coded HTML without the intervention of JS, loading images is an asynchronous operation – we ask the browser to go get an image and it assigns a separate thread for that operation which reports back on completion of the fetch. There’s no magic in relation to the canvas that changes this.
Therefore the code to load an image looks like this:
const myImage = new Konva.Image({
x: 0,
y: 0,
width: 600,
height: 400
})
layer.add(myImage);
// We load an image via a JS image object
const jsImage = new Image();
jsImage.onload = function(){
// when it has loaded we assign the
// JS image obj to the Konva image.
myImage.image(jsImage);
}
// We set the src attr of the JS image object to kick off the load
jsImage.src = "https://www.disney.com/donald.png";
The critical point here is that we load an image to the canvas by asking a JS image object to fetch the image for us. The act of setting the Konva.Image.image() property to the JS image object tells Konva to grab the image from that JS object, therefore we put that line in the oload() event of the JS image so that we know the image is loaded before we make the assignment to the Konva Image.
The ubiquitous brain-bug is assuming that the loading of the image is inline / synchronous. To avoid it, every time you code the phrase ‘new Konva.Image’ associate the word onload and expect to see it somewhere in the nearby code.
When should I draw ?
In the old days you would create the stage and all your shapes, then call for the stage to ‘draw’ the output, or more effectively ‘batchDraw’ it. Since v8 Konva handles drawing for you. Under the hood, Konva uses an animation frame process to make this magic happen. If you know anything about JS animation you’ll know that the animation frame stuff is optimized to work in harmony with the hardware to give you the optimum graphics output experience. All you need to worry about is your code – Konva knows best when to draw.
Summary
We’ve taken a skim through the critical differences and mindset you need to get into HTML5 development with Konva. There are a ton of resources at the Konva docs site. The tutorials, demos and API docs are very useful. They’re not perfect as this is a fast-moving open source project, but there’s also a significant set of questions and useful answers at StackOverflow, and a busy Discord channel where you can find a community of developers who are keen to invite you in and share what they’ve learned.
Thanks for reading.
VW. Jan 2023.
PS. This article was written in one afternoon after I saw another frustrated soul, probably under pressure from their boss to make something quickly, floundering over how to take those initial steps with the canvas. I’ll probably add more points in as more come to mind or surface on SO or Discord. So you might want to sign up for updates.