Un atelier devant par définition impliquer les participants, j’ai fait le choix de créer un mini-jeu en guise de support. C’est un site statique disponible en ligne, open-sourcé sur GitHub — pour que vous puissiez l’améliorer !

Quand je dis statique, c’est statique : le dépôt a une unique dépendance, servor, chargée de fournir un serveur HTTP basique pour travailler en local  et servor n’a elle-même aucune dépendance. Le reste n’est que HTML, CSS et JavaScript.

Ça m’a permis de revenir aux fondamentaux, considérablement gagner en efficience ; mais surtout… découvrir tout un tas de trucs et astuces !

La mécanique du jeu

En démarrant le jeu, vous commencerez par personnaliser votre personnage. Le seul objectif de cette étape est de découvrir la structure visuelle d’un niveau, en vous permettant de vous impliquer personnellement dans le jeu. Les valeurs choisies seront appliquées dès que possible à tous les personnages du jeu, dans une sorte de représentation en miroir.

Après avoir choisi votre personnage, un niveau d’entraînement vous familiarise avec la mécanique très simple du jeu : une portion de code à compléter et soumettre, exécutée en direct, qui affecte la zone envahie progressivement par des mutants ! Ce code est dans la plupart des niveaux les options passées à un mutationObserver, mais parfois aussi dans la fonction de rappel.

En cas d’échec comme en cas de réussite, une fenêtre modale vous informera. Parlons-en, de cette fenêtre modale !

Les particularités de <dialog>

J’en parlais en 2022 à Paris Web puis au devFest Nantes dans mon sujet « Découvrez " le bon HTML " et économisez du JS et du CSS », l’élément <dialog> est extrêmement intéressant et devrait à terme supplanter toutes les implémentations de fenêtres modales dans les différentes bibliothèques de composants.

Dans l’atelier, je m’en sers en plusieurs endroits :

  1. pour afficher les règles du jeu dans l’écran d’accueil ;
  2. pour interrompre un niveau, quand le nombre de mutants dépasse la centaine ;
  3. pour informer d’un échec lors d’une soumission de code qui ne fonctionne pas ;
  4. pour informer de la réussite, dans le cas contraire — et permettre de passer au niveau suivant.

Ouvrir la fenêtre

La plupart sont ouvertes programmatiquement, en réaction à un événement. Rien de plus simple : il suffit de récupérer une référence à l’élément <dialog> à l’aide de querySelector() ou une référence à un identifiant via l’accès aux propriétés nommées, ou named properties access (en anglais), et d’invoquer la méthode showModal() (sur MDN, en anglais).

document.querySelector('dialog').showModal();

Sans JavaScript externe

Une exception toutefois, pour éviter d’ajouter un écouteur d’événement inutile : la fenêtre des règles du jeu est invoquée grâce à un gestionnaire d’événement HTML onclick :

<button type="button" onclick="rules.showModal()">Règles du jeu</button>
<dialog id="rules" role="dialog" aria-label="Règles du jeu"></dialog>
Aparté : la projection des identifiants HTML en objets globaux

Dans cet exemple, j’invoque l’ouverture de la fenêtre modale avec rules.showModal(), sans avoir défini la variable rules. Comment est-ce possible ? En résumé, tout élément porteur d’un attribut id devient mécaniquement une propriété globale de l’objet window, et devient donc accessible directement par son nom. C’est spécifié sous le joli nom de Named Access on Window Object (en anglais).

C’est drôlement pratique, non ? Figurez-vous que c’est aussi un vecteur d’attaque méconnu faisant partie d’un groupe sobrement intitulé DOM clobbering (en anglais). Je vous encourage à parcourir les recommandations de l’OWASP pour mitiger le DOM clobbering (en anglais).

Accessibilité

La méthode showModal() permet d’ouvrir une fenêtre modale, pas une simple boîte de dialogue — en respectant les exigences en matière d’accessibilité : la focus est mécaniquement piégé dedans, la fermeture est possible avec la touche Échap, etc.

L’arrière-plan

Une fois la fenêtre modale ouverte, on peut s’appliquer à la styler. Là où moult bibliothèques de composants imposent une <div> (voire plusieurs) pour servir d’arrière-plan à la fenêtre, la version native est livrée avec un pseudo-élément ::backdrop qui s’étend naturellement sur tout le viewport et est promue, avec la fenêtre modale, par-dessus le reste de la page dans ce qui est spécifié sous le nom de top layer.

