Retour à la Knowledge Base
Breakthrough

silhouette-reveal-light-narrative

Avril 2026·Projet : Ayari Racing (ayari.racing)
ReactViteTypeScriptvanilla requestAnimationFrameCSS compositing

Vécu projet : Ayari Racing (ayari.racing) Date : Avril 2026 Stack : React 18, Vite, TypeScript, vanilla requestAnimationFrame, CSS compositing

Contexte

Animation d'intro de 4 secondes jouant avant qu'une vidéo YouTube ne démarre en fond de hero. Le brief : des particules dorées flottent dans le noir, convergent vers le centre, chargent un halo de lumière qui révèle progressivement le logo, puis explosent en diffusant une onde lumineuse qui remplit la page et laisse apparaître la vidéo sombre derrière. Le logo disponible est un wordmark bitmap (une variante "black typo" sur transparent).

Problème

Trois traps successifs, chacun révélant une incompréhension plus profonde du modèle mental sous-jacent :

Trap 1 — animer l'opacity du logo pour le révéler. Sur fond sombre, ça crée des frames intermédiaires où le logo ressemble à un ghost dimmé à opacity ~0.3. Pas de narration "lumière qui s'allume", juste un fade triste. Surtout : le fade se déclenche avec le temps, pas avec l'arrivée des particules — impossible de faire passer le message que les particules sont la CAUSE de la révélation.

Trap 2 — utiliser mask-image sur le logo pour ouvrir une zone révélée progressivement depuis le centre. Ça marche techniquement pour "découper un voile", mais pas pour "allumer une lumière". La métaphore est fondamentalement différente : on veut que ce qui se passe DANS la scène (particules, halo) soit la cause visible de la visibilité, pas qu'un effet indépendant déforme le logo lui-même.

Trap 3 — animer toutes les propriétés avec une seule courbe de temps. On a commencé phase 2 avec easeOutCubic(pp) comme "revealProgress" partagé pour le halo, l'opacity du logo, le drop-shadow, le scale. Mais la convergence des particules utilise easeInOutCubic(ppLocal) — une S-curve lente au démarrage. Résultat : à pp=0.2, revealProgress=0.49 (halo déjà à mi-charge), mais easeInOutCubic(0.2)=0.104 (particules quasi-immobiles). Le halo grossit avant que les particules n'arrivent. La causalité perçue est cassée visiblement : le spectateur voit le halo "en avance sur sa cause".

Solution

Deux insights combinés, formant un modèle de compositing cohérent :

1. Silhouette reveal by backlight

