Image de la matrice

Le Canvas et le clavier, les bases de l’animation interactive en HTML5

Image de la matrice

Cher 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 pousser l’idée un peu plus loin en jetant les bases d’un outil de discussion instantanée… qui utilise la matrice comme support !

Toujours dans le but de découvrir HTML5 je te propose cette fois d’aborder les ombrages, les fichiers audio, ainsi que la gestion des évènements claviers !

Les enjeux

A l’occasion du premier article sur les canvas nous avons pu voir qu’il est assez aisé de dessiner du texte sur une toile. Quelques règles globales définissent couleur, trait, ou encore police de caractère, et quelques méthodes se proposent de dessiner le texte, ou son contour. Sur le deuxième nous avons abordé la création d’une animation, qui peut finalement n’être considérée que comme une succession d’images à l’écran.

Je te propose maintenant de combiner ces techniques avec quelques autres. Nous allons mettre le nez dans la gestion des ombrages, qui permettent de dessiner des effets avancés sur notre texte, comme le permet CSS3 avec sa règle text-shadow. 

Nous aborderons la gestion du clavier, fonctionnalité qui n’est pas récente mais qui permet d’enrichir considérablement les interactions avec le dessin. Enfin et parce que tu le sais, je suis un apôtre du bon goût, nous verrons comment jouer des sons depuis notre toile ! Des sons de Matrix, évidemment. Ça sera sexy.

Tu constateras qu’il ne manque plus qu’un peu de travail côté serveur et un WebSocket pour transformer notre support de chat en véritable outil de discussion instantanée !

Rappels

La toile

La toile de dessin (l’élément canvas) sera de la dimension de l’écran, en largeur comme en hauteur.

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

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

Les dimensions fournies par window.screen ne sont pas toujours pertinentes. Pour ne pas provoquer l’affichage de la barre de défilement vertical et ensuite devoir soustraire sa largeur à notre toile nous plaçons l’élément canvas en position fixe.

#matrix{
	position:fixed;
}

La matrice

Il n’existe pas réellement de matrice sur notre toile, tout au plus la matrice des pixels, mais c’est un peu trop grand pour notre besoin. Nous en créons une nous-même !

Nous utilisons une police de caractère à espacement fixe. Chaque colonne est donc large comme un des caractères de cette police. La méthode context.measureText() nous permet de déterminer la largeur d’un de ces caractères :

var cw = ctx.measureText("0").width;

Nous en déduisons le nombre de colonnes à dessiner sur notre toile :

var nbc = Math.floor(matrix.width / cw);

Il n’est pas possible de récupérer la hauteur d’un caractère comme on le fait pour sa largeur. C’est donc de manière empirique que j’ai estimé la hauteur d’une ligne à neuf pixels. (Il se trouve qu’avec une police de dix points et des lignes de neuf pixels les caractères sont très proches les uns des autres, sans trop se coller.)

var ch = 9;

L’effet Matrix

L’animation « sauce Matrix » repose sur un artifice. Nous n’allons pas dessiner toutes les cellules sur chaque image.

Sur chaque image nous dessinons un caractère opaque à la fin de toutes les traînes (donc un caractère par colonne). Comme l’image précédente n’a pas été effacée les caractères précédents demeurent visibles.

/*
 * Un caractère de la table UTF-8 entre le 97ème et le 122ème.
 * Donc un caractère entre "a" et "z"
 */
var c = String.fromCharCode(97 + Math.floor(Math.random() * 26));
ctx.fillText(c, i * cw, y);

Pour obtenir le dégradé et l’impression que la traîne « disparaît » nous dessinons sur chaque image un rectangle noir avec une opacité de 5%. Ainsi les caractères dessinés quelques images plus tôt s’estompent petit à petit. Leur opacité décroit jusqu’à ce qu’ils deviennent transparents.

ctx.globalAlpha = 0.05;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, matrix.width, matrix.height);

Transformation

Ecrire un message

L’objectif est donc d’écrire des messages personnalisés, directement sur la matrice. Les caractères seront dessinés en blanc, à l’emplacement de ceux qui composent la traîne.