Vous n’avez plus qu’à lui appliquer une couleur, une opacité ou que sais-je encore. Dans le jeu, j’ai utilisé une propriété au nom évocateur de backdrop-filter pour appliquer un effet de flou grisé sur l’arrière-plan.

dialog::backdrop {
	backdrop-filter: grayscale(50%) blur(.25rem);
}

Les dimensions

Ne maîtrisant pas le mode de consultation du jeu, j’ai utilisé un peu de CSS moderne pour la largeur de la fenêtre modale afin qu’elle ait une largeur fluide, mais avec des valeurs minimum et maximum.

dialog {
	max-inline-size: clamp(50vw, 100%, 67.5rem);
}

La propriété max-inline-size est la propriété logique correspondant à max-width dans le cas du Français. Et la fonction clamp() est un petit bijou, dont j’abuse déjà copieusement dans chaarts (en anglais) pour obtenir un pseudo-booléen en CSS en fonction d’une valeur, comme expliqué slide 27 de ma conférence « Dessine-moi un graphique (en CSS) » donnée au devFest Nantes 2023, TNT #24 et DevQuest 2024.

Fermer la fenêtre

J’ai évoqué la capacité de fermer la modale avec la touche Échap, mais l’élément <dialog> tire d’autres super-pouvoirs du fait d’être natif, et notamment son association avec un élément <form>. C’est parfaitement naturel, puisqu’une fenêtre modale permet généralement de valider ou annuler une action associée à une saisie.

C’est pourquoi la valeur dialog est ajoutée à la method de soumission d’un formulaire. Elle ne correspond pas à une méthode HTTP comme get ou post, mais bien à un contexte HTML, et permet de fermer directement la fenêtre modale parente. L’utilisation est fort simple :

<form id="fermer" method="dialog">

Et, pour revenir à du HTML à l’ancienne : saviez-vous qu’un bouton à l’autre bout du DOM peut soumettre un formulaire ? Il suffit de lui indiquer le formulaire à soumettre :

<button form="fermer">Fermer la fenêtre</button>

Et voilà : c’est un bouton de type submit qui soumettra le formulaire avec l’identifiant fermer, qui lui-même fermera la fenêtre de dialogue. C’est beau, non ? Et cet attribut date (au moins) de 2006, dans les spécifications W3C des Web Forms (en anglais) dont les premiers brouillons remontent à 2004…

Les émojis

Pour cet atelier, j’avais besoin de méchants envahisseurs, et de décors. Clairement pas le temps de faire des illustrations à la main, ni les moyens d’acheter des visuels. Une quête sur les internets m’a appris que le type de visuels que je cherchais se nomme les top down tileset, ces petits décors et personnages généralement en 8-bit avec une perspective écrasée.

À force de regarder des visuels en 8-bit, j’ai fini par faire le lien avec une vieille habitude dans mes supports de conférences : les émojis décoratifs en fin de titre. Bon sang, mais c’est bien sûr ! Des émojis !

Les émojis sont formidables. Ce sont des points Unicode, purement textuels, et extrêmement nombreux désormais avec des pelletées de nouveautés dans chaque version d’Unicode. Il y a même des variantes, composées en séquence !

Les personnages

Le meilleur exemple de séquence Unicode à mon avis sont les personnages : le neutre Personne 🧑 peut devenir un homme 👨 ou une femme 👩 en y ajoutant le point unicode du genre masculin ♂️ ou féminin ♀️, séparé par une jointure de largeur zéro (zero-width joiner, ).

Pour obtenir un pompier 👨‍🚒, on ajoute simplement un camion de pompier 🚒 à une personne 🧑 ! N’est-ce pas génial, franchement ? Et on peut évidemment y ajouter le genre et le teint.

La personnalisation

Ainsi le premier palier permet de personnaliser le genre et le teint du héros.

Le formulaire n’est composé que de deux groupes de bouton radio, chacun ayant une valeur correspondant au point Unicode concerné.

<fieldset>
	<legend>Genre</legend>
	<input type="radio" name="genre" id="feminin" value="♀️">
	<label for="feminin">Féminin</label>
	<input type="radio" name="genre" id="masculin" value="♂️">
	<label for="masculin">Masculin</label>
	<input type="radio" name="genre" id="neutre" value="" checked>
	<label for="neutre">Neutre</label>
</fieldset>

Lors de la soumission, les deux valeurs sélectionnées sont poussées dans le localStorage et ré-employées dès que possible dans la suite du jeu. Pour certains méchants, il suffit de concaténer le caractère du méchant avec les deux sélections : voilà comme un Mage 🧙 devient une Mage au teint sombre 🧙🏿‍♀️.

