HTML5 Canvas Scaling
Recently I’ve had some time to fettle with things other than Infrastructure and Windows servers so I’ve decided to do my company website with HTML 5. It’s nothing special but as part of my tinkering, I created a new logo and I thought it’d be cool to re-create it in HTML5 Canvas so it was unique and somewhat versatile.
The process starts out as quite a difficult task. How on earth do you re-create a logo by drawing some lines? Well, to cut a long story short, I opened it up in my favourite vector drawing app (the one I used to create it) and just copied the x,y coordinates of the anchor points on to an HTML5 canvas that was the same size as the artboard the illustration was on.
First you’ll need a canvas element on your HTML5 page. Something like:
Pay attention to the width and height declarations since this is the canvas size (in pixels) you’re drawing on.
<canvas id="logo" height="102" width="102">Browser says no...<canvas></canvas>
The end result is pretty cool. All of this:
function drawLogo(target) { var element = document.getElementById(target); var canvas = element.getContext("2d"); /* Originally drawn on 102px x 102px canvas (it wouldn't go smaller without distorting!) Use this scale (the original canvas size) to calculate the appropriate scaling values for the intended target canvas. */ var oScaleY = 102; var oScaleX = 102; canvas.scale ((element.width / oScaleX), (element.height / oScaleY)); //Scale the canvas to fit the canvas object's width height. canvas.beginPath(); canvas.moveTo(5, 4); // Starting Point canvas.lineTo(5, 30); canvas.lineTo(23, 30); canvas.lineTo(23, 40); canvas.lineTo(5, 40); canvas.lineTo(5, 66); canvas.lineTo(23, 66); canvas.lineTo(23, 77); canvas.lineTo(5, 77); canvas.lineTo(5, 103); canvas.lineTo(90, 103); canvas.lineTo(98, 90); canvas.lineTo(90, 77); canvas.lineTo(79, 77); canvas.lineTo(79, 66); canvas.lineTo(84, 66); canvas.lineTo(92, 53); canvas.lineTo(84, 40); canvas.lineTo(79, 40); canvas.lineTo(79, 30); canvas.lineTo(92, 30); canvas.lineTo(100, 17); canvas.lineTo(92, 4); canvas.closePath(); // Seemingly the gradient size must be based on the original drawn canvas. var grad = canvas.createLinearGradient(0,oScaleX,0,0); grad.addColorStop(0, "rgb(46,92,139)"); grad.addColorStop(0.25, "rgb(0,113,188)"); canvas.fillStyle = grad; // Set the gradient as the fill colour. canvas.fill(); // Fill the closed path (with the gradient) canvas.fillStyle = "rgb(255,255,255)"; // Set a new fill colour (white) canvas.fillRect(29, 30, 44, 10); // Create a filled (white) rectangle. canvas.beginPath(); canvas.moveTo(29, 66); canvas.lineTo(29, 77); canvas.lineTo(64, 77); canvas.lineTo(64, 81); canvas.lineTo(73, 81); canvas.lineTo(73, 62); canvas.lineTo(64, 62); canvas.lineTo(64, 66); canvas.closePath(); canvas.fill(); } drawLogo("logo"); // Actually draw the logo
Gives you something looking like this:
Anyway, I’m not here to teach you how to re-create my company logo in HTML5, needless to say, that’s what it looks like. The JavaScript function above does some clever scaling so if your intended target HTML5 canvas element defines its height and width as something other than what you drew the original anchor points on, it’ll scale to that appropriately. To explain, I originally drew this on a 102px x 102px canvas so all the canvas.lineTo declarations relate to that size canvas. Say I want to scale it up to say 300px x 300px, I can use the canvas.scale method to achieve that so this bit of code calculates the scaling factor and applies that to the canvas which helps to retain quality when it’s scaled unlike bitmaps which, as we know, don’t scale well.
/* Originally drawn on 102px x 102px canvas (it wouldn't go smaller without distorting!) Use this scale (the original canvas size) to calculate the appropriate scaling values for the intended target canvas. */ var oScaleY = 102; var oScaleX = 102; canvas.scale ((element.width / oScaleX), (element.height / oScaleY)); //Scale the canvas to fit the canvas object's width height.
Now, as we look at the site on a desktop PC, the logo looks great and is razor sharp. It’ll scale up nicely in multiples of 2 by adjusting the canvas element’s width and height properties and retain its sharpness but values other than that will give you blurred edges. Still, SVG does the same thing as far as I can tell from my own eyesight.
So what’s the problem? Well, given that the new site is supposed to be responsive design and degrade (I think I’ll call it re-grading since there’s no loss of functionality for my site) for mobile and tablet, when you look at that image on a mobile or tablet, it looks blurred because the devicePixelRatio is greater than 1 and the image is scaled up by the device but retains the original dimensions as defined on the canvas element itself.
On a Nexus 5 with a devicePixelRatio of 3 (at least that’s what its browser tells me it is) this blurriness is quite profound and looks something like this which, for a sharp site, looks shocking compared to the HTML5 SVG above.
So, the fix to me seems logical: you should scale the canvas element up by the declared devicePixelRatio amount, let the scaling calculation in the drawing function handle the actual size that comes out and scale back using CSS.
So, here’s the extra code before we call our drawLogo function:
// Upscale the canvas element for mobile HiDPI screens. // It gets scaled down again to the original size by CSS. // Keeps the HTML5 canvas sharp on all screen types. var canvas = document.getElementById("logo"); canvas.width = canvas.width * window.devicePixelRatio; canvas.height = canvas.height * window.devicePixelRatio;
To retain the 102px x 102px image size that we originally intended for our canvas (remember this works for other sizes too!) and for it to look good (not just a bigger image) we need to use CSS to keep the canvas element’s dimensions at what we originally intended for our canvas.
canvas { height: 102px; width: 102px; }
In essence what I’ve done is to increase the dimensions of the canvas element proportionally to the devicePixelRatio so, if the devicePixelRatio is 3, the in-flight JavaScript resizes the entire element to: width = (102 x 3), height = (102 x 3). The increased size of the canvas element is passed to the drawLogo function which creates an image 3 times the size. This is then drawn on to the canvas which is then styled using CSS back down to 102px. The best way to think of this is like using an oversized PNG scaled down within the img dimensions so it looks OK on mobile devices.
The end result is images that are sharp and consistent across desktop, tablet and mobile screens:
NB: Please remember that I’ve only been able to test this with the limited hardware available to me, all of which are Android devices. Nexus 5, Nexus 7 (2012 and 2013), Samsung Galaxy Nexus and of course my Windows 8.1 desktop. If you get different or inconsistent results with your devices, please let me know!