HTML5 et les Canvas animés

Matrice

Ami lecteur si tu arrives sur cette page c’est peut-être parce que tu as lu le premier article relatif au dessin avec Javascript : Mon image est un script ! Introduction aux Canvas de HTML5. Si ce n’est pas le cas je te recommande sa lecture !

Dans ce deuxième volet dédié aux Canvas de HTML5 je te propose de creuser plus en détail la conception d’animations, et plus particulièrement une animation à la fois geek, corporate ET un rien ringarde (ça relève du défi !) : une matrice à la Matrix, aux couleurs de Soat !

Introduction

Le dessin avec les Canvas profite de la puissance importante des navigateurs ainsi que de celle de Javascript. En dessinant très vite des images successives nous créons… des animations ! Absolument, des images et rien que des images. Démonstration !

Sujet

Moult idées m’ont traversé l’esprit, et moult effets pyrotechniques les rendaient magnifiques, merveilleuses, grandioses ! Seulement voilà, je ne suis pas encore le concepteur d’animations HTML5 que j’aimerais être, alors j’ai choisi un sujet plus abordable.

Je me suis inspiré d’un script original trouvé à cette adresse pour concevoir quelque chose de simple et de très visuel. Tiens-toi bien le code original tient sur cette ligne :

for(s=window.screen,w=q.width=s.width,h=q.height=s.height,m=Math.random,p=[],i=0;i<256;p[i++]=1);setInterval('9Style=\'rgba(0,0,0,.05)\'9Rect(0,0,w,h)9Style=\'#0F0\';p.map(function(v,i){9Text(String.fromCharCode(3e4+m()*33),i*10,v);p[i]=v>758+m()*1e4?0:v+10})'.split(9).join(';q.getContext(\'2d\').fill'),33)

Si tu veux l’essayer il te suffit de le placer dans l’attribut onload d’une page HTML, et de déclarer un élément canvas avec un attribut id=”q”.

Bien entendu ce n’est pas ce code que je compte utiliser. La version que je vous propose compte une cinquantaine de lignes (sans les commentaires) et est beaucoup plus facile à assimiler.

Développement

Les éléments indispensables

Comme pour le logo de Soat développé à l’article précédent, il nous faut pour commencer un simple élément HTML :

<canvas id="matrix">
	Texte alternatif. Votre navigateur ne supporte pas encore le dessin sur toile !
</canvas>

…que nous récupérons dans notre script :

var matrix = document.getElementById('matrix');

Nous demandons le contexte de dessin en deux dimensions, et nous fixons les dimensions de la toile. Ici la toile prend tout l’espace disponible à l’écran.

var ctx = matrix.getContext('2d');

matrix.height = window.screen.availHeight;
matrix.width = window.screen.availWidth;

Un peu de personnalisation

Sur le script original les caractères d’une même colonne changent au fur et à mesure qu’elle glisse de haut en bas, ils sont tous de la même couleur, et ils sont très variés.

De mon côté j’ai choisi de n’afficher qu’un caractère par colonne. Les colonnes seront de couleurs variées et les caractères seront ceux qui composent « So@t ». Bien entendu à chaque fois que la trace d’une colonne aura terminé son parcours nous définirons une nouvelle couleur et un nouveau caractère, pour conserver le caractère aléatoire de la matrice de Matrix.

Donc dans le code :

var sogray = "#B3B3B3";
var sogreen = "#99CC00"; /* Le véritable vert Soat, merci Melvyna ! */
var soorange = "#e65c00";

var chars = ['S', 'o', '@', 't']; /* Les caractères que nous affichons */
var colors = [sogreen, sogray, soorange]; /* et les couleurs */

Du coup, comme il y a moins d’aléa que sur le script original, il nous faut mémoriser la couleur courante de chaque colonne ainsi que le caractère qu’elle trace, en plus de la position verticale du prochain caractère à tracer.
Ces informations sont stockées dans trois tableaux. Les indices du tableau représentent le numéro de la colonne (les informations relatives à la première colonne sont donc aux indices 0).

var ypositions = new Array(); /* Mémoire de l'ordonnée courante*/
var characters = new Array(); /* Mémoire du caractère courant */
var clmncolors = new Array(); /* Mémoire de la couleur courante */

Notre toile n’est pas réellement découpée en matrice, il n’y a ni colonnes ni lignes. C’est à nous de définir ce découpage.

Chaque colonne ne contenant qu’un caractère (en largeur) et le caractère « @ » étant le plus large, c’est sa dimension qui définit la largeur de la colonne.
Pour ne pas que deux colonnes affichant  « @ » soient collées l’une à l’autre j’ai ajouté dix pixels à cette largeur.

