Cliquez (ou touchez) l'écran pour laisser tomber une balle colorée. Les pegs fixes dessinent le mot ANNABELLE en grille de points, complétés par une pente de pegs très serrés du coin haut-gauche vers le coin bas-droite (sans jamais empiéter sur les lettres) sur laquelle les balles peuvent rouler, avec gravité et friction en JavaScript pur — aucune librairie.
Il n'y a rien à gagner ici. On clique ou on touche n'importe où dans le cadre et une balle apparaît à cet endroit, avec un rayon tiré au hasard entre 8 et 18 pixels et une petite poussée horizontale aléatoire. Elle tombe, frappe les pegs rouges, rebondit sur les murs et sur les autres balles déjà présentes.
Les pegs fixes dessinent le prénom ANNABELLE. Une deuxième série de pegs beaucoup plus serrés forme une pente qui descend du coin haut-gauche vers le coin bas-droite. C'est là que ça devient intéressant, parce que les balles y glissent au lieu de simplement rebondir. Le compteur indique combien de balles sont encore dans le bac, et le bouton d'effacement vide tout d'un coup.
La boucle est une intégration d'Euler explicite pilotée par requestAnimationFrame. À
chaque image je mesure un delta-temps réel, borné à un trentième de seconde, pour qu'un onglet
revenu d'arrière-plan ne téléporte pas tout le contenu du bac d'un seul pas.
Les constantes sont volontairement peu nombreuses. GRAVITY vaut 1400 px/s²,
RESTITUTION 0,72, et GROUND_FRICTION 0,86 s'applique à la vitesse
horizontale seulement quand la balle touche le plancher. AIR_FRICTION vaut 0,9995, et
c'est un défaut que j'assume: elle multiplie la vélocité une fois par image plutôt qu'en fonction du
delta-temps, donc la résistance de l'air est en réalité deux fois plus mordante sur un écran à
120 Hz que sur un écran à 60 Hz. Sur un bac à sable ça ne se voit pas, mais dans un moteur sérieux
il faudrait passer par une puissance de dt. MAX_BALLS est fixé à 220;
au-delà, addBall fait un shift sur le tableau, donc la plus vieille balle
disparaît au lieu que la nouvelle soit refusée.
Les lettres viennent d'une police maison en matrices de 5 colonnes sur 7 rangées, une chaîne de
zéros et de uns par rangée. layoutPegs déroule les neuf lettres du mot avec une colonne
vide entre chacune, calcule un espacement qui tient à la fois dans la largeur disponible et dans la
moitié de la hauteur du bac, puis centre la grille. Le rayon des pegs est dérivé de cet espacement
et plafonné à 7 pixels.
La pente est générée à part par layoutSlopePegs: un vecteur unitaire le long de la
diagonale et un pas égal à 1,3 fois le rayon, donc des cercles qui se chevauchent franchement. C'est
ce chevauchement qui transforme une suite de plots isolés en surface continue. Chaque candidat est
rejeté s'il tombe dans la boîte englobante du mot — la boîte au complet, pas seulement les lettres
— ce qui laisse un trou net au milieu de la pente.
Toutes les collisions sont de type cercle contre cercle: calcul de la normale, séparation par la valeur du chevauchement, puis réflexion de la vélocité autour de cette normale avec la perte de restitution. Les balles entre elles utilisent le même schéma en échangeant la composante normale et en supposant des masses égales. C'est du O(n²), soit près de vingt-quatre mille paires testées par image à pleine charge, ce qui reste confortable dans un navigateur moderne.
La leçon la plus utile, c'est qu'il n'y a aucune friction tangentielle dans ce code. Une balle qui semble rouler sur la pente ne roule pas: elle enchaîne des micro-rebonds sur des cercles qui se touchent presque. La densité des pegs remplace un modèle de contact que je n'ai jamais écrit. C'est une triche, mais une triche qui m'a appris ce qu'un vrai moteur devrait modéliser.
L'autre chose, c'est la discipline du delta-temps. Chaque fois qu'une constante s'applique par image plutôt que par seconde, le comportement se met à dépendre du matériel. Je fais le même genre de vérification au travail quand je valide une chaîne vidéo: un résultat qui a l'air correct sur un appareil à 60 Hz peut raconter tout autre chose ailleurs, et ça ne se voit qu'en comparant.