Shader d'image au survol

Trois paysages de montagne. Passe la souris (ou le focus clavier) sur une image : elle se déforme autour du curseur, se sépare en canaux rouge / vert / bleu, puis se dissout vers une seconde photo. Au clavier, le focus déclenche exactement le même effet.

Sommetdevient Brume
Brumedevient Clarté
Lacdevient Aube

Démarche

Un fragment shader GLSL fait tout le travail, un vertex shader minimal pose juste un quad plein écran. Trois effets superposés :

  • Déformation liquide — une poussée radiale autour du pointeur, modulée par la vitesse de la souris et un léger tremblement de bruit.
  • Chromatic aberration — les canaux R et B sont échantillonnés à des offsets opposés du canal V, proportionnels à la distance au curseur.
  • Dissolution organique — un bruit fractal (fBM, 4 octaves) décide quels pixels basculent les premiers vers l'image d'arrivée, pour un seuillage doux.

Détails techniques

  • Lissage exponentiel indépendant du framerate pour la souris, la vitesse et la progression — la courbe 1 - exp(-dt * k) donne un suivi qui reste cohérent même si le navigateur drop des frames.
  • Mise en veille quand la carte sort du viewport (IntersectionObserver) et quand elle est au repos (plus rien ne bouge), pour épargner le GPU.
  • Respect de prefers-reduced-motion — un uniforme uReduit désactive la déformation et la dissolution bruitée, il reste un simple fondu entre les deux images.
  • Accessibilité clavier — tabindex="0" sur chaque carte, focus / blur déclenchent la même animation que pointerenter / pointerleave, avec la souris centrée.
  • Chargement non bloquant — les textures sont chargées en parallèle via Promise.all, chaque carte s'initialise dès que ses deux images sont prêtes.
  • Fallback sans WebGL — si getContext('webgl') renvoie null, on tombe sur un simple drawImage 2D de l'image de départ.

Pile

WebGL 1 brut (pas de lib), ES modules, Vue 3 pour l'intégration dans le portfolio. Images : photos de paysages de montagne libres de droits Unsplash, redimensionnées en 1024 × 1280 (ratio 4:5).

© 2026 Alla Lozinskaya