Manage Your Javascript Canvas Elements
Nov 8, 2017 • 3 Minute Read
The browser JavaScript ecosystem moves fast. It’s easy to become overwhelmed trying to keep up with new browser features and JavaScript application frameworks. Oftentimes, developers get burnt out by trying to keep up, and it can feel like you’re rewriting the same code over and over again.
A good pattern to help avoid this burnout is centralization of concerns. If you’re trying to avoid rewriting the same modules over and over again every time you work with a new framework, you might want to consider writing native JavaScript implementations that don’t rely on your framework and then tying them into the framework with minor modifications, or by treating them as modules using a module loader such as WebPack or Browserify. The most basic option would be to attach them to the global window object directly (if you’re not worried about namespace collisions, at least).
I have hundreds of these singletons to help organize my code. Manually keeping track of instances of various browser APIs can be tiresome, so my preferred pattern is a registry. For this first post, I’ll provide an example of a 2D canvas registry that will create, manipulate, and track instances of canvas elements throughout the lifecycle of an application, allowing you to centralize things like responsive updates and DOM manipulation. For simplicity’s sake, this singleton will be directly attached to the window for now, but in forthcoming posts, I will provide examples on how to package it as an ES6 module for use alongside a framework of your choice. This particular module works well with stock JS applications, React applications, or it can work well as an Angular 1 service with some minor modifications.
First, I’ll explain the structural elements that act as the infrastructure of the module. Then I’ll go through each function, explaining what it does and how it works. Finally, I’ll provide some usage examples to wrap up the post and paste a link to the full module for inclusion in your projects.
Note: this module uses ES6 syntax for clarity. If you prefer ES5 syntax for compatibility reasons, it is a pretty trivial exercise to remove the ES6 specific features.
Module Structure
window.CanvasSingleton = function (){
let registry = {};
The first line attaches a constructor function (CanvasSingleton) to the global namespace. The second line initializes an empty object that will act as our registry for representing individual instances of our responsive canvas objects.
this.pixelRatio = (function () {
var ctx = document.createElement('canvas').getContext('2d'),
dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
})();
This code is an IIFE (Immediately Invoked Function Expression). This is run as the interpreter instantiates the CanvasSingleton object the first time, as it only needs to be set once per app lifecycle. It sets this.pixelRatio to the measured pixel density of the particular device this code is executing on. By multiplying pixel values by this ratio later–in our render function–and then shrinking the canvas CSS down to the DOM element’s parent dimensions, we can seamlessly support high-DPI displays without having to manually perform this trick for every single canvas on the page. This also makes it easier to use canvas responsively. The long string of references, separated by the || pipe operators, is to help accommodate the various browser-specific implementations of the context.backingStorePixelRatio field.
Lines between the first two and last two are functions attached to the this keyword that will be used for creating, manipulating, and destroying canvases.
The penultimate line of the function returns the this keyword, allowing external code to access any functions or values attached to this.
The Important Bits
The code in the middle of the module is all of the functions we will be using to actually generate and keep track of our various canvas elements. I’m going to go through them one at a time and explain how they work.
this.create = (parent, render) => {
let canvas = document.createElement('canvas');
let [w, h] = [parent.clientWidth, parent.clientHeight];
let context = canvas.getContext('2d');
context.identifier = Object.keys(registry).length;
let listener = window.addEventListener('resize', () => {
this.update(context.identifier);
});
canvas.width = w * this.pixelRatio;
canvas.height = h * this.pixelRatio;
canvas.style.height = `${h}px`;
canvas.style.width = `${w}px`;
parent.appendChild(canvas);
context.scale(this.pixelRatio, this.pixelRatio);
registry[context.identifier] = {
parent,
canvas,
render,
listener,
context
};
render(canvas, context);
return context;
};
The above function takes two parameters. The first needs to be a DOM element, preferably empty. The dimensions of this DOM element will determine the virtual size of the canvas element we’re generating and appending to the DOM.
The Render Function
The second parameter is a render function. This is where you’ll put all of your draw logic. The render function will be passed configured canvas and context objects that can be used to draw. If you want to make your drawings relative to the canvas size, you can use canvas.clientWidth / 100 and canvas.clientHeight / 100 as percentage units.
The render function can be fired manually via this.update, and will also be fired on window.resize events to allow for responsivity. (Note: for more CPU intensive canvas elements, you’ll likely want to debounce calls to the render function to avoid excessive CPU utilization).
The Rest of the Create Module
The first task the create function does is create a canvas element in the document scope. Then, it initializes some local variables, w and h by measuring the client dimensions of the DOM node that was passed into the function as parent. Next, we create a 2D canvas context on the canvas element we created and give it an identifier (an integer that is equal to the number of keys currently in the registry, incremented by one).
The next discrete action this function performs is setting an event listener for the ‘resize’ event. All this does is call the CanvasSingleton.update function and pass in the canvas identifier from the canvas we’re creating.
Next, we set the canvases literal width and height to the measured dimensions multiplied by the devices pixel ratio. The following lines set the canvases style dimensions to the measured dimensions of the parent DOM element. What this means, in effect, is that the canvas is much bigger in memory than it will be rendered, by a factor of the devices pixel ratio. The canvas is compressed by the renderer to the measured size. For devices with pixel ratios higher than 1 (almost everything, nowadays), this means your canvas will be crisp at any pixel density.
The last lines of this function append the canvas element we initialized to the parent element that was passed into the function via parameter, calls the context.scale method with our pixel ratio values to make the high-DPI support work, set the registry entry at context.identifier to be an object containing references to our parent, canvas, render function, listener, and context, call the render function directly once, and then return the context object to the function that called the create function so that code external to this can access the identifier for actions later in the canvases lifecycle.
this.update = (id) => {
let {parent, canvas, context, render} = registry[id];
let [w, h] = [
parent.clientWidth,
parent.clientHeight,
];
canvas.width = w * this.pixelRatio;
canvas.height = h * this.pixelRatio;
context.scale(this.pixelRatio, this.pixelRatio);
canvas.style.height = `${h}px`;
canvas.style.width = `${w}px`;
context.clearRect(0, 0, canvas.width, canvas.height);
render(canvas, context);
};
this.update is a utility function we can call whenever we want to redraw the canvas. It updates the dimensions and scaling of the canvas as well so that our canvases are responsive to new dimensions. It’s also useful to call directly from external code if the data you’re rendering has changed. It takes one parameter: a canvas identifier that is returned as part of the this.create function.
The first two lines initialize some variables. The first set is pulled directly out of the registry. These are just for convenience and clarity. The w and h variables initialized on the second line are measured directly from the original parent element, whose referenced we saved in registry[id].
Next, we redo the setup and scaling work we did on this.create function to ensure that the canvas dimensions continue to match the parent DOM node. Finally, we clear the canvas and then call the render function again.
this.animateTick = (id) => {
let {canvas, context, render} = registry[id];
context.clearRect(0, 0, canvas.width, canvas.height);
render(canvas, context);
};
this.animateTick is a function meant to be called inside of a game or animation loop. Ideally, you’d use something like requestAnimationFrame whenever the browser is ready to re-render an animation and change the input state to the render function to make this into a very simple game engine. animateTick is basically a very pared down version of this.update. It too takes a context identifier from this.create.
this.destroy = (id) => {
let canvas = registry[id].parent;
canvas.parent.removeChild(canvas.canvas);
window.removeEventListener(canvas.listener);
delete registry[id];
};
The last function, this.destroy is a general cleanup function. It removes the canvas element from the DOM. Next, it removes the event listener that we set up in this.create for resizing that individual canvas so we dereference the render function and avoid memory leaks. Finally, we delete all of the references in the object at registry[id]. It is very important to call this function when your canvas element is no longer needed; if you spawn a lot of canvases and never destroy them, it can cause memory leaks.
Extremely Basic Usage Examples
<!DOCTYPE html>
<html>
<head>
<title>CanvasSingletonExample</title>
<meta charset="utf-8">
<script src="/canvasSingleton.js"></script>
<style type="text/css">
#parent{
width: 50%;
float: left;
height: 300px;
margin: 50px auto;
}
</style>
</head>
<body>
<div id="parent"></div>
<script type="text/javascript">
const canvasSingleton = new CanvasSingleton();
let canvasReference = canvasSingleton
.create(document.getElementById('parent'), (canvas, context)=>{
// set canvas fill to black
context.fillStyle = '#000';
// fill context with black rectangle
context.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight)
// start path and set the stroke styles
context.strokeStyle = '#fff';
context.lineWidth = 2;
context.lineCap = 'round';
context.beginPath();
// start draw cursor at top left, draw a line to bottom right
context.moveTo(0, 0);
context.lineTo(canvas.clientWidth, canvas.clientHeight)
// stroke line with styles set above.
context.stroke();
})
window.setTimeout(()=>{
canvasSingleton.destroy(canvasReference.identifier);
}, 1000*20);
</script>
</body>
</html>
This code is an HTML page that contains a width-responsive canvas element. It should have a black background with a small white line drawn diagonally from the top left to bottom right. If you resize the canvas, you can see that it will redraw to match the new dimensions of the parent element. If you are on a high-DPI device, you shouldn’t be able to see any pixelation or excessive aliasing. You can create and manage as many canvases as you want to without writing the responsive or high-DPI boilerplate more than once. You also get a nice handle to destroy the canvas with when you’re done (in the case of the above example, after 20 seconds).
Wrapping Up
I find myself reusing this pattern all the time to wrangle some of the more unwieldy browser APIs into shape. By defining our elements as entries in a registry–outside of the external code that is creating them–and providing utility functions to mutate registry entries, we can negate some of the complexity and separate the boilerplate from the actual code that does the drawing. It’s also easy to debug and inspect this pattern. You can easily trigger draw cycles from a JavaScript console after tweaking values, inspect the contents of the registry, and actually get a feel for how your code is interacting with the render engine itself. I hope that you find the module and moreover the pattern it relies on to be as useful as I have.
One final note: this code is by no means final, nor is it exactly what I use on production systems. I realize there are lots of ways to extend it, enhance it, and refactor it; not least of which is changing the registry to a normal array and using the index as the identifier. This particular implementation of the canvasSingleton concept is focused on clarity over raw performance, for instructional purposes.