Dans un premier temps j’ai dessiné le texte avec une dispersion verticale et horizontale, pour donner une dimension aléatoire et un peu complexe. Finalement il s’avère qu’en dispersant les caractères le texte devient difficile à lire.

Avec dispersion des caractères

Avec dispersion des caractères

J’ai préféré retirer cet effet et le remplacer par une ombre blanche qui « brille » derrière la lettre lorsqu’elle est « percutée » par la traîne. (Cet effet est  également présent sur l’image précédente.)

Un ombrage blanc

Un ombrage

Les deux classes Javascript WLetter et WMessage permettent de faciliter la gestion des messages dans la matrice. Ce ne sont pas des classes de HTML5. Je n’expliquerai que les portions de code qui me semblent pertinentes, n’hésite pas à consulter le code de ces classes sur le projet complet pendant la lecture.

Les lettres en blanc (« White Letters ») sont affichées quelques secondes, puis supprimées de l’objet WMessage. Avant d’afficher le nouvel élément d’une traîne nous interrogeons cet objet pour savoir s’il ne faut pas plutôt dessiner un caractère blanc :

var message = new WMessage(/* ... */);

/* ... */

if(message.IsLetter(i * cw, y))
	message.Refresh(i * cw, y);
else
	ctx.fillText(String.fromCharCode(97 + Math.floor(Math.random() * 26)), i * cw, y);

La méthode IsLetter() détermine à partir des coordonnées d’un point si une lettre blanche est présente à cet endroit. Si c’est le cas nous appelons Refresh() qui la dessine au niveau d’opacité maximal, et lui trace une ombre blanche.
Si aucune lettre n’est à dessiner à cet endroit l’algorithme continue de manière classique, il pioche un caractère dans l’alphabet et le dessine à la suite de la traîne.

Pour que les caractères blancs du message demeurent opaques ils sont dessinés sur chaque image, écrasant les précédentes. Le grand rectangle faiblement opaque qui fait disparaître les traînes ne semble alors pas agir sur eux.
C’est la méthode KeepBright() qui redessine les lettres.

Pour faire corps avec l’animation nous dessinons une ombre sur le caractère blanc lorsqu’il est percuté par la traîne. Ce halo ne sera pas redessiné, et s’estompera donc en même temps que la traîne. Visuellement le texte « vie » avec la matrice ! C’est beau.

Les lettres inscrites sur la matrice sont toutes mémorisées dans l’objet WMessage et le constructeur de ce dernier reçoit une durée d’affichage en secondes. Chaque lettre ne sera donc dans le message que ce nombre de secondes. Lorsque sonne l’heure fatidique la méthode Refresh() supprime la lettre. L’animation suit son court et le caractère disparaît inexorablement jusqu’à devenir complètement transparent, ou être écrasé par la traîne de sa colonne.

Création d’une ombre

L’ombre derrière le caractère est dessinée par la méthode fillText(), en même temps que la fonction dessine le caractère lui-même. Il nous faut uniquement paramétrer les bons attributs du contexte de dessin :

ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 15;
ctx.shadowColor = "rgba(255, 255, 255, " + _opacity + ")";

ctx.fillStyle = "white";
ctx.fillText(_c, _x, _y);

Les attributs shadowOffsetX et shadowOffsetY sont le décalage horizontal et vertical de l’ombre, en pixels. Ici nous voulons une ombre centrée sur le caractère, donc deux zéros.

L’attribut shadowBlur est le « niveau de flou ». Plus il est élevé plus l’ombre sera floue et dispersée. La valeur que nous avons choisie est assez élevée et donne un effet halo approprié.

Enfin shadowColor renseigne la couleur – c’était attendu – mais permet surtout de préciser le niveau d’opacité voulu. Ici nous utilisons la variable _opacity qui est le niveau d’opacité de la traîne à notre emplacement.

Les attributs shadowOffsetX et shadowOffsetY acceptent des valeurs négatives. J’en ai renseigné pour le titre du projet de démonstration, mais c’était en CSS :

Une ombre en CSS3

Une ombre en CSS3

