Désactivation de l’anti-aliasing du canvas 2D

L'anti-aliasing est activé par défaut dans le canvas. Il permet de limiter la perception de l'effet d'escalier que la juxtaposition de pixels aux couleurs contrastées peut générer du fait de leur forme rectangulaire.
Ce traitement peut se révéler gênant dans certains cas (dessin au pixel, jeu old school, etc.), car il n'y a plus de certitude sur les pixels qu'une opération graphique vient modifier.
Par exemple, sur un canvas où on aura d'abord dessiné un rectangle jaune :
context2d.strokeStyle = "rgb (255, 255, 0)";
context2d.fillStyle = "rgb (0,255, 0)";
context2d.fillRect (2, 2, 1, 1);
context2d.strokeRect (1, 1, 2, 2);
Anti-aliasing avec fillRect () et strokeRect ()
Dès lors, comment s'y prendre pour désactiver l'anti-aliasing dans le canvas ?
Mise à jour du 07/09/2018 : la propriété imageSmoothingEnabled, qui n'est pas évoquée ici, ne sert qu'à désactiver l'anti-aliasing pour l'affichage d'images.

La solution

L'image réelle produite à l'écran est une version retraitée d'une image virtuelle plus grande, chaque pixel de l'image réelle correspondant à un bloc de pixels dans l'image virtuelle qu'il est possible d'adresser par des coordonnées décimales.
La solution passe par un décalage des coordonnées de l'image réelle dans l'image virtuelle de telle sorte que l'anti-aliasing soit neutralisé. Ce décalage doit être effectué généralement par un appel translate (0.5, 0.5) et spécifiquement par un décalage qui dépend de la fonction utilisée pour dessiner :
  • s'agissant de fillRect (), il faut retirer 0.5 à l'abscisse et à l'ordonnée en pixels ;
  • s'agissant de strokeRect (), il faut retirer 0.5 à l'abscisse et à l'ordonnée uniquement si l'épaisseur du trait (lineWidth) est paire.
Et pour les autres fonctions ? Il n'existe pas de solution. Pour dessiner une droite oblique, il faudra implémenter un algorithme tel que celui de Bresenham en s'appuyant sur un dessin au pixel permis grâce à getImageData () et putImageData (). Retour à l'âge de pierre, donc...

Le code JavaScript

En JavaScript, la solution se traduit par le code suivant pour dessiner une grille de rectangles dotés d'un bord d'une certaine épaisseur à l'aide de strokeRect () et fillRect () :
var context2D, strokeOffset, left = 3, top = 5, width = 1, height = 1, lineWidth = 4, i, j, x, y;

context2D = document.getElementById ("tagCanvas").getContext ("2d");
context2D.translate (0.5, 0.5);
context2D.fillStyle = "yellow";
context2D.fillRect (0 - 0.5, 0 - 0.5, 400, 400);
context2D.strokeStyle = "rgb(255,0,0)";
context2D.fillStyle = "rgb(0,255,0)";
context2D.lineWidth = lineWidth;
strokeOffset = (lineWidth >> 1) - (lineWidth & 1 ? 0 : 0.5);
for (j = 0; j != 10; j ++) {
	y = top + j * (height + lineWidth);
	for (i = 0; i != 10; i ++) {
		x = left + i * (width + lineWidth);
		context2D.fillRect (x + lineWidth - 0.5, y + lineWidth - 0.5, width, height);
		context2D.strokeRect (x + strokeOffset, y + strokeOffset, width + lineWidth, height + lineWidth);
	}
}
Dans cet exemple, left, top, width, height et lineWidth correspondent exactement aux dimensions suivantes dans l'image réelle :
Coordonnées et dimensions pour fillRect () et strokeRect ()

L'exemple

Cliquez ici pour accéder à une page de test minimaliste. Vous pourrez visualiser le code et le récupérer pour travailler avec.

La logique

Dans le contexte 2D du canvas (communément désigné comme le canvas, par abus de langage assimilant l'élément à un de ses contextes), l'anti-aliasing est activé par défaut.
En fait, comme le précise la spécification du contexte 2D, ce qui est donné à voir à l'écran correspond au retraitement d'une image virtuelle dont les dimensions peuvent excéder celles de l'image réelle représentée à l'écran, notamment pour produire un anti-aliasing par la technique dite de l'over-sampling (plusieurs pixels générés au lieu d'un, dont les couleurs sont mélangées pour donner celle du pixel affiché) :
The size of the coordinate space does not necessarily represent the size of the actual bitmaps that the user agent will use internally or during rendering. On high-definition displays, for instance, the user agent may internally use bitmaps with two device pixels per unit in the coordinate space, so that the rendering remains at high quality throughout. Anti-aliasing can similarly be implemented using over-sampling with bitmaps of a higher resolution than the final image on the display.
Dans ces conditions, il ne faut pas s'étonner de pouvoir adresser les pixels de l'image à l'aide coordonnées qui ne sont pas entières, d'une part, et d'obtenir des résultats variables à l'écran, d'autre part.
Par exemple, l'affichage de 20 rectangles avec bord d'épaisseur 1 via strokeRect (), prétendument de 4 pixels de côtés, aux coordonnées x et y telles que x prend une valeur entière et y prend une valeur décimale qui progresse de 0.1 à chaque itération (1.0, 1.1, 1.2, etc.) :
var side = 4, space = 3, x = 2, y = 1, i;

for (i = 0; i != 20; i ++) {
	context2d.strokeRect (x, y + (i / 10.0), side, side);
	x += side + space;
}
Produit en utilisant des couleurs très contrastées telles que du rouge et du jaune, le résultat (ici agrandi avec une loupe sans filtre) est édifiant :
20 appels à strokeRect () avec une ordonnée décimale progressant de 0.1
La contemplation de cette série de rectangles baveux conduit à formuler plusieurs remarques témoignant de l'ampleur de l'imprécision quant à la nature des pixels affectés :
  • un rectangle ne fait pas 4 pixels de côté, mais jusqu'à 6 pixels de côté ;
  • l'apparence d'un rectangle varie chaque fois que y progresse de 0.1 ;
  • ce n'est véritablement qu'à partir de y = 2.5 que le rectangle n'est affiché qu'à partir du pixel d'ordonnée 2.
Le décalage opéré par un appel à translate (0.5, 0.5) ne suffit pas pour corriger le problème. Il faut aussi modifier les coordonnées et les dimensions fournies aux fonctions de dessin comme expliqué plus tôt.
Méfiance, car cette nécessité ne se fait pas sentir toujours immédiatement :
  • Pour fillRect (), la nécessité de retirer 0.5 à l'abscisse et à l'ordonnée en pixels se fait sentir dès le rendu d'un rectangle de 1x1. Par exemple, un rectangle pur vert rgb (0, 255, 0)) :
    fillRect () avec et sans décalage après translate ()
  • Pour strokeRect (), la nécessité de retirer 0.5 à l'abscisse et à l'ordonnée uniquement si l'épaisseur du trait (lineWidth) est paire ne se fait sentir qu'à partir d'une épaisseur de 4 pixels. Par exemple, un rectangle doté d'un bord pur rouge rgb (255, 0, 0) :
    strokeRect () avant et sans décalage après translate ()
Attention ! Les exemples rapportés ont été produits avec Firefox 47.0. Or la spécification du contexte 2D précise bien que le rendu final est à la main de l'agent. En conséquence, il convient de procéder à des vérifications avant de déployer la technique décrite sur d'autres agents...
Désactivation de l’anti-aliasing du canvas 2D