Afficher du texte en gros pixels dans une page Web

Pas facile de la jouer rétro avec les technologies du jour ! Afficher un texte en gros pixels comme sur les vieilles bécanes, c'est toute une affaire quand on sait que le moindre texte subit désormais de multiples traitements pour le rendre bien lisible à l'écran. En particulier, ses courbes sont lissées en recourant à l'anti-aliasing.
L'anti-aliasing: voilà l'ennemi. On connaît quelques contournements mis en place pour tenir compte du revival des jeux à gros pixels. Sur ce blog, cet article montre comment désactiver l'anti-aliasing dans le contexte 2D du canvas.
Toutefois, ce contournement ne fonctionne que dans le cas de la copie d'une image et le dessin de formes simples - le rectangle. L'API Canvas ne propose rien pour afficher un texte en gros pixels dans une police donnée, une police de 8x8 pixels à l'ancienne qui plus est.
Dès lors, comment parvenir à produire un pareil résultat ?
La page d'accueil de Pixel Saga: du texte pixelisé généré automatiquement
La solution est très simple. C'est tout simplement celle qui était en vigueur sur les vieux coucous... Explications.

La solution

Dans les jeux et les démos sur une bonne vieille machine comme l'Amiga, le texte était affiché à partir de caractères extraits d'une image :
Une police 8x8 sur Amiga
Les caractères figurant dans l'ordre des caractères ASCII à partir du caractère espace, la routine était des plus simples. Il s'agissait de récupérer le code ASCII de chaque caractère, et lui soustraire le code ASCII du caractère espace $20, et d'adresser avec le résultat le premier pixel en haut à gauche du caractère dans l'image.
Par exemple, en assembleur MC68000 :
	lea font,a0
	lea text,a1
	lea bitplane,a2
_printTextLoop:
	move.b (a0)+,d0
	beq _printTextEnd
	subi.b #$20,d0
	lsl.w #3,d0
	lea (a1,d0.w),a3
	movea.l a2,a4
	REPT 8
	move.b (a3)+,(a4)
	lea DISPLAY_DX>>3(a4),a4
	ENDR
	lea 1(a2),a2
	bra _printTextLoop
_printTextEnd:

font:	INCBIN "DATA:fonts/fontWobbly8x8x1.raw"
text:	DC.B "Hello world",0
		EVEN
Pour afficher du texte en gros pixels dans une page Web, c'est exactement la même technique qu'il faut mettre en oeuvre. La même image est donc utilisée, et le code est simplement adapté à la réalité des opérations graphiques dans le contexte 2D d'un objet <canvas>.

Le code JavaScript

En JavaScript, la solution se traduit par le code suivant :
function print (fontImage, fontFactor, canvas, message) {
	var fontSize, context2d, i;

	fontSize = fontImage.height;
	canvas.width = message.length * fontSize * fontFactor;
	canvas.height = fontSize * fontFactor;
	context2d = canvas.getContext ("2d");
	context2d.imageSmoothingEnabled = false;
	for (i = 0; i != message.length; i ++)
		context2d.drawImage (fontImage, (message.charCodeAt (i) - 0x20) * fontSize, 0, fontSize, fontSize, i * fontSize * fontFactor, 0, fontSize * fontFactor, fontSize * fontFactor);
}
La fonction prend en arguments :
fontImage L'objet <img> de l'image de la police
fontFactor Le facteur multiplicateur de la taille de la police, présumée correspondre à la hauteur de l'image
canvas L'objet <canvas> où dessiner le texte
message Le texte à dessiner
Toutefois, il est bon de s'assurer que l'image de la police de caractères a été bien chargée, ce qui peut s'effectuer en développant un petit loader comme expliqué plus loin.

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

La logique de la fonction a déjà été présentée, et elle est assez simple pour qu'il ne soit pas nécessaire d'y revenir.
Le seul point qu'il faut préciser est relatif au chargement de l'image de la police de caractères. En effet, il faut que cette dernière soit accessible via un objet <img>. Un moyen de s'en assurer est de procéder au chargement asychrone de l'image, et d'enchaîner sur l'affichage du texte sur résolution d'un objet Promise.
L'objet Promise a déjà été longuement présenté dans cet article. Ici, il peut être utilisé pour préciser l'enchaînement des actions au chargement d'une image par une requête HTTP présentée via l'objet XMLHttpRequest.
L'exemple donné comporte un objet Gallery que la fonction print () interroge pour récupérer l'image, en fournissant l'URL de cette dernière.
Cet objet vérifie s'il dispose déjà d'un élément <img> correspondant. Si oui, il retourne la promesse, déjà résolue, créée lors du chargement de l'image. Si non, il procède au chargement de l'image. A cette occasion, il fournit une callback à un objet XMLHttpRequest. Si le chargement réussit, cette callback résout une nouvelle promesse. C'est donc cette promesse que print () retourne.
function Gallery () {
	this.images = new Array ();
}

Gallery.prototype.load = function (url) {
	var image;

	image = { url: url, tag: null, promise: null };
	this.images.push (image);
	image.tag = document.createElement ("img");
	image.promise = new Promise ((resolve, reject) => {
		image.tag.addEventListener (
			"load",
			((id) => () => this.onIMGLoaded.call (this, id, resolve, reject)) (this.images.length - 1),
			{ once: true, capture: false }
		);
		image.tag.src = image.url;
	});
	return (image.promise);
};

Gallery.prototype.onIMGLoaded = function (id, resolve, reject) {
	resolve (this.images[id]);
};

Gallery.prototype.getImage = function (url) {
	var image;

	for (image of this.images) {
		if (image.url == url)
			return (image.promise);
	}
	return (this.load (url));
};
Le code de la fonction print () est légèrement adapté pour utiliser l'objet Gallery :
  • son premier argument n'est plus un objet <img> correspondant à l'image de la police de caractères, mais l'URL de cette image ;
  • tout son code est logé dans un resolver qu'elle transmet à la promesse via la méthode .then () de cette dernière ;
  • elle peut retourner la promesse en question, pour permettre au développeur d'enchaîner d'autres traitements subséquents au chargement de l'image de la police de caractères.
function print (fontURL, fontFactor, canvas, message) {
	return (gallery.getImage (fontURL).then ((image) => {
		var fontSize, context2d, i;

		fontSize = image.tag.height;
		canvas.width = message.length * fontSize * fontFactor;
		canvas.height = fontSize * fontFactor;
		context2d = canvas.getContext ("2d");
		context2d.imageSmoothingEnabled = false;
		for (i = 0; i != message.length; i ++)
			context2d.drawImage (image.tag, (message.charCodeAt (i) - 0x20) * fontSize, 0, fontSize, fontSize, i * fontSize * fontFactor, 0, fontSize * fontFactor, fontSize * fontFactor);
	}));
}
Afficher du texte en gros pixels dans une page Web