Les décors

J’ai un peu lutté avec les décors, demandant même de l’aide à mon camarade Clément Étienne. Et finalement, je suis revenu aux émojis : certains ont un caractère paysager intéressant, il suffit de les agrandir un peu…

🏔️

Les navigateurs

Les navigateurs et systèmes d’exploitation ont leur propre livrée d’émojis, avec des supports disparates et des rendus variés. Pour palier cet écueil, j’ai opté pour une solution très simple technologiquement parlant : une typographie. Et à ce jeu-là, j’avais déjà ma préférée : la Twemoji-COLR par Mozilla (sur GitHub, en anglais).

Les utilisateurs de Mozilla ne verront pas grand chose de nouveau : elle est embarquée dans Firefox sous le nom de Twemoji Mozilla, ce qui permet de tenter d’utiliser la version locale en CSS.

@font-face {
	font-display: swap;
	font-family: 'Twemoji Mozilla';
	font-style: normal;
	font-weight: 400;
	src: local('Twemoji Mozilla'), url('/assets/fonts/Twemoji.woff2') format('woff2');
}

Et le tour est joué : les utilisateurs de Firefox ne chargeront rien, et les autres téléchargeront une typographie pour afficher la même chose que Firefox. Choisissez mieux votre navigateur, la prochaine fois !

Capture d’écran de l’inspecteur de Polices de Firefox, pour la Twemoji Mozilla.
Firefox indique bien « système » pour l’origine de la typographie.

WebKit

Comme souvent quand je prépare un sujet, je me suis heurté à quelques limites des navigateurs. En l’occurrence, WebKit, le moteur de Safari et Epiphany, a des problèmes avec les variantes de teinte de la Twemoji-COLR. J’ai pu ouvrir un ticket sur leur Bugzilla (en anglais).

La coloration syntaxique sans JS

Dans la mécanique du jeu, des portions de code sont affichées (pour faire un « code à trous ») et du code est saisi des éléments <input> et <textarea>.

Et pour lire et écrire du code, la coloration syntaxique est drôlement pratique et agréable ! Mais charger un script tel que PrismJS (en anglais) ou highlight.js (en anglais) m’a toujours semblé démesuré pour la valeur ajoutée. Le bloc de code se retrouve charcuté dans le DOM, où des <span> avec des classes plus ou moins lisibles saucissonnent chaque portion de texte en fonction de son rôle syntaxique. C’est carrément indigeste.

Mais au moment où je préparais cet atelier, Heikki Lotvonen a publié un article ahurissant : Font with Built-In Syntax Highlighting (en anglais). C’est à mon sens, une petite révolution : une typographie tirant parti des fonctionnalités OpenType et notamment la table COLR. Fini les tartines de <span>, place à un code lisible et propre !

Si les détails d’implémentation OpenType vous intéressent, je vous encourage à lire l’article. De mon côté, je me suis focalisé sur la personnalisation de la palette, rendues possibles en CSS avec @font-palette-values (sur MDN, en anglais) et la propriété override-colors (sur MDN, en anglais).

Voilà ce que ça donne pour le jeu, dans lequel je profite de l’utilisation de propriétés personnalisées CSS pour la gestion des couleurs :

@font-palette-values --syntaxHighlighter {
	font-family: 'FontWithASyntaxHighlighter';
	override-colors:
		0 var(--foreground),
		4 rebeccapurple,
		5 var(--accent),
		7 var(--muted);
}

Le rendu est pas mal, non ?

Bloc de code avec coloration syntaxique et champs de formulaires.

Et c’est de la pure amélioration progressive : si votre navigateur ne supporte pas la table COLR, la règle @font-palette-values ou la propriété override-colors, vous aurez juste du texte brut avec la monospace par défaut.

Les Space Invaders

Le dernier point sur lequel je me suis beaucoup amusé, c’est le niveau des aliens. L’émoji alien monster 👾 ressemble beaucoup, beaucoup, beaucoup aux vaisseaux de Space Invaders. Pour un jeu d’invasion, ça tombe bien.

J’ai donc voulu assumer la référence : arrière-plan noir, animation des envahisseurs qui défilent vers le bas, et… un compteur de score.

Les compteurs

Pour ceux qui font du CSS depuis longtemps, vous avez peut-être déjà entendu parler des compteurs CSS. Notre score correspondra simplement au nombre d’aliens présents.

