will.institute

by Will Lesieutre

Device Pixels

The previous post covered adjusting the canvas size to match how large it’s being drawn on the web page. And if you’re reading on a computer with a 1080p screen or similar, it looked pretty good. But ever since the iPhone 4 and its “retina display” stepped up the pixel density much higher than historical norms, there’s one more piece of this pixelation puzzle.

When Apple subdivided every pixel on their screen into two rows and two columns, they couldn’t treat these like normal pixels any more - an object on screen defined as 10 pixels wide would be only half its intended size if it were displayed using 10 of these tiny new pixels.

Instead, they used multiple physical pixels to make up each pixel of size defined in CSS or HTML, giving objects the same size as before but allowing for sharper detail. In those first retina screens they used 2 hardware pixels for every 1 pixel of a web page layout. That 2:1 ratio is called the “device pixel ratio,” and it tells us that the width and height of the canvas both need to be doubled to display a sharp image on that screen. Newer screens with even higher pixel densities have driven that up even further with a 3:1 ratio.

Here’s a canvas with the same line drawing demo as before. If you’re reading on a standard DPI screen this won’t look much different. But if your screen has extra pixels that weren’t accounted for previously, this one finally matches the screen’s sharpness:

This canvas draws a line account for increased pixel counts on high DPI screens

function sharpLine(color) {
    const canvas = document.getElementById('dpiCanvas');
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio
    canvas.width = dpr * canvas.scrollWidth;
    canvas.height = dpr * canvas.scrollWidth / 2;
    ctx.lineWidth = 10;
    ctx.strokeStyle = color;
    ctx.moveTo(0, 0);
    ctx.lineTo(canvas.width, canvas.height);
    ctx.stroke();
}

Proportion problems

The line’s width is set in pixels on the canvas, and with the device pixel ratio factored in those pixels have become very small. On an iPhone X with 458 pixels per inch, a 10 pixel wide line means about half a millimeter.

Measuring in pixels isn’t usually the right approach anyway. If the canvas gets larger, most of the time it makes sense to have its contents get larger along with it. Instead of drawing a 10 pixel wide line, the canvas below draws it at 10 percent of the canvas width. The proportions of this sketch stay the same regardless of its size:

This canvas shows a line drawn in proportion to the canvas size

function wideLine(color, linePercent) {
    const canvas = document.getElementById('proportionalCanvas');
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio
    const width = dpr * canvas.scrollWidth
    canvas.width = width;
    canvas.height = width / 2;
    ctx.lineWidth = width * linePercent / 100;
    ctx.strokeStyle = color;
    ctx.moveTo(0, 0);
    ctx.lineTo(canvas.width, canvas.height);
    ctx.stroke();
}

Still not responsive

It’s finally drawing enough pixels to match the screen, but it doesn’t yet adapt to changes in size. If it’s drawn at a small size and then the window gets larger, the canvas can still become pixelated until it’s manually redrawn.