h1, h2, h3, h4, h5, h6{
	text-align:center;
	color: rgb(177, 202, 0);
	text-shadow:
		-2em	/* Décalage horizontal */
		-0.5em 	/* Décalage vertical */
		2px 	/* Dispersion */
		#DDDDDD;/* Couleur */
}

Jouer des sons

Je t’entendais te murmurer « ce n’est pas de très bon goût » lorsque tu lisais l’introduction ! Je te l’accorde ça ne se fait pas beaucoup. Lorsqu’un site Internet se met à jouer de la musique sans prévenir, souvent par-dessus la tienne (qu’il n’a pas détectée) je suis le premier à maudire le webmaster. Cependant c’est l’occasion d’introduire la gestion de l’audio avec HTML5, alors nous ferons tous les deux un effort.

Une classe Javascript permet de jouer des fichiers sons. J’ai utilisé un fichier key.mp3 qui propose le bruit d’une frappe au clavier, et un fichier fond.mp3 qui propose un bruit de fond de Matrix.

D’autres formats de fichiers audio sont supportés par les navigateurs. Je lis à la rédaction de cet article que MP3, AAC et Vorbis sont les trois principaux.

var skey = new Audio("sounds/key.mp3");
var sbackground = new Audio("sounds/fond.mp3");

Beaucoup de paramètres et de méthodes sont proposés par l’interface Audio (voir www.w3schools.com) mais notre cas d’usage est très simple. Voici quelques paramètres qui nous intéressent :

skey.preload = "auto";
skey.loop = false;

sbackground.preload = "auto";
sbackground.autoplay = true;
sbackground.loop = true;

Le paramètre preload à la valeur auto provoque le chargement du média dès l’ouverture de la page. Le paramètre loop est un booléen spécifiant si le son doit être répété ou non. Dans notre cas le son de frappe au clavier ne sera joué que lors d’un appui clavier. Le fond sera répété. L’attribut autoplay vaut true si le son doit être joué dès son chargement. Il n’est alors pas utile d’appeler play().

Le son est joué par le navigateur lors de l’appel à play(), et il est arrêté lors de l’appel à pause(). Il n’y a pas de fonction stop(), pour recommencer avant la fin il faut affecter à l’attribut currentTime.

skey.play();

/* Pas besoin d'appeler sbackground.play() */

Il est possible d’agir en fonction de l’état du lecteur audio. Un attribut paused nous permet par exemple de savoir si le son est joué à un moment donné. Dans notre projet et lors d’un appui sur la touche de retour arrière, le son sera alternativement joué et mis en pause :

if(sbackground.paused)
	sbackground.play();
else
	sbackground.pause();

Utiliser la saisie clavier

Les évènements du clavier ne sont par défaut pas capturés par les éléments Javascript de notre page. En revanche les navigateurs en font souvent un usage avancé, qui permet de faciliter la navigation.

Ce que nous voulons ici est simple : écrire sur notre matrice ce que l’utilisateur tape sur son clavier. Trois évènements sont mis à notre disposition : keydown, keypress et keyup. Le premier est généré lorsque la touche est enfoncée sur le clavier. Le deuxième est généré si la touche enfoncée est un caractère imprimable. (Pas d’évènement keypress pour les touches Ctrl, Alt, Maj, Shift, etc.) Le troisième évènement est généré lorsque la touche est relâchée.

keydown et keypress sont répétés si tu restes au fond du clavier.

Ces trois évènements sont déclenchés pour chaque appui sur une touche du clavier. La combinaison Ctrl + A provoquera par exemple les évènements keydown + keyup pour la touche Ctrl, puis les évènements keydown + keypress + keyup pour la touche A.

Nous capturons les deux premiers évènements claviers de tout ce qui est saisi dans le corps de la page :

document.body.onkeydown = function(e)
{
	/* ... */
}
document.body.onkeypress = function(e)
{
	/* ... */
}

Les raccourcis, l’ergonomie

L’évènement keydown nous intéresse car il est levé pour chaque appui sur le clavier. Nous pouvons nous en servir pour créer des raccourcis. Les attributs keyCode des objets event reçus en paramètre nous renseignent sur la touche qui a été utilisée :