Cependant, si notre compteur commence à 1 et peut monter jusqu’à 100 — et sachant que le jeu original disposait d’un compteur sur cinq chiffres — le rendu ne sera ni élégant ni une belle citation. Heureusement, CSS nous permet de personnaliser le style du compteur avec @counter-style.

Pour obtenir un compteur sur cinq chiffres, affichant des 0 avant la valeur du compteur, voici la déclaration utilisée :

@counter-style invasion {
	system: numeric;
	symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
	pad: 5 "0";
	speak-as: numbers;
}

WebKit (encore)

Là aussi, WebKit est limité : les compteurs CSS ne sont pas incrémentés quand on ajoute des éléments au DOM. C’est Karl Dubost qui a ouvert ce ticket sur Bugzilla (en anglais).

Les couleurs

Un autre point saillant pour citer visuellement Space Invaders, ce sont les couleurs vives. L’émoji utilisé vient avec une couleur qu’on ne peut pas surcharger, donc on va devoir l’altérer. Cette technique n’est pas récente, mais extrêmement utile : l’accumulation de filtres CSS pour atteindre la bonne couleur.

C’est un exercice compliqué, et je remercie Barrett Sonntag pour son générateur de filtres pour convertir du noir vers un code héxadécimal (sur CodePen, en anglais). La seule contrainte est de commencer par du noir ce qui se résout facilement en appliquant en premier grayscale(100%) brightness(0%).

mu-tant[type="invaders"]:nth-child(1n + 1) {
	filter: grayscale(100%) brightness(0%) invert(15%) sepia(90%) saturate(5339%) hue-rotate(6deg) brightness(96%) contrast(127%);
}

mu-tant[type="invaders"]:nth-child(2n + 1) {
	filter: grayscale(100%) brightness(0%) invert(66%) sepia(82%) saturate(4488%) hue-rotate(88deg) brightness(117%) contrast(129%);
}

mu-tant[type="invaders"]:nth-child(3n + 1) {
	filter: grayscale(100%) brightness(0%) invert(9%) sepia(90%) saturate(7442%) hue-rotate(247deg) brightness(91%) contrast(149%);
}

mu-tant[type="invaders"]:nth-child(4n + 1) {
	filter: grayscale(100%) brightness(0%) invert(91%) sepia(27%) saturate(1428%) hue-rotate(1deg) brightness(110%) contrast(104%);
}

C’est verbeux, mais ça fonctionne !

Des lignes d’aliens aux couleurs vives sur un fond noir, et un score en haut à droite en monospace.
Plutôt ressemblant, non ?

Les Web Components

Et dire que je n’ai parlé que de HTML et CSS, pour le moment… Je ne m’étendrai pas autant, mais côté JavaScript, je me suis (un peu trop) amusé avec les Web Components. En résumé :

  • <mu-tant> est le composant qui affiche un mutant, et gère sa mutation : un changement d’attribut, de valeur d’attribut, de contenu, de descendance, etc. Le tout à intervalle irrégulier, et de façon désordonnée.
  • <code-runner> étend la fonctionnalité du formulaire pour normaliser les réponses et les envoyer au <play-ground>. Pour le clin d’œil, l’événement qui permet de diffuser les réponses est intitulé voightkampff.
  • <play-ground> est le composant le plus critique : il déclenche l’invasion, surveille l’événement voightkampff, exécute le code soumis, et donne le verdict (en ouvrant la fenêtre modale appropriée).

Dans tout ça, j’ai énormément joué avec les mutationObserver, les intervalles et les minuteurs, les émojis, et la génération de valeurs aléatoires.

Conclusion

Si tout ce fatras vous rend curieux, je vous invite à visiter le dépôt du jeu sur GitHub et à en faire ce que vous voulez !

Niveau 2-1 : le fantôme, dans lequel il faut configurer les options du mutationObserver pour enrayer l’invasion. Les fantômes apparaissent sur un décor nuageux, au milieu duquel émerge un pont ressemblant au Golden Gate, à San Francisco.

Et si vous vous lancez dans le jeu, je vous invite à consulter les slides adossés au jeu. En avançant, vous verrez que chaque mutant a son slide. N’avancez pas trop vite, car le slide suivant donne la réponse…

Faites chauffer votre inspecteur !


Note

Cet article fait partie du « Advent of Tech 2024 Onepoint », une série d’articles tech publiés par Onepoint sur dev.to pour patienter jusqu’à Noël.

Article rédigé par . Publié le .