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 :
- pour afficher les règles du jeu dans l’écran d’accueil ;
- pour interrompre un niveau, quand le nombre de mutants dépasse la centaine ;
- pour informer d’un échec lors d’une soumission de code qui ne fonctionne pas ;
- 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 !
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 ?
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 !
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énementvoightkampff
, 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 !
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.
Laisser un commentaire