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 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 Asteroids 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 and 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.
How big is the canvas?
Faced with stage content that is larger than your browser window, your first instinct might be to set the size of the stage to whatever large dimensions you need. This can be handy because it gives you an immediate scrollbar solution via the CSS overflow attribute. [Yes I did say there is no CSS in the HTML5 canvas, but the canvas element itself is a first class citizen in the browser DOM so you can use CSS on it. Good try though! While we are in these brackets I can also mention that we don’t actually declare the canvas element itself – instead Konva injects it into that container div you provided. Don’t believe me? Go ahead and check it out in the browsers developer tools.]
So yes – you might set your stage size to be 3 times the width of the page and that could work. Where it starts to break down is in the area of UI performance. There’s no hard and fast rules here – no specific dimensions I can quote that will unravel the UX of the perfectly crafted app you are constructing, but with a large canvas some combination of device capabilities, canvas size and content will give you a jerky / jumpy refresh at some point after you’ve got past the stress of the rush to get V1 out the door.
Ok, so how big should I make the stage then? Good question, glad you asked. Make the stage the same size as the space it is displayed in. Yes even if the contents of the stage use a space 2000 x 2000 and you only have space on the page for 500 x 400, stick with that smaller space. Yes – the contents of the stage that are ‘out of view’ still exist and you can still grab an image of the entire stage, but the important thing is that you are letting the GPU work less hard as it only has to shuffle onto the screen the bits & bytes for the part you can see.
In that case, how do we scroll the view? Another good question – you might want to think about how to zoom in and out too. There are various scrolling options described here. For zooming you’ll be changing the stage scale and you’ll most likely want to zoom in place as explained here.
If your requirements list includes scrolling some out-of-view section of the stage into view then you need to consider the effect of the zoom magnification on that feature – zoom in to x2 and the distance to that area of interest is half as far as it is at x1. Have fun.
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 onload() 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 (we are now at v9 in Feb ’24) Konva handles refresh of the 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.
Is there a lot of overhead to code for drag & drop, rotate & scale?
I added this after I saw a few new folks doing way too much work to cover what Konva does for free. The quick answer is no – Konva does all the work for you.
Long answer – if you find yourself worrying about calculating shape positions after rotation or scaling then in most use cases you took a wrong mental turn somewhere upstream.
[Mostly you don’t need any code in the Konva.Transformer’s events]
Here’s an example. What’s happening here is that we are letting the user click the stage to set points for a Konva.Line. The Line shape will draw between those points, and we can close the shape to make a polygon. The circles on the points are there so that we can edit the points by dragging the circles. A super-easy use case for editable polygon or lines.
But wait – we’re looking at small circles placed over the line joins? What if we rotate the polygon, or drag it, or scale it. What scary math do we need to use to put the circles back over those points?
None, nada, zilch, nothing. With the caveat that we put the line and the circles inside the same Konva.Group. A Group is a container. Any shapes that it contains live in their own little universe as far as transformations are concerned. Any transformations like scale or rotation that we apply to the Group affect only the Group. Its child shapes just go along for the ride.
Plus, when we drag one of those circles, it’s drag event has co-ordinates in reference to the (0,0) position of the Group. Therefore when we need to feed the new position to the Line we don’t have to do any math.
So – the erroneous mental turn in the road was not thinking about grouping shapes that live their lives together.
Summary
We’ve taken a skim through the critical differences and mindset you need to get into HTML5 canvas 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. And there's a growing set of blog posts about Konva in this, my blog, that you are reading now.
Thanks for reading.
VW. Jan 2023, updated Feb 2024.
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.