La méthode measureText() ne permettant pas de déterminer la hauteur d’une séquence, c’est de manière empirique que j’ai fixé la hauteur de chaque cellule à dix pixels.  Cette valeur n’est liée en rien aux dix pixels ajoutés sur la largeur.

A partir de là et connaissant la largeur du dessin nous pouvons déduire le nombre de colonnes à dessiner.

var cw = ctx.measureText("@").width + 10;
var ch = 10;
var nbc = Math.floor(matrix.width / cw); /* nb. de colonnes */

Avant de s’attaquer au dessin lui-même il convient d’initialiser nos variables. Pour une colonne donnée la fonction ResetColumn() place en haut de la toile l’ordonnée courante. Elle définit aléatoirement une nouvelle couleur et un nouveau caractère à dessiner. (Parmi les couleurs et les caractères prévus, bien entendu.)

function ResetColumn(i)
{
	ypositions[i] = 0;
	characters[i] = chars[Math.floor(Math.random() * chars.length)];
	clmncolors[i] = colors[Math.floor(Math.random() * colors.length)];
}

L’initialisation :

for (i = 0; i < nbc; i++)
{
	ResetColumn(i);
	ypositions[i] = Math.floor(Math.random() * matrix.height * 3);
}

Comme toutes les ordonnées sont initialement fixées à 0, les premières images font toutes descendre les traces depuis le haut de la toile. Les colonnes se terminant de manière aléatoire en bas de toile cet effet « rideau » ne se produit qu’au début de l’animation.
Pour l’éviter nous affectons une valeur aléatoire aux ordonnées courantes, avant le départ. Sur le script original ce n’est pas fait, libre à toi d’adopter ou non « l’effet rideau ».

Nous sommes maintenant prêts à dessiner nos images !

L’animation

Tout le code qui suit dessine une unique image. C’est cette image, rafraîchie rapidement, qui fait l’animation. Le code qui suit est donc placé dans une fonction step(). A la fin de son traitement cette fonction demandera au navigateur de l’appeler à nouveau.
Notre rôle se limite donc à dessiner une image, et à appeler step() une première fois :

function step()
{
	/* Dessin de l'image */

	/* Ô gentil navigateur, call me back ! */
	if(window.requestAnimationFrame)
		requestAnimationFrame(step);
	else
	{
		/*
		 * Un appel toutes les 33ms provoque
		 * un affichage d'environ 30 images
		 * par secondes.
		 */
		setTimeout(step, 33);
	}
}

/* Le premier appel */
step();

La fonction setInterval() pourrait s’avérer toute indiquée pour répéter un script comme nous le faisons. Cependant et avec l’avènement de Canvas les navigateurs ont implémenté une méthode requestAnimationFrame() adaptée aux animations, qui fait un meilleur usage des ressources du navigateur. Cette méthode arrêtera par exemple l’animation si vous n’êtes plus sur son onglet. Malin !
En revanche comme elle n’est pas implémentée sur tous les navigateurs il faut encore penser une solution de repli, ici avec setTimeout().

L’opacité pour les masquer tous !

Le principe de notre matrice est une illusion. Chaque « tour » un caractère complètement opaque sera dessiné sur chaque colonne, mais avant, nous rendrons tous les précédents un peu plus transparents.
Cette superposition de « couches transparentes » nous donnera l’impression que le caractère le plus récent de chaque colonne laisse une traîne derrière lui.

ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, matrix.width, matrix.height);

Le rectangle noir que nous traçons au dessus de toute la toile n’ayant qu’une opacité de 5%, 20 appels à step() auront effacé la traîne. Le dernier caractère visible de chaque colonne a donc une opacité de 5%, l’avant-dernier de 10%, etc. jusqu’au deuxième qui vient de passer à 95%. Le premier que nous allons tracer sera complètement opaque.

En théorie 20 appels nous ramènent à un niveau d’opacité de 100% et donc chaque traîne fait au maximum 20 caractères de haut (en comptant le nouvel élément complètement opaque).
Dans les faits tous les navigateurs ne nous ramènent pas sur du noir et on peut deviner les anciens passages. Dommage.

Un petit nouveau à la traîne

Nous allons tracer le dernier élément de chaque traîne. Nous connaissons la couleur des colonnes, les caractères qu’elles affichent, et la position où nous devons continuer le dessin.

Il nous reste à calculer l’abscisse de notre lettre, ce qui est fait par la multiplication i * cwest l’indice de la colonne que nous dessinons. cw est la largeur d’une colonne, mais ça je l’ai déjà écrit.

