Drawing Crisp Lines on the HTML5 Canvas

When we came up with the idea to build Canvas2D as a wrapper around the HTML5 Canvas, we first had to learn a bit about the HTML5 Canvas element itself. One of the first scenes we drew was:

The image above is drawn with our Canvas2D implementation, in its current incarnation. But, the first image didn’t look like this … it actually looked like this:

 
Notice the difference ? Let’s zoom in on both images and compare them:

Now see the difference? And both are drawn using the same HTML5 Canvas API method:

  ctx.strokeRectangle(left,top,width,height);

And still our first 1 pixel vertical and horizontal lines were not crisp, but in stead looked like semi-transparent 2 pixel lines. We knew it had something to do with anti-aliasing, but still we just couldn’t figure this out. Why would all implementations do this to “simple” straight vertical and horizontal lines?

Of course we started googling, but even then nothing really turned up. Until finally I found a blogpost by Ben Galbraith. The post seemed to describe the symptoms we encountered, but the cause was a different one – we didn’t have our browser zoomed-in one click, hell we had the problem on all browsers. Since this seemed to be the first reference to this kind of problem, I decided to submit a comment and ask for help:

Hi Ben,

I came across you post looking for "crisp canvas lines".
You describe the problem I'm facing, but zooming appears
not to be the cause of my problem.

If you like, please take a look at the following example:

var ctx = document.getElementById("canvas").getContext("2d");
ctx.save();
// not crisp
ctx.moveTo(33,10);
ctx.lineTo(33,90);
ctx.stroke();
// crisp
ctx.fillRect(66,10,1,80);
ctx.restore();

When I use the "default" way of drawing lines, I get the impression
that two anti-aliased(?)/semi-transparent lines are drawn right next
to each other, resulting in a 2px dark-grey line, where I would expect
to be able to draw a crisp 1px black line.

It almost looks like your first example and I would expect it to look
like your expected example. The only way I can draw these crisp lines,
is by using the fillRect method to fill a rect of width 1.

Do you know if this behaviour is "by design" ? How do you go about
drawing these crisp lines ?

Thanks a lot in advance.

At that point, we decided to move along and continue with the first release of Canvas2D including the non-crisp lines and solve the problem in a subsequent release.

While looking for examples to use in our introductory tutorials, we decided to reuse those of the Mozilla Developer Center. This allowed us to show that Canvas2D was a drop-in replacement for the native Canvas API. While going through these examples, we encountered the lineWidth example. And suddenly it all made sense:

Time to blame myself for not reading the documentation available:

https://developer.mozilla.org/en/Canvas_tutorial/Applying_styles...

nicely explains why this is happening and how to resolve it.

The result boils down to:

var ctx = document.getElementById("canvas").getContext("2d");
ctx.save();
// not crisp
ctx.moveTo(25,10);
ctx.lineTo(25,90);
ctx.stroke();
// one way to solve it
ctx.fillRect(50,10,1,80);
// the right way to solve it
ctx.moveTo(75.5,10);
ctx.lineTo(75.5,90);
ctx.stroke();
ctx.restore();

One of the goals of Canvas2D is to be a wrapper around the different browser-implementations of the HTML5 Canvas element. We try to accomplish this by smoothing the differences and filling in the missing parts. While doing so we also try to add more support for end-users, thus making the Canvas easier for them to use. The crisp line issue was yet another example of this: adding support for crisp lines where an end-user would expect them and keeping the anti-aliased drawing style where needed.

Whenever a user of Canvas2D currently wants to draw a line, we apply the required logic and add half a pixel if needed. This behavior is of course configurable, to still take advantage of the anti-aliasing. One example where we needed this ourselves is in the implementation of CanvasText, which implements text rendering through the line-drawing functions of the canvas. Comparing text-rendering with crisp lines or without clearly shows the difference. At small font-sizes the characters are a lot nicer to read and at bigger font-sizes they are much smoother:

Of course, this only applies to browsers that don’t support text-rendering, where we fall back to rendering text through CanvasText. On browsers that support text-rendering, we won’t be able to render “crisp” text at all, unless we really, really want to.

We will probably have to tune this feature a bit more, but currently what should be crisp is crisp, what ought to be fuzzy is fuzzy, which brings us one step closer to the Canvas2D.DoWhatIMean() method, rendering …


… crisp vertical and horizontal lines and anti-aliased fonts.

Leave a Reply