Cinq cents particules dérivent sur un fond presque noir. Le curseur agit comme un centre d'attraction: dès qu'il entre dans un rayon de 260 pixels autour d'une particule, celle-ci accélère vers lui. L'essaim se resserre en spirale quand le curseur reste immobile, et s'étire en filaments quand on le déplace vite.
Quand une particule touche le curseur — moins de 16 pixels — elle est repoussée au lieu d'être avalée, ce qui empêche l'essaim de s'effondrer en un seul point. La couleur indique la vitesse: bleu quand ça dérive, rouge vif quand ça file. Si le curseur quitte la fenêtre, l'attraction s'éteint et l'essaim retombe au repos.
Chaque particule est un objet ordinaire: position, vitesse en x et en y, masse tirée entre 0,6 et 2,4, et un rayon dérivé de cette masse. Pas de tableau typé: cinq cents objets simples suffisent pour tenir soixante images par seconde.
L'intégration est un Euler semi-implicite à pas fixe: à chaque image je calcule l'accélération, je l'ajoute à la vitesse, puis j'ajoute la vitesse à la position. Il n'y a pas de delta de temps réel dans l'équation — le pas, c'est l'image. Moins rigoureux qu'un pas variable, mais stable et prévisible.
L'attraction suit une loi en inverse du carré de la distance: la constante
GRAVITY_STRENGTH vaut 420 et est divisée par la distance au carré. Deux garde-fous
encadrent le calcul. La distance au carré est bornée par le bas à MIN_DIST, soit 14
pixels, pour éviter une accélération qui part à l'infini quand une particule frôle le curseur. Et la
vitesse obtenue est plafonnée par MAX_SPEED, neuf pixels par image.
Un détail n'est pas de la vraie gravité, et je l'assume: je divise ensuite l'accélération par la masse de la particule. En gravitation, la masse du corps attiré s'annule et tout tombe pareil. Ici, les grosses particules réagissent moins que les petites, ce qui donne un effet de profondeur.
Le reste est du réglage: une friction de 0,992 par image, un rebond sur les bords qui conserve 70 % de
la vitesse entrante, et une collision qui applique une impulsion de 5,5 le long de la normale de
contact avant de replacer la particule juste à l'extérieur du rayon, pour qu'elle ne colle pas. La
traînée n'est pas un effet séparé: au lieu d'effacer le canvas, je peins par-dessus un rectangle de la
couleur du fond à 22 % d'opacité. speedToColor() termine en interpolant du bleu vers le
rouge selon la vitesse normalisée.
Les garde-fous comptent plus que la formule. Sans le plancher sur la distance, une particule qui passe à un pixel du curseur reçoit une accélération énorme, traverse l'écran en une image et ne revient jamais. Sans le plafond de vitesse, la friction ne rattrape plus rien. La physique naïve est facile à écrire; ce sont les cas limites qui décident si l'animation tient.
J'ai aussi retenu qu'un paramètre bien choisi vaut mieux qu'une simulation exacte. Diviser par la masse est faux physiquement, mais ça donne la lecture de profondeur que je cherchais. Tant que le but est de montrer plutôt que de mesurer, l'exactitude n'est pas le bon critère.
Enfin, le budget par image devient très concret: cinq cents racines carrées et cinq cents tracés de cercle en moins de seize millisecondes.