Fixing HTML5 2d Canvas Blur

Understand how to use DPI to fix Canvas Blur

The Canvas Blur Issue

(If you just want the solution scroll down to Solution)

A few months ago I found myself building a Visualization Component for a React Application. I realize “Visualization Component” sounds purposefully vague. Here’s a basic example:

You’ll notice the above charts out the different nodes according to a from, to multi-dimensional array in the data object. It’s super cool but who cares? That’s not the point of this article.

First and foremost, if you’ve worked with Canvas at all, you’ll notice there isn’t any Canvas Blur in the above demo. All the circles and arrows are made with clean lines, which to those who haven’t seen Canvas Blur must sound kind of obvious and mundane, but getting there can actually be a frustrating milestone in beginning 2d Canvas Development.

To the uninitiated(and to those who just want to be sure that they’ve finally found the right article) the issue that I’m referring to looks like this:

A grotesque blurry circle.

The above is in my browser and the live example can be found here:

Note: It may look a bit different on your browser/mobile device. That is fine. The aim of this article is to demonstrate how to fix the blur, but the amount of blur itself often depends on the browser/device you are using to view the Canvas Application.

(I’m going to go into the background of writing this answer and the trials and tribulations of Development for a minute, but if you would like to just skip that, head down until you see How Do We Fix It? )

I need to point out that initially in the development of my Canvas Component it was not clean. The lines had an unusual blur to them and every line seemed to have pixelation that I sometimes could just barely tell was there. Of course, upon increasing the size or quantity of my nodes, I could absolutely tell that something was not right, but I had no idea how to fix it — so I did what every Developer who doesn’t know a solution should do. I Googled.

My Search For An Answer

I found this StackOverflow Question circa 2013

With a great answer — that, to be honest, I did not understand at all.

This is a good answer, but outdated, and by no means the best answer. For the use case that I wanted to use it for I could not for the life of me understand the code given. To me it wasn’t explained in depth enough but it did link me to another article on a secondary site — html5rocks(a generally great site for HTML5 tips and tricks), and so I followed the rabbit hole down.

This article is totally great — but, again, as I would find out later it is wholly outdated, and even as I read it I did not understand. It was too verbose and, though it tried to be relatively simple(and even included pictures to explain its points, for gods sake!) I found myself wholly confused and muttering to myself. I tried and I read it over a few times, then gave up and decided to plug in the singular code example given. I got it to work… kind of. It wasn’t a great implementation and it seemed to be hanging on by a shoestring, but it worked. The only problem was that I still didn’t understand it, and therefore couldn’t really duplicate it.

I searched other places and implemented weird solutions like using the translate method to move everything half a pixel over(no, that didn’t really work) but for some reason this html5rocks article kept cropping up. It had somehow become the premiere resource pointed to for this issue, even though it was out of date and had been for quite awhile — So much so that one of the important concepts you needed to learn within it(the backing store) isn’t even a relevant concept in Canvas any longer.

This all being the case, when I did finally figure it out and understood DPI and Canvas Scaling, I decided to write this article so that others could more easily solve this frustrating issue.

How Do We Fix It?

(to skip the explanation of how it gets fixed scroll down to Solution)

It is so much simpler than you think, I guarantee it. It just takes a minute to wrap your head around, so bare with me and I will explain everything you need to know.

Device Pixel Ratio

Your screen is made of pixels! Shocking information, I know. This is a Web Development blog(and it’s 2017) so I won’t do you the disservice of explaining what a pixel is. Anyhow, the number of pixels the screen is made of depends on what type of screen it is. If you connect your computer to a 55" ultra high definition 4k computer monitor compared to a Smartphone or Tablet they will obviously not have the same number of pixels. There just isn’t enough space or power to manage it on the smaller devices.

This means that every device has a certain and finite number of pixels that are available to it. You’ll notice though that you don’t generally see a difference in text or image quality when comparing your phone to your computer screen and some of you may not remember when that was a thing(I’m getting older I guess), so from that we can derive that your system/browser will automagically define the best number of pixels to use for pretty much everything! In most every web interaction it does a fantastic job at this, but when it comes to HTML5 2d Canvas, it tries its best — but has a tendency to fail horribly.

The Problem

When a Browser interprets a web page it will interpret the page like this:

  1. A DOM representation is formed from the HTML the Browser receives.
  2. Styles are parsed and loaded to form a representation called the CSSOM.
  3. A “render tree” is created.
  4. Elements are rendered and painted to the screen with the appropriate styling.

The problem lies between the first step and the second. In the initial DOM representation the attributes width and height are used to declare size. Some elements have nothing set(undefined) that will render them to 0, such as any div tag. Some elements will have default values such as Canvas, which always automatically has 300px width and 150px height.

To demonstrate this we can look at the result of this JavaScript Fiddle:

Indeed if we were to always know exactly how large we wanted our Canvas element to be, and kept its size static, we would be able to create a Canvas without Canvas Blur or with very little. As an overall generalization, this is not the case. Often times we want our Canvas element to be dynamic, to scale appropriately, and to change fluidly within our application.

It’s for this reason that we begin to dabble in CSS. We may think that by altering CSS alone, as with most other web elements, we can make the element dynamically scale to fit the width and height of its container and to retain a clear-cut, crisp image. We would be wrong.

canvas {
width: 100%;
height: 100%;
}

Here’s an example of what may happen when altering CSS without adjusting the Canvas size with the appropriate DPI:

You’ll notice that the text is extremely grainy and the image has scaled far too large for the screen.

I think it’s about time we fix this.

Solution

What we end up doing to solve our DPI issue is this:

//get DPI
let dpi = window.devicePixelRatio;
//get canvas
let canvas = document.getElementById('myCanvas');
//get context
let ctx = canvas.getContext('2d');
function fix_dpi() {
//get CSS height
//the + prefix casts it to an integer
//the slice method gets rid of "px"
let style_height = +getComputedStyle(canvas).getPropertyValue("height").slice(0, -2);
//get CSS width
let style_width = +getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);
//scale the canvas
canvas.setAttribute('height', style_height * dpi);
canvas.setAttribute('width', style_width * dpi);
}

A fixed example:

What we end up doing is taking the Canvas that we’ve rendered(sitting within our Canvas element), which depending on whether the window was sized down or up, has either grown or shrunk — and multiplied that new size by however many pixels are needed on our device so that we get a clear image. We set that as our attribute size on our element which is, really, a viewport to the Canvas we’ve rendered, and this ensures that our Canvas elements are clear from then on.

One last example:

//get the canvas, canvas context, and dpi
let canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
dpi = window.devicePixelRatio;
function fix_dpi() {
//create a style object that returns width and height
  let style = {
height() {
return +getComputedStyle(canvas).getPropertyValue('height').slice(0,-2);
},
width() {
return +getComputedStyle(canvas).getPropertyValue('width').slice(0,-2);
}
}
//set the correct attributes for a crystal clear image!
  canvas.setAttribute('width', style.width() * dpi);
canvas.setAttribute('height', style.height() * dpi);
}
function draw() {
//call the dpi fix every time
//canvas is redrawn
fix_dpi();

//draw stuff!
ctx.strokeRect(30,30,100, 100);
ctx.font = "30px Arial";
ctx.fillText("Demo!", 35, 85);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);