La page remplit la fenêtre de colonnes de caractères qui tombent. Chaque colonne a sa propre vitesse, sa propre taille de police et sa propre luminosité, et le caractère affiché change à chaque image. Derrière la tête lumineuse de chaque colonne, une traînée s'estompe progressivement vers le noir.
Il n'y a rien à cliquer: c'est une pièce décorative, faite pour tourner en boucle. Le jeu de caractères mélange des katakana, les chiffres de 0 à 9 et les majuscules latines. Je les ai choisis pour leur largeur régulière en police à chasse fixe et leur lisibilité à petite taille.
Le contexte de dessin est obtenu avec getContext('2d', { alpha: false }). Sans couche
alpha, le navigateur compose plus vite, et comme le fond est un noir opaque, je ne perds rien au change.
L'état tient dans un tableau columns. layoutColumns() le remplit en divisant
la largeur de la fenêtre par COLUMN_WIDTH, fixé à 4 pixels, ce qui donne plusieurs
centaines de colonnes qui se chevauchent volontairement. Chaque colonne créée par
makeColumn() reçoit une taille de police entre 14 et 26 pixels, une vitesse
proportionnelle à cette taille, une opacité entre 0,35 et 1, et une position de départ au-dessus du
haut de l'écran. Les grosses polices descendent plus vite: c'est le seul indice de profondeur.
Le mécanisme central est la traînée, et elle ne coûte presque rien. À chaque image, je remplis tout le canvas d'un noir à 8 % d'opacité. Ce voile assombrit un peu ce qui était déjà là, image après image, jusqu'à ce que les vieux caractères se fondent dans le fond. La mémoire de la traînée est donc dans les pixels, pas dans une structure de données: je ne conserve jamais la liste des caractères déjà tracés.
Chaque colonne ne dessine ensuite que deux glyphes. La tête, en blanc verdâtre, avec
shadowColor et shadowBlur pour le halo; puis juste au-dessus un second
glyphe vert néon, un peu moins opaque, qui adoucit la transition. Les deux caractères sont retirés au
hasard à chaque image, ce qui donne l'impression que le texte se réécrit.
L'avancement se fait en unités de ligne plutôt qu'en pixels: col.y s'incrémente d'une
vitesse pondérée par le temps écoulé, et la position en pixels s'obtient en multipliant par la taille
de police. Une boucle de temps simple ignore l'image si moins de seize millisecondes se sont écoulées.
Quand une colonne est passée assez bas sous le bord, makeColumn() en fabrique une neuve à
la même abscisse.
La leçon principale, c'est qu'un effet peut vivre dans le rendu plutôt que dans les données. J'avais commencé en supposant qu'il faudrait garder pour chaque colonne la file des caractères affichés et redessiner la traînée au complet. Le rectangle semi-transparent rend tout ça inutile.
Le code en garde la trace. Chaque colonne possède encore des champs glyphs et
switchChance qui ne sont jamais relus, et un champ len qui ne sert plus qu'au
seuil de réapparition. Je les ai laissés comme points de réglage, mais c'est un bon exemple de dette
qui s'installe sans faire de bruit.
Dernier constat: un voile à 8 % ne ramène jamais un pixel exactement à zéro. Sur un fond noir opaque, ça ne paraît pas; sur un fond clair, la même technique laisserait une salissure.