Effets vivants
Quatre effets qui maintiennent une page animée une fois son contenu affiché :
- Aurore du héros : un champ de lumière douce WebGL derrière le héros qui se courbe autour du pointeur.
- Cartes hologrammes : une carte s'incline vers le curseur, se soulève et attrape un reflet et un bord irisé.
- Projecteur de grille : la grille de fond de la page s'illumine dans un cercle doux autour du curseur.
- Texte glitch et brouillé : les titres se décomposent RVB à l'arrivée et les étiquettes des liens se décodent au survol.
La règle est « vivant, jamais gênant ». Les gens doivent sentir que la page réagit à eux, mais rien ne doit saccader, décaler la mise en page, voler les clics, ou cacher du texte à un lecteur d'écran.
Exemple en direct : https://www.lamanhtruong.com (survolez les cartes de projet, déplacez la souris sur le héros, survolez la navigation).
Contenu de assets/fx/
| Fichier | Rôle |
|---|---|
ticker.ts | Une boucle requestAnimationFrame partagée qui se met en veille quand personne ne demande d'images |
built.ts | Porte de disponibilité (isBuilt, buildActive), frozen(), PRNG basé sur une graine, requête média du pointeur |
aurora.ts | Le shader WebGL, son adaptateur de qualité et son cycle de vie (createAurora) |
hero-shader.tsx | <HeroShader />, l'enveloppe React pour l'aurore |
holo.ts | Inclinaison + reflet + brillance sur les cartes, physique à ressort, superposition injectée |
spotlight.ts | Projecteur de grille qui suit le curseur (transform uniquement) |
glitch.ts, glitch-copies.tsx | Planificateur de glitch, <GlitchCopies /> et <GlitchHeading /> |
scramble.ts, scramble-text.tsx | Exécuteur de brouillage et le crochet de balisage <ScrambleText /> |
fx-root.tsx | <FxRoot /> : monte holo, projecteur, glitch et brouillage une fois |
fx.css | Tous les styles, thématisés avec des propriétés personnalisées --fx-* |
Le code nécessite uniquement React ; aucune bibliothèque d'animation. Les modèles sont typés sous TypeScript strict pour les types React 18 et 19.
Installer
- Copiez
assets/fx/dans l'application, par ex.src/components/fx/. Gardez les fichiers ensemble, car ils s'importent mutuellement par chemin relatif. - Montez-le. La page elle-même peut rester un composant serveur statique ; seuls
FxRootetHeroShadersont des composants client.
import { FxRoot } from "@/components/fx/fx-root";
import { HeroShader } from "@/components/fx/hero-shader";
import { GlitchHeading } from "@/components/fx/glitch-copies";
import { ScrambleText } from "@/components/fx/scramble-text";
export default function Page() {
return (
<div className="fx-grid"> {/* grille de base fixe ; ignorez si vous avez la vôtre */}
<main className="relative z-[1]"> {/* le contenu se trouve au-dessus de la grille + projecteur */}
<section className="relative min-h-[80vh]"> {/* le héros doit être position: relative */}
<HeroShader /> {/* premier enfant, derrière le contenu du héros */}
<h1>…</h1>
</section>
<GlitchHeading as="h2" className="text-5xl font-semibold">Travaux sélectionnés</GlitchHeading>
<article data-holo className="rounded-2xl border p-6">
<a href="/work/x" className="inline-flex items-center gap-2">
<ScrambleText>Lire l'étude de cas</ScrambleText> <ArrowIcon />
</a>
</article>
</main>
<FxRoot /> {/* une fois par page, à l'intérieur de l'enveloppe de grille */}
</div>
);
}
- Thématisez-le : éditez les variables
--fx-*en haut defx.css, et passezpalette/intensityà<HeroShader />(voir « Thématisation » ci-dessous). - Vérifiez avec les tests Playwright dans
references/verification.mdavant de déclarer la tâche terminée.
Désactivez des pièces avec des props : <FxRoot holo={false} spotlight={false} />. Pointez l'inclinaison sur des cartes existantes sans toucher à leur balisage : <FxRoot holo={{ selector: ".card", within: "main" }} />.
Règles qui maintiennent la fluidité
Ces règles sont ce qui distingue « premium » de « saccadé ». Chacune existe parce que l'enfreindre a causé un problème visible.
-
Attendez que le contenu soit prêt. N'inclinez, ne glitchez et ne brouillez jamais quelque chose qu'un autre script est encore en train de révéler ou de mesurer. Une transformation sur un élément en cours de révélation corrompt les mesures de celui qui l'anime, et le résultat ressemble à un bégaiement.
built.tsimplémente un contrat : tant quehtml[data-build="on"]est défini, un élément n'est compté qu'une fois que ses[data-build-item]et[data-build-section]portent[data-built]. Sans moteur de build sur la page, tout est considéré comme construit immédiatement. Toute animation d'introduction peut y adhérer en définissant ces attributs. Détails :references/gating-and-performance.md. -
Utilisez une seule boucle d'images, et laissez-la dormir. Chaque effet s'abonne à
ticker.tsau lieu d'appelerrequestAnimationFramelui-même. Un abonné renvoiefalselorsqu'il s'est stabilisé, et lorsque personne n'a besoin d'images, la boucle s'arrête. Une page inactive ne coûte alors rien, et les effets ne peuvent pas se battre pour les images. Le glitch n'utilise aucune image : ce sont des images clés CSS activées par un attribut. -
Animez uniquement
transform,opacityetclip-path. Ceux-ci restent sur le compositeur. Rien ici ne change la taille, la position dans le flux ou les mesures de police, donc le décalage de mise en page est nul par construction. Le brouillage maintient le vrai texte en place (transparent) et dessine les glyphes dans une superposition, donc même une étiquette changeante ne change jamais de largeur. -
Adaptez l'effet à l'entrée. L'inclinaison et le projecteur suiveur n'existent que sur
(hover: hover) and (pointer: fine). Sur le tactile, s'incliner sous un doigt est perçu comme un bug, donc le tactile reçoit une courte atténuation au point de contact et une lentille de shader à dérive automatique à la place. -
Laissez le DOM de React tranquille au repos. La
<span>de superposition holo et latransformen ligne sont ajoutées lorsqu'une carte s'engage et supprimées lorsqu'elle est revenue au repos. Au repos, le DOM est exactement ce que React a rendu, donc les rendus, l'hydratation et les styles:hoverde la carte continuent de fonctionner. Les couches de glitch et les étendues de brouillage sont écrites dans le balisage (<GlitchCopies>,<ScrambleText>) plutôt que d'être insérées autour des nœuds appartenant à React au moment de l'exécution ; la restructuration de l'arbre React de l'extérieur casse la réconciliation. -
Gardez le texte réel. Les copies de glitch sont côté client uniquement et
aria-hidden, donc le code HTML statique, la recherche dans la page et les lecteurs d'écran voient chaque titre une fois. La superposition de brouillage estaria-hiddenet le vrai libellé reste le nom accessible tout du long. -
Nettoyez et survivez au StrictMode. Chaque
setup*renvoie un dispositif de suppression qui supprime les écouteurs, observateurs, superpositions et styles en ligne. Le shader libère son buffer et son programme mais n'appelle délibérément pasWEBGL_lose_context, car React StrictMode monte, démonte et remonte sur le même canvas, et un contexte perdu le reste. -
Budgétisez le GPU. Le shader s'affiche en résolution réduite sous un budget de pixels, ne dessine que lorsqu'il est visible, limite les dessins à ~80 par seconde sur les écrans à haut rafraîchissement, et réduit la qualité d'un cran si la page tourne en dessous de ~38 fps. Il refuse de démarrer sur une solution de repli logicielle (
failIfMajorPerformanceCaveat) et n'affiche rien plutôt que quelque chose de lent.
Les effets
Shader aurore du héros (aurora.ts, hero-shader.tsx)
Le shader est un bruit de valeur à déformation de domaine (fbm déformé par fbm), dessiné comme une large lueur plus de fines bandes brillantes le long des contours déformés. Le pointeur agit comme une lentille : il magnifie et tourbillonne le flux à proximité. Entrer dans le héros, cliquer ou se déplacer rapidement dépose un doux anneau d'ondulations. La sortie est un alpha prémultiplié et atténué à chaque bord, de sorte que l'arrière-plan de la page et toute grille fixe apparaissent, et que le héros se fonde dans la section suivante au lieu de se terminer dans une boîte.
Cycle de vie : le canvas s'estompe en 1,8 s une fois sa section construite. Il s'affiche uniquement lorsque la section est à l'écran (IntersectionObserver avec une marge de 80 px) et que l'onglet est visible. Si WebGL est absent, il n'affiche rien.
Boutons de réglage :
palette:{ veil, primary, secondary, accent }en hex. La palette est compilée dans le shader, donc les changements nécessitent un remontage.intensity: multiplie la lumière. 0,6 est calme, 1,4 est fort.- Lisibilité du texte : le shader biaise la lumière loin du bas à gauche, où se trouve généralement le texte du héros. Si votre texte se trouve ailleurs, modifiez la ligne
bias(voir la référence).
Lisez references/aurora-shader.md avant de modifier les calculs du shader, la vitesse ou l'adaptateur de qualité.
Cartes hologrammes (holo.ts)
Un pointerover délégué trouve la carte sous un pointeur fin. À l'engagement, il mesure la boîte non transformée de la carte, injecte une étendue de superposition aria-hidden, et exécute un ressort à amortissement critique (sous-échantillonné à 120 Hz pour se comporter de la même manière à 60 ou 144 ips) vers une inclinaison visant le pointeur. Le bord sous le curseur vient vers le spectateur. Au départ, il revient en arrière avec un léger dépassement et supprime tout une fois stabilisé.
- L'inclinaison est à l'échelle de la taille de la carte, de 2,5° à 7,5° : les grandes cartes s'inclinent moins pour que leurs bords éloignés ne balancent pas. La perspective est 1,6× le côté le plus grand. Le soulèvement est de 3 px.
- La superposition a un reflet radial au pointeur, un lavis de couleur diagonale faible, et un anneau en dégradé conique masqué sur la bordure (tamisé partout, lumineux près du pointeur).
- Sur une carte
overflow: hidden, la boîte de bordure de 1 px est coupée, donc l'anneau est dessiné juste à l'intérieur (.fx-holo--in, détecté automatiquement). - La superposition se trouve à
z-index: 3avecpointer-events: none, donc elle peint au-dessus des contrôles à l'intérieur de la carte (un bouton vidéo, un lien) sans bloquer les clics. - Une carte en position statique reçoit
position: relativeuniquement lorsqu'elle est active. - Désabonnement :
data-holo="off"sur une carte ou un ancêtre. Mettez en pause toute inclinaison avechtml[data-fx-freeze]pendant la lecture de vrais rects (un inspecteur, un passage de capture d'écran). L'interrupteur d'inspection de living-ui-swarm est détecté automatiquement.
Projecteur de grille (spotlight.ts, div.fx-spot)
Une fenêtre fixe de 560 px, masquée radialement, contient une copie plus lumineuse de la grille de base. Chaque image déplace seulement deux transformations : la fenêtre suit le curseur, et la grille à l'intérieur est décalée en arrière par floor(x / cell) * cell - x pour que ses lignes tombent exactement sur le réseau de la grille de base. Les deux grilles doivent être ancrées à l'origine de la fenêtre d'affichage, et .fx-grid::before l'est. L'interpolation de traînée se stabilise et la boucle dort lorsque le curseur s'arrête. Elle se cache lors du flou de la fenêtre ou lorsque le pointeur quitte la fenêtre. Le tactile reçoit une atténuation de 360 ms au point de contact.
La cellule de la grille, l'espacement des lignes fines et la taille de la fenêtre proviennent de --fx-grid-cell, --fx-grid-fine et --fx-spot-size (valeurs en px), donc le CSS reste la seule source de vérité.
Titres glitch (glitch.ts, <GlitchHeading> / <GlitchCopies>)
Balisage : une enveloppe .fx-glitch[data-fx-glitch] contenant le vrai titre (.fx-glitch__base) et deux copies côté client uniquement. GlitchHeading écrit tout ; utilisez GlitchCopies directement lorsque le titre existe déjà et porte d'autres attributs.
- Entrée : 560 ms lorsqu'un titre construit est vu à au moins 50 %. Le même titre ne rejoue pas dans les 2,5 s.
- Micro-glitch : 260 ms sur un titre visible aléatoire toutes les 8-14 s.
- L'aléatoire est basé sur une graine, donc le rythme est le même à chaque visite et dans les tests.
- Un seul titre glitch à la fois ; les entrées se mettent en file d'attente les unes derrière les autres.
- Implémenté comme des images clés CSS pures activées par
[data-glitch]: tranche avecclip-path: inset(), décalage avectranslate3d, et division des couleurs avectext-shadow. --fx-gimet à l'échelle les décalages par exécution.
Texte brouillé (scramble.ts, <ScrambleText>)
Enveloppez uniquement le libellé, jamais l'icône : <a><Icon/> <ScrambleText>{label}</ScrambleText></a>.
- Déclencheurs : survol (pas le tactile) ou
:focus-visibleclavier sur lea,buttonou[data-fx-scr-host]englobant. - Durée : 360-520 ms, selon la longueur.
- Résolution : les caractères se résolvent de gauche à droite avec une petite gigue. La queue non résolue se redessine à ~18 Hz plutôt qu'à chaque image, donc elle se lit comme un décodage, pas comme du bruit.
- Répertoires de glyphes : correspondant à la casse, et la ponctuation comme
@ . / : -reste en place afin que les e-mails et les chemins restent lisibles pendant le brouillage. - Nettoyage : la superposition est toujours vidée, y compris une minuterie de secours pour les onglets d'arrière-plan où le rAF s'arrête.
Utilisez inline pour les liens en ligne simples (conserve le soulignement). Utilisez quiet sur les boutons remplis, avec --fx-scr-tail: currentColor sur le bouton.
Thématisation
| Variable | Utilisée par | Défaut |
|---|---|---|
--fx-accent | couche A du glitch, glyphes de brouillage, lueur du projecteur, ombre holo | #5eead4 |
--fx-hot | couche B du glitch | #f472b6 |
--fx-sheen-1..5 | anneau holo, dans l'ordre autour du cercle | sarcelle, bleu, violet, rose, ambre |
--fx-grid-line, --fx-grid-fine-line | lignes de la grille de base | ardoise alpha faible |
--fx-grid-cell, --fx-grid-fine | espacement de la grille (px) | 96px, 24px |
--fx-spot-size | fenêtre du projecteur (px) | 560px |
--fx-mono | police des glyphes de brouillage | mono système |
--fx-scr-tail | remplacement de la couleur des glyphes par élément | non défini |
Les couleurs se mélangent via color-mix(in srgb, …), pris en charge dans tous les navigateurs actuels. Le shader prend sa propre palette : <HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />.
Choisissez les couleurs du shader dans la même famille que l'accent de la page. Le shader est une ambiance, pas une deuxième couleur de marque ; si un visiteur pour la première fois remarque le shader avant le titre, c'est trop fort.
Superposition
| Couche | z-index | Notes |
|---|---|---|
Grille de base (.fx-grid::before, fixe) | 0 | Ancré à l'origine de la fenêtre d'affichage |
Projecteur (div.fx-spot, fixe) | 0 | Après la grille dans l'ordre de l'arbre, donc il peint au-dessus d'elle |
Contenu (main, footer) | 1 | Doit être positionné avec z-index: 1 |
Shader du héros (canvas.fx-shader) | auto | Absolu à l'intérieur du héros, premier enfant, donc le contenu ultérieur peint par-dessus |
| Superposition holo | 3 à l'intérieur de sa carte | pointer-events: none |
| En-tête fixe, tout canvas de superposition d'animation de build | au-dessus du contenu | Non affecté |
Si le projecteur s'affiche au-dessus de votre texte, l'enveloppe de contenu n'a pas position: relative; z-index: 1.
Préférences de mouvement
Sur lamanhtruong.com, le propriétaire a choisi de jouer tous les effets pour tout le monde, y compris les visiteurs avec prefers-reduced-motion, car le mouvement est le point central du portfolio. C'est une décision produit légitime, mais faites-la intentionnellement. Pour respecter le paramètre à la place :
<FxRoot motion="reduce" />désactive les animations d'inclinaison, de projecteur, d'entrée de glitch et de brouillage en respectantprefers-reduced-motion: reduce. L'effet d'aurore se fige également. Les micro-glitch périodiques se produisent toujours, car ils ne provoquent pas de mouvement.- Le contrôle par attribut HTML fonctionne également :
<html data-fx-motion="reduce">désactive le mouvement pour tout le monde, indépendamment des préférences du navigateur.
Pour vérifier que le comportement est correct dans les tests, l'API matchMedia peut être simulée ou l'attribut data-fx-motion peut être défini directement.
Notes de performance
Les effets sont conçus pour être économes en ressources :
- La boucle d'animation partagée évite les conflits et la surcharge.
- Le shader ajuste automatiquement sa résolution et sa fréquence d'images.
- Le projecteur et l'inclinaison utilisent uniquement
transform, qui est optimisé par le navigateur. - Les animations de texte utilisent du CSS pur ou des superpositions légères.
- Le code se nettoie proprement, évitant les fuites de mémoire.
Accessibilité
Tous les éléments visuels ont des équivalents textuels ou des attributs ARIA appropriés :
- Les copies de glitch sont
aria-hidden. - Les superpositions de brouillage sont
aria-hidden, et le texte réel reste lisible. - Les animations ne bloquent pas la navigation au clavier.
- Les préférences de mouvement réduit sont respectées (voir ci-dessus).
Dépannage
- L'aurore ne s'affiche pas : vérifiez que WebGL est activé dans le navigateur et que le canvas a une taille suffisante.
- Les cartes ne s'inclinent pas : assurez-vous qu'elles ont
data-holo(ou un sélecteur configuré) et qu'il n'y a pas de conflit avec d'autres transformations. - Le projecteur ne suit pas la souris : confirmez que la grille de base est correctement positionnée en
fixed. - Le texte glitch ou brouillé ne fonctionne pas : vérifiez que les composants React sont correctement importés et rendus.
Si un problème persiste, vérifiez la console du navigateur pour les erreurs JavaScript et assurez-vous que les dépendances React sont correctement installées.