State of Nearest-neighbor Interpolation in Canvas

November 8th, 2013

Nearest-neighbor interpolation is the bread and butter of pixel art and a staple for many indie games. It’s what allows us to create crisp and sharp pixelated graphics, responsively without exporting all our graphics upscaled via image editing software.

So, what’s our options in HTML5? Well, they are kind of all over the place, depending on what browser(s) you need to support. I’ve kind of been waiting years for everything to catch up, but sadly it hasn’t… well not really. I’m still stuck with exporting up-scaled graphics to do cross-browser support. The point of this article is to offer a quick reference guide to what methods (and hacks) are available and what is supported on which browser.

Nearest-neighbor Interpolation Support

Nearest-neighbor Method Comparison

As you can see, if we want full support, we are still stuck in Photoshop, exporting our upscaled pixel graphics at full size. Technically, we could pre-render using the getImageData method, but it’s incredibly slow and non-efficient, especially for lots of sprite work. If you don’t care about Internet Explorer support (shame on you!), upscaling via drawImage is definitely the method to use.

Surprisingly Chrome has been dragging their feet the most in this regard. The CSS image-rendering property still doesn’t function properly and that’s something I have been waiting years for. Chrome also has no support for nearest-neighbor interpolation when using browser zoom or using a higher resolution display (retina). I know, the specification doesn’t exactly call for this, but I would expect zoom to behave as defined, and not default to bicubic scaling. Speaking of browser zoom and retina support, let’s see how the other browsers perform in this regard.

Zoom & Retina Support

These examples use the same set as above, but zoomed in with browser zoom (or using a retina device). This may seem trivial for some, but with users now zooming in on mobile and tablets, and retina screens on the rise, this is now even more important than ever.

Nearest-neighbor Zoom Comparison

This one is a little hard to spot the differences, especially if you aren’t viewing the image at full size. Surprisingly though, almost no browsers support this. But it’s actually a bit weird that they do at all as the use cases are fairly obscure (well, not for pixel artists making HTML5 games). Firefox, Safari and Mobile Safari all support retina/zoom nearest-neighbor scaling using the CSS method. Even our tried and true fallback method won’t save us now.

But wait, there’s more! If you use CSS image-rendering modes on the rest of them (the Upscaled via CSS method already had this applied), lots of them will magically support the zoom/retina! This chart shows which browsers support it. Like the above example, it is only working in Firefox, Safari and Mobile Safari.

Nearest-neighbor Zoom Comparison - Revised

So the best method, in the end, remains the same. Use “Upscaled via drawImage + image-rendering” across the board if you don’t need IE support, or stick with “Upscaled via Export (fallback) + image-rendering” if you do need IE support.

Code!

Here’s the full code used in these examples and test cases.

CSS

canvas {
	image-rendering: optimizeSpeed;
	image-rendering: -moz-crisp-edges;
	image-rendering: -webkit-optimize-contrast;
	image-rendering: optimize-contrast;
	-ms-interpolation-mode: nearest-neighbor;
}

#upscaledcss {
	height: 120px;
	width: 120px;
}

#imagedatasource {
	display: none;
}

JavaScript

var sourceImage = new Image();
sourceImage.src = "http://vaughnroyko.com/jsfiddle/pixels.png";
var sourceImageUpscaled = new Image();
sourceImageUpscaled.src = "http://vaughnroyko.com/jsfiddle/pixels-large.png";

sourceImageUpscaled.onload = function() {

	//Upscaled via drawImage (canvas)
	var upscaledCanvas = document.getElementById('upscaledcanvas').getContext('2d');
	upscaledCanvas.mozImageSmoothingEnabled = false;
	upscaledCanvas.webkitImageSmoothingEnabled = false;
	upscaledCanvas.msImageSmoothingEnabled = false;
	upscaledCanvas.imageSmoothingEnabled = false;
	upscaledCanvas.drawImage(sourceImage, 0, 0, 8, 8, 0, 0, 120, 120);

	//Upscaled via CSS
	var upscaledCSS = document.getElementById('upscaledcss').getContext('2d');
	upscaledCSS.drawImage(sourceImage, 0, 0, 8, 8);

	//Upscaled via Pattern (hack)
	var upscaledPattern = document.getElementById('upscaledpattern').getContext('2d');
	upscaledPattern.mozImageSmoothingEnabled = false;
	upscaledPattern.webkitImageSmoothingEnabled = false;
	upscaledPattern.msImageSmoothingEnabled = false;
	upscaledPattern.imageSmoothingEnabled = false;
	upscaledPattern.scale(15, 15);
	upscaledPattern.fillStyle = upscaledPattern.createPattern(sourceImage, 'repeat');
	upscaledPattern.fillRect(0, 0, 120, 120);

	//Upscaled via getImageData (slow)
	var imageDataSource = document.getElementById('imagedatasource').getContext('2d');

	imageDataSource.drawImage(sourceImage, 0, 0);
	var imgData = imageDataSource.getImageData(0,0,sourceImage.width,sourceImage.height).data;

	var upscaledImageData = document.getElementById('upscaledimagedata').getContext('2d');

	for (var x = 0; x < sourceImage.width; ++x) {
		for (var y = 0 ; y < sourceImage.height; ++y) {
			var i = (y * sourceImage.width + x) * 4;
			var r = imgData[i];
			var g = imgData[i + 1];
			var b = imgData[i + 2];
			var a = imgData[i + 3];
			upscaledImageData.fillStyle = "rgba(" + r + ", " + g + ", " + b + ", " + (a / 255) + ")";
			upscaledImageData.fillRect(x * 15, y * 15, 15, 15);
		}
	}

	//Upscaled via Export (fallback)
	var upscaledCanvas = document.getElementById('upscaledexport').getContext('2d');
	upscaledCanvas.drawImage(sourceImageUpscaled, 0, 0, 120, 120);

}

HTML

<canvas id="upscaledcanvas" width="120" height="120"></canvas>
<canvas id="upscaledcss" width="8" height="8"></canvas>
<canvas id="upscaledpattern" width="120" height="120"></canvas>
<canvas id="imagedatasource" width="8" height="8"></canvas>
<canvas id="upscaledimagedata" width="120" height="120"></canvas>
<canvas id="upscaledexport" width="120" height="120"></canvas>

Hosted Preview/Demo

Demo

This entry was posted in Blog and tagged , , , .

7 Responses to State of Nearest-neighbor Interpolation in Canvas

February 18th, 2014
Hans Gerwitz says:

Thank you much for publishing this!

Reply
February 27th, 2014
Isaac Keller says:

Thanks so much for this, as It seems to be basically the only way to scale pixel art up correctly and I was able to continue my tooltip development for the game Starbound. So this was huge!

Reply
March 26th, 2014
Kristofer says:

Excellent post!!

Reply
March 26th, 2014
Maël says:

Is there an issue on Chrome’s bug tracker ?

Reply
December 6th, 2015
Sam Keddy says:

Tried the demo on my phone and even the clearer ones are slightly blurry. Sad that this is still a problem in 2015.

Reply
January 25th, 2016
phelm says:

the ‘image-rendering’ css property can now be set to ‘pixelated’

http://caniuse.com/#feat=css-crisp-edges

Reply
January 25th, 2016
Vaughn Royko says:

Cool! I have seen this indeed. This article could use a “Part 2”.

Reply

Leave a Reply

*

*

TOP