- 3 200 particules, 30 KoLe ciel est en WebGL brut, écrit à la main — les nappes de fond qui dérivent comprises. L’original demande Three.js, React Three Fiber et une passe de post-traitement : 3,6 Mo de JavaScript.
- 19 animations mesuréesLues dans le bundle de l’original — durées, courbes, décalages, seuils — puis refaites sans GSAP, sans ScrollTrigger, sans Lenis.
- 0 requête externeAucune dépendance, aucun CDN, aucune étape de build. On ouvre le fichier et ça marche.
Ce que c’est
Le portfolio d’un développeur créatif, en trois pages. L’accueil : un hero plein écran, une déclaration, trois projets encadrés de deux rails collants, une scène épinglée où neuf services défilent en trois panneaux, un processus en quatre étapes, un appel de fin en très grand, un pied de page. La page « à propos » : le parcours, les outils dans une section entièrement en négatif, l’expérience. La page « work » : les trois projets en carrousel plein écran. Derrière les trois, le même champ de particules, qui change de forme section après section.
Le design reprend celui du site d’un design engineer en activité, dont rien n’a été gardé : ni les images, ni le code. L’original est une application Next.js qui embarque Three.js, React Three Fiber, GSAP avec ScrollTrigger et SplitText, Lenis et opentype.js : 3,6 Mo de JavaScript pour la seule page d’accueil. Ici la même page tient en 42 Ko.
C’est tout l’intérêt : ce template n’est pas là pour être admiré, il est là pour être lu, cassé et refait.
Ce que tu changes en premier
Les couleurs — quatre lignes
Tout en haut de assets/css/style.css :
--night: #070b1a; /* le fond, et la lueur qui monte du bas */
--halo: #243c7e; /* la lumière derrière la page */
--spark: #4a5a9e; /* les particules */
--ink: #f4f6fa; /* le texte */
Une chose à comprendre avant de toucher au reste : la couleur des particules existe deux fois. Un shader ne lit pas une variable CSS, donc --spark est répété dans field.js sous forme de trois nombres entre 0 et 1. Changer l’une oblige à changer l’autre.
Le nom
Cherche « Noray » dans index.html. Aucune classe CSS ne porte le nom de la marque : un rechercher-remplacer ne peut rien casser. Un endroit en plus : sampleGlyph('N') dans field.js — cette lettre est la forme que prennent les particules du pied de page.
Les tailles
L’échelle typographique est déclarée une fois, puis redéclarée à chaque point de rupture. Un titre n’a donc jamais sa propre media query : tu changes --h2 et toute la page suit.
Les 19 effets, et comment ils sont faits
L’original ne cache pas ses réglages : ils sont dans son bundle, sous forme de constantes nommées et d’appels GSAP en clair. Tout ce qui suit vient de là, puis a été mesuré dans le navigateur — jamais deviné à l’œil.
| Effet | Où | Comment |
|---|---|---|
| Lignes qui montent | 26 blocs de texte | le texte est découpé en vraies lignes par la mesure, chacune masquée ; yPercent 320 → 0 avec 10° d’inclinaison, 0,4 s, 0,07 s d’écart |
| Texte qui s’imprime | 4 titres | découpé en caractères, opacité 0,1 → 1 pilotée par le défilement ; le JS écrit une seule valeur, clamp() fait le reste |
| Étapes qui s’éclairent | le processus | l’opacité suit la distance au centre de l’écran — l’étape au milieu est celle qu’on peut lire |
| Scène épinglée | services | même mécanique, mais trois écrans au lieu de six : un écran de défilement par panneau. À six, la section met deux fois trop longtemps à passer |
| Couvertures en direct | travaux, page « work » | chaque couverture charge le vrai site voisin dans un cadre quand la carte arrive à l’écran — rendu en 1440 de large puis mis à l’échelle, pour qu’il serve bien sa mise en page de bureau. L’image fixe reste dessous, en repli et en attente |
| Rails jumeaux | travaux | le projet le plus proche du milieu de l’écran donne son nom à gauche et son lien à droite |
| Champ de particules | toute la page | 3 200 points en WebGL brut ; les formes sont calculées par superquadriques, pas modélisées — une étoile, un diamant taillé, une flamme, une lettre |
| Retrait devant le texte | sections de lecture | absent de l’original : sur une section marquée data-field-quiet, le canvas descend à 30 % d’opacité. Un nuage dense entre l’œil et un paragraphe, c’est joli une seconde et illisible ensuite |
| Ciel d’étoiles | toute la page | 45 % des points vivent sur une nappe lointaine, dessinée dans sa propre passe : elle ne tourne pas avec la forme, sinon la moitié des étoiles passerait derrière la caméra deux fois par tour |
| Nappes de fond | toute la page | un rectangle plein écran, deux octaves de bruit fractal qui glissent entre --night et --halo ; descendre les accélère, remonter les freine |
| Lettre en particules | pied de page | le glyphe est dessiné une fois dans un canvas 2D et ses pixels relus : 30 lignes au lieu d’une bibliothèque de 90 Ko |
| Six tours de défilement | toute la page | le nuage tourne de −2160° du haut en bas de la page, et continue un instant après l’arrêt |
| Graisse au curseur | logo, menu, liens du pied | Red Hat Display est une police variable : l’axe wght suit le curseur, 400 → 900 en 80 px |
| Curseur personnalisé | partout | quatre traits en mix-blend-difference qui se replient en anneau au-dessus d’un lien |
| Menu qui se déplie | en haut à droite | clip-path plutôt qu’une hauteur, plus inert pour que le panneau fermé quitte l’ordre de tabulation |
| Aperçu en grand | couvertures de projet | une carte ouvre le projet plein écran dans un <dialog> natif : le site voisin s’y charge vivant, il se défile et se clique pour de bon. Adresse relative, même origine, chargée au clic et jamais au chargement ; l’image fixe reste le repli |
| Transition de page | entre les trois pages | le contenu sort en 0,36 s, le nuage se disperse, puis le lien suit son cours. Seule une navigation ordinaire est interceptée — sans JavaScript, un lien reste un lien |
| Section en négatif | page « à propos » | le ciel bascule pour de bon : field.js interpole les deux shaders — particules comprises — vers leur variante claire. Une nappe CSS double la mise, et porte la section seule si WebGL manque |
| Carrousel de projets | page « work » | un vrai défileur horizontal avec scroll-snap : il glisse, se balaye et répond aux flèches avant qu’aucun script ne tourne. Le script ajoute la perspective, le tirage au pointeur, la molette verticale transformée en déplacement latéral, et le compteur |
Ce qu’il faut savoir
- Aucune requête externe. Tout est local, polices comprises. L’original va chercher ses bundles et ses polices sur son propre CDN.
- Le ciel est en CSS, pas dans le canvas. Le dégradé est un vrai fond ; le canvas ne dessine que les particules. Sans JavaScript, sans WebGL, ou si le contexte est perdu, il reste un ciel de nuit.
- Un contexte WebGL perdu se peint en blanc. Un portable qui sort de veille suffit. Le canvas écoute
webglcontextlost, s’efface, et reconstruit tout quand le contexte revient. - L’apparition au défilement ne peut pas masquer le contenu. L’état caché n’existe qu’une fois le JavaScript passé. Vérifié en retirant les balises
<script>: les 26 blocs restent visibles et les trois panneaux de services s’empilent les uns sous les autres. - Zéro style en ligne, zéro script en ligne. Le template respecte la politique de sécurité stricte du site, vérifié en le servant avec cette politique devant — pas supposé.
- Vérifié à 390, 1440 et 2560 px, au chargement et après avoir déroulé toute la page, sans débordement horizontal. Le banc est livré avec :
node _mesures/banc.mjs.
Ce qui n’est pas parfait
Mieux vaut le dire ici que te le laisser découvrir.
- Les trois couvertures de projet sont des rendus abstraits, pas des captures. Les couvertures de l’original sont des logotypes de marques réelles : impossible à reprendre, et une image générée n’a pas le droit d’en inventer un. Le nom du projet est écrit en HTML par-dessus l’image — ce qui a au moins l’avantage d’être traduisible.
- Les vidéos de la page « work » d’origine ne sont pas reprises. Elle y joue trois captures animées de 3 à 4 Mo chacune, en textures WebGL. Elles appartiennent à ses clients et ne sont pas redistribuables : le carrousel montre les mêmes images fixes que l’accueil. On perd le mouvement dans les cartes, on gagne 11 Mo.
- Les cartes ne se déforment pas comme celles de l’original. Là-bas, un shader plie le plan pendant le glissement. Ici c’est une perspective CSS et un cisaillement proportionnel à la vitesse : le mur incurvé y est, l’élasticité non.
- « See project » pointe sur l’ancre du projet. Les trois projets sont inventés, il n’y a donc pas de destination honnête. Remplace les trois
hrefpar tes URL. - Pas de post-traitement. L’original passe la scène dans un bloom. Ici la lueur est dans le shader du point lui-même — un cœur, un halo, un mélange additif. La différence se voit surtout sur les plus gros points, un peu moins diffus.
- Le sélecteur de langue est décoratif. L’original a une version espagnole complète ; un template d’une page n’a qu’une langue.
- Le formulaire de contact n’envoie rien. C’est un
<dialog>natif et le bouton d’envoi remet les champs au client de messagerie du visiteur. L’endroit où brancher ton point d’entrée est indiqué dansmain.js.
Il t’attend dans la bibliothèque.
Noray est inclus dans l’abonnement, avec le reste de la bibliothèque et la formation complète.
Accès immédiat · Sans engagement · Résiliable en un clic