Create a scalable canvas in javascript

For anyone who’s worked with HTML Canvas elements, they know that formatting the canvas for a flexible and dynamic user interface can be a real headache. Normal elements can have their size adjusted dynamically as the user stretches or shrinks their window, but canvas elements can’t have their size changed so easily. The problem lies in the fact that any time you try to resize a canvas element, it resets the canvas, clearing any images or marks that may have already been put on the canvas. How we’re going to solve this is by drawing onto an offscreen canvas and rendering its image to the onscreen canvas any time a user interacts with the canvas. This also keeps pixels from being distorted as you stretch the canvas at odd pixel numbers.

This is what we’ll be creating:

In this tutorial, we’ll be creating a flexible canvas that changes with the window size.

The HTML and CSS we’ll be working with:

<div id="container">
<div class="canvas-container">
<canvas id="onScreen"></canvas>
</div>
</div>
-----------------------------------------#container {
width: 100%;
height: 97vh;
display: flex;
}
.canvas-container {
width: 100%;
}
canvas {
background: #66CCCC;
}

First, get the canvas and its context.

let onScreenCVS = document.getElementById("onScreen");
let onScreenCTX = onScreenCVS.getContext("2d");

We’ll make a few variables that we’ll need later, and set the initial size of the canvas We’ll go over the setSize function later.

let baseDimension;
let rect;
setSize();
onScreenCVS.width = baseDimension;
onScreenCVS.height = baseDimension;

Before we do anything with an offscreen canvas, we’ll need an image variable that we can use to keep our onscreen canvas updated with the correct image. We’re separating it like this instead of just using the offscreen canvas as our source directly so that it’s more compatible with larger projects or frameworks where you’d want to save the image as a state.

let img = new Image;
let source = onScreenCVS.toDataURL();

Finally, create the offscreen canvas and set its dimensions. These are the dimensions of the actual image and any scaling we do for the onscreen canvas won’t effect these dimensions. This is important to keep our source image consistent and free of distortions. It’s also important that the ratio of the dimensions of the onscreen and offscreen canvases match each other.

let offScreenCVS = document.createElement('canvas');
let offScreenCTX = offScreenCVS.getContext("2d");

offScreenCVS.width = 32;
offScreenCVS.height = 32;

Add event listeners for the mousemove, mousedown, and mouseup events, so that we can draw on the canvas to view the effects of scaling.

onScreenCVS.addEventListener('mousemove', handleMouseMove);
onScreenCVS.addEventListener('mousedown', handleMouseDown);
onScreenCVS.addEventListener('mouseup', handleMouseUp);
let clicked = false;function handleMouseMove(e) {
if (clicked === true) {
draw(e)
}
}
function handleMouseDown(e) {
clicked = true;
draw(e)
}
function handleMouseUp() {
clicked = false;
}

Now we will define our helper functions, starting with the draw function called above. For this example, I’m drawing a single pixel using the built-in fillRect function. By dividing the onscreen coordinates of the mouse by the ratio of the size difference of the canvases, and rounding down, I ensure that I draw a clear pixel (no half-pixels or anything in between) at the relative coordinates on the offscreen canvas. Then we render the offscreen image so it looks like we drew directly onto the onscreen canvas:

function draw(e) {
let ratio = onScreenCVS.width/offScreenCVS.width;
offScreenCTX.fillRect(Math.floor(e.offsetX/ratio),Math.floor(e.offsetY/ratio),1,1);
//Set the source of the image to the offscreen canvas
source = offScreenCVS.toDataURL();
renderImage();
}

Rendering the image only requires drawing it to fit exactly onto the onscreen canvas, and making sure the image’s source is updated as well as disabling image smoothing if you don’t want anti-aliasing.

function renderImage() {
img.onload = () => {
//Prevent blurring
onScreenCTX.imageSmoothingEnabled = false;
onScreenCTX.drawImage(img,0,0,onScreenCVS.width,onScreenCVS.height)
}
img.src = source;
}

Since we’re going to render the image any time the size is changed as well, we’ll also need to include the size changing in the img.onload function.

function renderImage() {
img.onload = () => {
//if the image is being drawn due to resizing, reset the width and height. Putting the width and height outside the img.onload function will make scaling smoother, but the image will flicker as you scale. Pick your poison.
onScreenCVS.width = baseDimension;
onScreenCVS.height = baseDimension;

//Prevent blurring
onScreenCTX.imageSmoothingEnabled = false;
onScreenCTX.drawImage(img,0,0,onScreenCVS.width,onScreenCVS.height)
}
img.src = source;
}

It needs to be changed only once the image is loaded or else the image will flicker as you resize it.

For the setSize function, we’ll check whether the height or width of the parent element is smaller and change the base dimension accordingly:

function setSize() {
rect = onScreenCVS.parentNode.getBoundingClientRect();
rect.height > rect.width ? baseDimension = rect.width : baseDimension = rect.height;
}

We called setSize in the initial load of the page and now, we’ll call it any time the window is resized along with re-rendering the image. You may choose to base it off of an element resize instead.

function flexCanvasSize() {
setSize();
renderImage();
}
window.onresize = flexCanvasSize;

Visit the codepen at the top to play around with it!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store