ypositions.map(function (y, i){
	/* Police de caractères officielle ! On est sexy ou on ne l'est pas. */
	ctx.font = "10pt Tahoma";
	ctx.textAlign = "start";
	ctx.textBaseline = "top";

	ctx.fillStyle = clmncolors[i];
	ctx.fillText(characters[i], i * cw, y);

	/* Cf. point suivant */
}, ypositions);

Sur un schéma :

Explication de l'astuce

Une fin aléatoire

Enfin et pour que l’animation en soit une la partie qui suit est tout simplement indispensable. En effet maintenant que nous avons tracé notre caractère, il faut déterminer pour chaque colonne ce qui se passera au prochain tour.

Si le caractère que nous venons de tracer est encore haut sur la toile, nous ajoutons la hauteur d’une cellule (soit ch) à ypositions[i]. De cette manière au prochain tour le caractère qui sera tracé sera juste en dessous du nôtre. (En passant le nôtre aura pris un rectangle légèrement opaque sur le nez, il ne sera déjà plus le petit nouveau… quel monde impitoyable…).

Si en revanche le caractère que nous venons de tracer est sorti de la toile (il est toujours dessiné mais plus visible), nous lui donnons une chance sur cent d’être le dernier de sa colonne. S’il « saisi » cette chance, la colonne est remise à zéro, et le prochain appel à step() commencera une nouvelle traîne en haut de la toile.

ypositions.map(function (y, i){
	/* Cf. point précédent */

	if(y > matrix.height) /* Sortie de toile */
	{
		/* 1% des traînes sorties de la toile sont remises à zéro. */
		if(Math.ceil(Math.random() * 100) > 99)
			ResetColumn(i);
		else /* Les autres continuent */
			this[i] += ch;
	}
	else /* Encore visible, je continue */
		this[i] += ch;
}, ypositions);

Nous aurions pu décider de terminer un pour cent des traînes sur la partie visible de la toile. Pour cela il suffit de tester dès la hauteur voulue. Par exemple pour terminer des traînes aux trois-quarts de l’image on peut tester if(y > 3/4 * matrix.height).

De la même manière les chances que vous donnez aux colonnes d’être remises à zéro sont entièrement sous votre contrôle. Il se trouve qu’avec un pour cent l’animation n’est pas trop chargée. C’est une question de goût !

Résultat de l'animation

Démonstration pour HTML – Canvas – Animation

Conclusion

C’est déjà terminé ! Comme tu as pu le constater notre animation est à peu de choses près une simple image, l’effet animation n’étant qu’artifice et illusion d’optique.

Fort de ce constat je te file un petit conseil : n’explique surtout pas à ton entourage comment cela fonctionne. Toute la magie que tu auras fais naître dans leurs yeux en leur disant « c’est mon bébé » s’évanouirait… Ça m’est arrivé, et je suis retourné à mon clavier pour te mettre en garde !

Au vu de la simplicité d’une telle animation il est facile d’imaginer l’enrichir avec la gestion d’évènements claviers ou de messages reçus via les WebSockets. T’imagines-tu toi aussi lire tes emails sur la matrice ? La classe.

Le projet complet de démonstration est disponible sur GitHub : SoCanvasAnimations. Pour tester en live ça se passe sur JSFiddle !

Nombre de vue : 263

COMMENTAIRES 6 commentaires

  1. Excellent travail !
    peux tu rajouter des exemples ?

    encore merci.

  2. Luc Dachary dit :

    Merci pour ton retour ! Quels exemples aimerais-tu ?

    Je n’ai pas beaucoup de travaux à présenter mais un dernier article relatif aux animations avec les Canvas de HTML5 va très certainement être publié dans le courant de la semaine prochaine. Il traite des interactions entre l’utilisateur (avec son clavier) et le dessin.
    Comme il fait suite à cet article-ci je vais plus loin dans le travail avec la toile.

  3. Autant pour moi !
    J’avais lu trop rapidement le dernier paragraphe, y’a tous ce qu’il faut dernière le lien GitHub.

    Encore merci.

  4. […] lecteur, tu as peut-être lu avec intérêt l’article qui te proposait la création d’une matrice sauce Matrix. Aujourd’hui je te propose de […]

  5. dric dit :

    bonjour, super tuto merci.

    j’ai pas bien saisie l’utilité de la méthode .map à ypositions.map. Ca sert à creer comme des instances de la fonction? pour pouvoir déssiner plus d’une trainnée a la fois?

  6. dric dit :

    C’est bon en fait! j’ ai bosser mon «array.prototype.map» je comprend mieux maintenant 🙂
    Sinon sympa le canvas matrix 😉

AJOUTER UN COMMENTAIRE