will.institute

by Will Lesieutre

Responsive Canvas

Last demo left off with the drawing adjusting to the correct resolution every time that it’s drawn. But it was drawn manually with button clicks, and if the page size changed after drawing it stretched to the new size instead of staying sharp.

To work well on a responsive page, the canvas needs to update automatically when the window changes size. A resize event listener on the window object will suffice, but it fires repeatedly as the window size changes.

You can see this in action below. The number counts how many times the resize event occurs:

0

If the canvas is computationally expensive to draw, then it’s even more expensive to draw over and over as the window size changes. Better to let the image be upscaled until the resizing is done, then only redraw it once. A technique called “debouncing” will do just that.

Debouncing?

In electronics, a common issue with mechanical inputs like buttons is that they can “bounce” back and forth between open and closed states as they change from one to the other. Say you have a button that toggles a light on and off. What if you pressed the button once, but the control circuit saw a press, then a moment later a release for a split second, followed by a longer press?

A simple implementation of “When the button is pressed, toggle the light between on and off” would see two presses and toggle back to where it started.

But a smarter one prevents that by debouncing the input. When you first hit the button it starts a short countdown timer. If the countdown reaches zero it acknowledges it as a real button press. But if any additional presses are registered before the countdown finishes, the countdown starts over. In the end only the last press registers; the burst of fast input bounces at the start are ignored.

The counter below is triggered by a debounced window resize event, waiting for a clear 0.1 seconds after an event to know that the resize is over:

0

On mobile this won’t look like much because the browser viewport goes directly from one size to another (like rotating portrait to landscape). But with a desktop web browser’s freely resizeable windows, you can see this counts up much less often than the resize event counter does. A typical window resize might send an event 20 times (see the first counter up top) while this debounced resize counter is only hit once at the end.

The javascript implementation of this is a bit more abstract than managing a timer directly in an a microcontroller’s loop, since web pages need to make way for other tasks happening on the CPU. The debounce timer is handed off to the browser using setTimeout() to call the draw function if the countdown finishes. Each resize event starts a 0.1 second timer, but a new resize will cancel the previous one using clearTimeout() and replace it with a new one. The number above will count up once a timer runs for its whole 0.1 seconds without being canceled by a newer event. Thanks to Shalvah for showing this implementaiton on Bits and Pieces.

function debounce(f, t) {
    return function (args) {
        let previousCall = this.lastCall;
        this.lastCall = Date.now();
        if (previousCall && ((this.lastCall-previousCall) <= t)) {
            clearTimeout(this.lastCallTimer);
        }
        this.lastCallTimer = setTimeout(() => f(args), t);
    }
}

The canvas below is hooked up to the same debounced resize event as the second counter - when you finish resizing the window it updates drawing context size and redraws the circles:

Responsive canvas changes resolution automatically.

And there we have it: an automatically resizing canvas to fit a responsive website.

As you enlarge the window this won’t update to a higher resolution until you stop resizing, so there could still be scaling artifacts in the meantime. But that strikes a good balance of canvas quality and not wasting computation on redraws until it comes to rest at a final size.

If the temporary upscaling is noticeable and needs to be reduced, a throttled version could periodically redraw it at a limited rate. Or the resize event could check both the debounce timer and make sure the size is within an acceptable range of the correct size, calling for a redraw if the canvas is too small by 50 pixels or more.

Another avenue for improvement would be checking that the size of the canvas has actually changed during the window resize. If the window is big enough that only the size of the margins has changed, then there’s no reason to update the canvas at all. But unless drawing the canvas is very computationally expensive, one unneeded redraw is a tiny performance hit.

These improvements are left as an exercise for the reader.