Fixing Canvas Blur by Respecting Pixel Density

Pixel Density #

Canvas newcomers may notice that curved lines and text can appear blurry or pixelated. In other words, like the first (red) canvas here, instead of the second (blue) one:


if you are using a low pixel density device, these might look the same

Both of these canvases take up the same screen space: their CSS size is set to an identical width: 300px and height: 150px.

But the red canvas has its canvas.width and canvas.height also set to 300 and 150. This means for every CSS pixel, there is 1 canvas pixel. CSS pixels represent physical space, there are (usually) 96 pixels-per-inch, but device manufacturers may want to create screens that look nicer, so they pack more pixels in, often 1.5x, 2x, or 3x as many real physical pixels. This is known as the device’s pixel ratio and can be queried easily in the JavaScript via window.devicePixelRatio. The value on your machine is:

So setting both the CSS and the Canvas width/height to the same values ends up looking not as crisp as it would otherwise. To fix this, we want to scale the canvas size up by the pixel ratio, while keeping the CSS the same, so that a larger canvas is condensed into the smaller CSS box for it. We also need to scale all drawing on the canvas, which we do with a call to context.scale.

For the blue canvas, the code looks like this:

const width = 300;
const height = 150;
// Setup for the second (blue) canvas
const pixelRatio = window.devicePixelRatio;
const can2 = document.getElementById('canvas2');
can2.width = width * pixelRatio;
can2.height = height * pixelRatio;
can2.style.width = width + 'px';
can2.style.height = height + 'px';
const ctx2 = can2.getContext('2d');
ctx2.scale(pixelRatio, pixelRatio);

Any time you reset your canvas context (with ctx.reset() or settings canvas.width or canvas.height), you will need to again scale the canvas transform:

ctx2.scale(pixelRatio, pixelRatio);

Pixel Ratio can change dynamically #

The pixel ratio is not just affected by the pixel density of the screen, but also by the user zooming, for example with CTRL or Command +/- shortcuts.

To dynamically update the pixel ratio, you will need to use an event listener onto a MediaQueryList, which you can get from calling matchMedia with the current resolution:

// Dynamically listen for pixel ratio changes:
function updatePixelRatio() {
  const pr = window.devicePixelRatio;
  // call code to update your canvas size with the new pixel ratio here
  // but on this page I'm just gonna the value it on screen:
  pixelRatioBox.innerText = pr;
  // listen to call this method again whenever it changes from current value:
  matchMedia(`(resolution: ${pr}dppx)`).addEventListener('change',
    updatePixelRatio, { once: true })
}

updatePixelRatio();

The value for this page right now is:

If you zoom this page in a desktop browser, the value will update.