HTML5 Canvas – Crisp lines every time

If you have a look at the following example from Mozilla, you’ll notice that every other line has blurred edges.

https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_lineWidth_example

How can we fix it?

// <![CDATA[
var ctx = document.getElementById('canvas').getContext('2d');
for (i=0;i

The blurred lines are all those with odd widths, e.g 1, 3, 5. Drawing crisp lines using the HTML Canvas requires an understanding of how each line lines up with the pixels on screen. Here is an excellent diagram which explains it nicely.

canvas-grid

As you can see, lines are drawn at exact pixel coordinates, which are essentially grid lines which go in between pixels. So a 1 pixel line has to span 2 pixels. The resulting line will be an approximation only, and will look faded or blurred. A 2 pixel line spans nicely either side of the coordinate lines, and is therefore crisp.

The general solution proposed around the net for this is to add or subtract 0.5 pixels from all coordinates for lines of odd widths. This can become difficult to keep track of, and in my opinion, makes drawing more complicated than it should be. It also means that you need to offset your coordinates if you change your line widths. For simple drawing I propose a simple solution.

There is a translate method which allows you to offset the drawing coordinates of the entire canvas. It is used like so.


context.translate(0.5, 0.5);

This will allow you to use whole integers for drawing with odd line widths. But what if you don’t know the line width ahead of time? How about calculating the offset needed?


var iStrokeWidth = 1;
var iTranslate = (iStrokeWidth % 2) / 2;
context.translate(iTranslate, iTranslate);

This results in 0.5 for odd numbers, and 0 for even ones. Perfect! You can change your stroke width without any need to mess with your coordinates. Here is the Mozilla example, adjusted using this technique.

// <![CDATA[
var ctx = document.getElementById('canvas2').getContext('2d');
for (var i=0; i

The above is an example of how it is possible to call context.translate() at any point. This is how we can change the translation before drawing each line with a new width. However, it is important to undo the applied translation after each line. This is necessary so that we don’t attempt to draw even widths after translating the canvas for uneven ones, and vice-versa. Here is the code to produce the example above.


<canvas id="canvas" width="150" height="150"></canvas>

view raw

canvas.html

hosted with ❤ by GitHub


var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 10; i++) {
var iStrokeWidth = 1 + i;
var iTranslate = (iStrokeWidth % 2) / 2;
ctx.translate(iTranslate, iTranslate);
ctx.lineWidth = iStrokeWidth;
ctx.beginPath();
ctx.moveTo(5+i*14, 5);
ctx.lineTo(5+i*14, 140);
ctx.stroke();
// reset the translation back to zero
ctx.translate(-iTranslate, -iTranslate);
}

Another way to do this would be to store the current translation in a variable and check it is correct for each new line width. This would reduce the number of translations needed, and may speed up execution.

Those with good eyesight may notice that the start and end of each odd numbered line is still blurred. The translation adjusted the starting X position of the lines, which is great, because that has caused them to fall in line with the pixels on screen. But it has also shifted the starting Y position by half a pixel, causing the same problem, just less noticeable. An obvious solution to this would be to only translate one axis. The X axis for vertical lines, and the Y axis for horizontal lines, and that would work in this case. If you know which direction your lines are going ahead of time, this solution is fine. If you don’t then you’ll have to add a basic calculation to ensure you’re shifting the pixels in the right direction. Here is the final solution, adjusted for a single axis only.


var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 10; i++) {
var iStrokeWidth = 1 + i;
var iTranslate = (iStrokeWidth % 2) / 2;
ctx.translate(iTranslate, 0);
ctx.lineWidth = iStrokeWidth;
ctx.beginPath();
ctx.moveTo(5+i*14, 5);
ctx.lineTo(5+i*14, 140);
ctx.stroke();
// reset the translation back to zero
ctx.translate(-iTranslate, 0);
}

view raw

both-fixed.js

hosted with ❤ by GitHub

// <![CDATA[
var ctx = document.getElementById('canvas3').getContext('2d');
for (var i=0; i

It is also important to note that there is no such thing as 0.5 pixels. This is why browsers anti-alias lines, and it gives the illusion that lines are in the correct place. This is most important for arcs, where the fractional pixels of black implied by a whole grey pixel fool our eyes into seeing a smooth line overall. So, in overriding this effect using this method, we may for instance, cause two lines which are 1 pixel apart, but are supposed to look 1.5 pixels apart to actually look 1 pixel apart. Bear this in mind when trying to create pixel perfect canvas drawings.

Leave a comment

Create a website or blog at WordPress.com

Up ↑