Le logo reste toujours à opacity 1, sans mask, sans filtre, sans drop-shadow coloré. On ne le touche jamais. À la place, on construit un "éclairage de scène" derrière lui :

  • Fond sombre (radial-gradient(ellipse at center, #1a1f2e 0%, #0a0e18 70%, #050709 100%)) — le logo noir disparaît naturellement dessus (dark-on-dark).
  • Particules placées DERRIÈRE le logo (z-index inférieur) avec mix-blend-mode: screen et un gros box-shadow warm gold. Chaque particule devient une source de lumière additive qui illumine le backdrop là où elle est.
  • Light cone circulaire sous le logo — un <div> avec un radial-gradient gold, qui grossit et s'intensifie pour former le halo central "chargé".
  • Le logo paint par-dessus avec alpha compositing normal. Les pixels noirs des lettres occludent la lumière peinte derrière ; les pixels transparents du PNG laissent passer.

Résultat visuel : partout où les particules ou le halo éclairent le backdrop, les lettres noires apparaissent en silhouette par contraste négatif. Le logo devient visible là où la scène est illuminée, invisible ailleurs. C'est littéralement un projecteur qui éclaire l'arrière d'un objet opaque, comme dans la réalité physique.

┌─────────────────────────────┐  ← overlay (dark bg)
│  z=1 : explosion wave       │  (phase 3 only)
│  z=2 : light cone (halo)    │  paints gold backdrop
│  z=3 : particles            │  mix-blend-mode: screen, brighten backdrop
│  z=4 : logo (opacity 1)     │  occludes light → silhouette emerges
└─────────────────────────────┘

2. Narrative curves must share timing

Quand plusieurs propriétés animées racontent la même beat narrative ("les particules arrivent → le halo charge → le logo prend force"), elles doivent partager une seule variable de progression, pas des courbes indépendantes qui divergent.

Fix concret : introduire un chargeProgress unifié avec démarrage retardé, qui pilote simultanément toutes les propriétés qui montent ensemble :

const pp = (progress - emergenceEnd) / (revealEnd - emergenceEnd);

// Charge progress — delayed until particles have meaningfully begun converging
const chargeStart = 0.35;
const chargePp = Math.max(0, (pp - chargeStart) / (1 - chargeStart));
const chargeProgress = easeInOutCubic(chargePp);

// Halo (scale + opacity)
lightCone.style.transform = `scale(${0.65 + chargeProgress * 2.4})`;
lightCone.style.opacity   = String(0.12 + chargeProgress * 0.83);

// Logo (scale) — same variable → synchronized with the halo
triangle.style.transform  = `scale(${0.95 + chargeProgress * 0.1})`;
logoGroup.style.transform = `scale(${1 + chargeProgress * 0.05})`;

Avant chargeStart, chargeProgress = 0 → halo et logo restent figés sur leur état de fin-de-phase-1. Pas d'anticipation. Puis tout démarre ensemble en S-curve, au moment où les particules ont visiblement commencé leur course vers le centre.

Pourquoi

Silhouette reveal by backlight marche parce que le cerveau humain interprète instantanément un objet noir contre un fond lumineux comme "éclairé par l'arrière". C'est une lecture primitive de contraste, pré-cognitive. Animer l'opacity d'un logo donne l'impression que le logo lui-même change ; animer la lumière derrière donne l'impression que la scène change — ce qui est infiniment plus riche narrativement et physiquement crédible.

Narrative curves unifiées garantissent la causalité perçue. Si la courbe du halo avance plus vite que celle des particules, le spectateur voit le halo "en avance" et l'animation semble pré-enregistrée plutôt que causale. En partageant la même courbe (avec le même délai), tout ce qui est censé être causé par le même événement bouge au même rythme — le spectateur lit la chaîne de causalité automatiquement.

Le remap Math.max(0, (pp - start) / (1 - start)) évite les if branches dans le code per-frame et garantit la continuité C0 à la frontière : chargePp = 0 exactement au seuil, et easeInOutCubic(0) = 0, ce qui donne exactement l'état de fin de phase précédente. Pas de saut visuel possible par construction.

La révélation devient émergente, pas scriptée. On ne dit jamais "à 1.76s, révèle le logo". On dit "à 1.76s, les particules commencent à charger le halo". Et le logo devient visible parce que le halo l'illumine. La différence est subtile mais fondamentale : l'animation raconte une histoire physique, pas une séquence temporelle.

Points d'attention

  • Z-order critique : si les particules sont AU-DESSUS du logo (pas en dessous), leur mix-blend-mode: screen blend avec les pixels du logo et brighten les lettres noires elles-mêmes — effet indésirable, on veut la lumière DERRIÈRE. Vérifier que les particules sont au z-index strictement inférieur à celui du logo.
  • Le logo doit être un PNG avec alpha transparent autour des lettres. Un SVG avec un fond blanc, un PNG avec fond opaque, ou un logo dont l'alpha layer ne découpe pas proprement les lettres casse l'effet silhouette.
  • mix-blend-mode blend avec le backdrop, pas avec les éléments au-dessus. Le logo au-dessus des particules paint normalement (alpha compositing) — il ne participe pas au blend. C'est ce qu'on veut, mais il faut le savoir pour ne pas s'attendre à des effets de blend logo/particule.
  • Ancrer le "zéro-state" de chaque phase à la valeur de fin de phase précédente. Sinon, à la frontière, un saut discret apparaît. La continuité est gratuite si on construit les courbes pour arriver à 0 et 1 exactement aux bonnes frames — mais il faut le faire volontairement.
  • Choisir la durée globale d'abord, puis sub-diviser les phases, puis tuner les courbes. Inverse (ajuster une courbe en isolation pour "sentir bien") mène à des désynchros entre éléments narrativement liés.
  • Ne pas abuser du délai (chargeStart). À 0.35, la charge a 65 % de la phase 2 pour se dérouler, ce qui suffit. Au-dessus de 0.5, la charge devient précipitée et perd la sensation de "build up".
  • Harmony gradient final : calculer les stops pour que la zone "brightness" match la taille de l'élément révélé. Pour un logo wordmark de 280px sur un viewport 1920×1080, une wave 150vmax × 150vmax avec gold au stop 16-22% positionne l'aura dorée juste autour du logo. Les stops sont dépendants des dimensions de l'élément central et du viewport — penser en pourcentages du rayon de la wave.
  • Seamless handoff vers le contenu qui suit : le dernier stop de la gradient doit matcher la couleur de ce qui s'affiche après l'overlay. Ici #000 pour raccorder au bg-black de la vidéo YouTube. Sinon, le fade de l'overlay crée un flash de transition.

Dernière mise à jour : avril 2026