FrozenHeart FrozenHeart - 9 days ago 5
HTML Question

How to avoid "antialiasing" effect in HTML5 Canvas

Why does the following code changes line's width to 2 pixels instead of 1 eventually?

var canvas = document.getElementById('c');
var context = canvas.getContext('2d');

function clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}

function draw() {
context.save();
context.beginPath();
context.scale(zoom, zoom);
context.moveTo(100, 50);
context.lineTo(100, 100);
context.restore();
context.stroke();
}

var zoom = 1.0;

$('#c').mousewheel(function(e) {
if (e.originalEvent.deltaY < 0) {
zoom *= 1.1;
} else {
zoom /= 1.1;
}
clear();
draw();
});

draw();


You can try it out here -- https://jsfiddle.net/818j0646/.

Just try to zoom in / out and you'll notice what I'm talking about:

enter image description here

How can I avoid this behavior? I need my line to stay 1 width always, without such "antialiasing" effect.

K3N K3N
Answer

You can add support for line-width relative to zoom (just make sure restore() is applied after stroke or all settings will return before anything is drawn):

var lineWidth = 1;                       // line width

function draw() {
  context.save();
  context.beginPath();
  context.scale(zoom, zoom);
  context.lineWidth = zoom * lineWidth;  // line-width * zoom
  context.moveTo(100, 50);
  context.lineTo(100, 100);
  context.stroke();
  context.restore();                     // restore last
}

Modified fiddle

If you want to keep about 1 pixel regardless of scale you can invert the line-width formula:

  context.lineWidth = 1 / (zoom * lineWidth);

Result

However, there will be small rounding errors affecting the anti-aliasing processing on some scales.

Solution

The only real way to avoid this issue is to manually apply a matrix to points representing the line, make the values integers, then render the result of those as a line using the Bresenham or, IMO, the better and faster EFLA algorithm and via ImageData, pixel-by-pixel, and finally push that to the bitmap. You can wrap all this into a single function of course.

This also mean you need to track the matrix. In newer browsers you can use currentTransform and soon getTransform() to obtain current transformation, or you can use a custom matrix solution for cross-browser and backward compatibility (there are many out there, here is mine).

There is currently no way to turn off anti-aliasing for vectors rasterized to the canvas.

Demo

window.onload = function() {
  
var canvas = document.getElementById('c');
var context = canvas.getContext('2d');
var matrix = new Matrix();
var zoom = 1.0;

function clear() {
	context.clearRect(0, 0, canvas.width, canvas.height);
}

function draw() {
  matrix.reset();  // replaces save/restore
  matrix.scale(zoom, zoom);

  // manually draw line via matrix and EFLA
  line(context, 100, 50, 100, 100);
}

// custom line function
function line(context, x1, y1, x2, y2) {

  // instead of transforming context, apply matrix to points:
  var p1 = matrix.applyToPoint(x1, y1);
  var p2 = matrix.applyToPoint(x2, y2);

  // create a bitmap (for demo), or obtain an existing one (getImageData)
  var idata = context.createImageData(canvas.width, canvas.height);
  var data32 = new Uint32Array(idata.data.buffer);
  
  _line(data32, p1.x|0, p1.y|0, p2.x|0, p2.y|0, canvas.width);
  context.putImageData(idata, 0, 0);
}

// EFLA line algorithm
function _line(data, x1, y1, x2, y2, w) {

	var dlt, mul,
		sl = y2 - y1,
		ll = x2 - x1,
		yl = false,
		lls = ll >> 31,
		sls = sl >> 31,
		i;

	if ((sl ^ sls) - sls > (ll ^ lls) - lls) {
		sl ^= ll;
		ll ^= sl;
		sl ^= ll;
		yl = true
	}

	dlt = ll < 0 ? -1 : 1;
	mul = (ll === 0) ? sl : sl / ll;

	if (yl) {
		x1 += 0.5;
		for (i = 0; i !== ll; i += dlt)
			setPixel(data, (x1 + i * mul)|0, y1 + i, w)
	}
	else {
		y1 += 0.5;
		for (i = 0; i !== ll; i += dlt)
			setPixel(data, x1 + i, (y1 + i * mul)|0, w)
	}
}

// Set a pixel (black for demo)
function setPixel(data, x, y, w) {data[y * w + x] = 0xff000000}

$('#c').mousewheel(function(e) {
  e.preventDefault();
  if (e.originalEvent.deltaY < 0) {
    zoom *= 1.1;
  } else {
    zoom /= 1.1;
  }
  clear();
  draw();
});


draw();
  };
body {overflow:hidden}
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js"></script>
<script src="//cdn.rawgit.com/epistemex/transformation-matrix-js/master/matrix.min.js"></script>

<canvas id="c" width="800" height="600"></canvas>

As fiddle