if(8 == e.keyCode) /* Retour arrière */
{
	if(sbackground.paused)
		sbackground.play();
	else
		sbackground.pause();
	e.preventDefault();
}
else if(13 == e.keyCode) /* Entrée */
{
	message.NextLine();
	e.preventDefault();
}

Le visiteur tape du texte alors il utilisera la touche « Retour Arrière ». Comme par défaut rien n’est prévu c’est le navigateur qui va récupérer l’évènement. Il se trouve que sur Mozilla Firefox et Google Chrome un appui sur « Retour Arrière » provoque une redirection vers la page précédente. C’est énervant. Pour éviter ce comportement et ne pas agacer l’internaute nous taisons l’évènement associé avec preventDefault().

Sur les éléments de formulaire comme les éléments input avec le type text le retour arrière est utilisé, et il n’est pas remonté jusqu’au navigateur. Comme nous n’utilisons pas de champ texte il faut penser à ce cas particulier.

Ce n’est pas spécialement intuitif mais j’en ai profité pour utiliser la touche « Retour Arrière » et la faire déclencher le son d’ambiance.

La touche « Entrée » provoque un changement de ligne. C’est un comportement tout à fait attendu pour l’utilisateur, mais encore une fois c’est à nous de le prévoir.

Récupérer les saisies

Le fond de notre idée reste la récupération de ce que l’utilisateur écrit sur son clavier. Comme toutes les touches susceptibles d’afficher quelque chose génèrent l’évènement keypress, c’est à sa capture que nous ajouterons nos lettres blanches au message :

if(e.ctrlKey)
	return;

var c = getChar(e);
if(null != c)
{
	message.Add(c);
	skey.play();
	e.preventDefault();
}

Pour éviter de bloquer tous les raccourcis du navigateur nous ne traitons pas les évènements générés avec la touche de contrôle enfoncée.

Comme j’écrivais plus haut la combinaison Ctrl + A va générer deux puis trois évènements, mais dans les trois évènements dédiés à la touche A l’attribut ctrlKey sera à true puisque la touche Ctrl est enfoncée avant d’appuyer sur A.

Les combinaisons Ctrl + qqch sont très utilisées par les navigateurs.

La fonction getChar() permet de faciliter la récupération d’un caractère. En effet tous les navigateurs ne fonctionnent pas de la même façon.

function getChar(e)
{
	if(null == e.which) /* IE */
		return String.fromCharCode(e.keyCode);
	else if(0 != e.which && 0 != e.charCode) /* Les autres */
		return String.fromCharCode(e.which);
	else
		return null; /* Caractère non imprimable */
}

Conclusion

C’est terminé ! Pour te faire une idée tu peux retrouver le projet complet sur GitHub : SoCanvasAnimationsChat. Pour tester en live ça se passe sur JSFiddle !

Comme tu peux le constater ce projet n’est techniquement pas compliqué. Il est surtout nécessaire de ne pas perdre de vue les habitudes de navigation des internautes, et de proposer des solutions ergonomiques.

HTML5 et Javascript permettent aisément de combiner plusieurs interfaces afin d’enrichir rapidement l’expérience utilisateur. Toutes les interfaces de cette spécification offrent une grande liberté au développeur.

L’absence d’environnement de développement intégré ou de framework dédié au dessin en deux dimensions peut rendre le développement un peu laborieux. Cependant beaucoup de solutions existent déjà et d’autres sont en cours de développement. La programmation avec les Canvas sur de gros projets se fera bientôt les doigts dans le nez !

Comme je l’écrivais plus haut nous avons à peu de choses près un chat fonctionnel. Il manque surtout le code serveur capable de faire interagir plusieurs clients.
Comme les WebSockets exigent une implémentation un peu particulière il n’est pas suffisant d’ouvrir un canal classique. Beaucoup de projets existent dans des langages très divers comme Javascript avec Node.js, Java avec jWebSocket ou encore C# avec Nugget. Il y a également PHP avec phpwebsocket. Ils te permettront tous de rapidement développer un serveur.

Tu peux bien entendu utiliser le serveur disponible en ligne qui répond ce que tu lui envoies. Je m’en sers pour les tests.

Nombre de vue : 330

AJOUTER UN COMMENTAIRE