Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

ProjectedText

BêtaComposant

Un mot répété et projeté dans un tunnel, un couloir ou un vortex, en boucle sans couture ou au défilement.

Frameworks
ReactVueVanilla
Technologies
WebGL
Déclenché par
DéfilementContinuPiloté par l'application
Mis à jour le
Paramètres

ProjectedText répète un mot, ou une phrase courte, sur les parois d'un espace : un tunnel carré, un couloir ou un vortex. La caméra avance, roule ou plonge, et les rangées fuient vers le fond. Le texte devient un décor typographique plein cadre, net au premier plan et sans moiré au loin. Utilisez-le pour un hero, une section épinglée au défilement ou un bandeau ; pour un titre qu'on doit lire d'un coup d'œil, préférez un texte simple au-dessus.

Installation

Installez l'élément et ses dépendances en une commande :

pnpm dlx shadcn@latest add https://o-ui.hd987.dev/r/projected-text.json

ProjectedText dessine avec WebGL2, dans le contexte partagé de la page (WebGLSurface), et avance par une phase (PhaseDriver). Ces primitives s'installent avec lui. Sans WebGL2, le texte HTML s'affiche à la place. Les prérequis communs (Tailwind CSS v4, React 19 ou Vue 3.5) sont décrits dans Installation.

Utilisation

Donnez une taille à l'élément et une police à ses classes : il les reprend pour dessiner.

import { ProjectedText } from "@/components/o-ui/projected-text/projected-text";

export function Hero() {
  return <ProjectedText text="Motion" className="h-96 font-black uppercase" />;
}

Les règles

  1. Une hauteur. L'élément est un bloc : sans hauteur, il ne montre rien. Donnez-la par une classe (h-96, aspect-video) ou par son parent. La projection remplit sa boîte de padding ; en JavaScript seul, un élément static est positionné (relative) le temps de l'instance.
  2. La police vient de l'élément. Famille, graisse, style, letter-spacing et text-transform sont relus au montage et à chaque changement de classe : font-black uppercase suffit. Une police web est attendue avant le premier dessin.
  3. Les couleurs sont des couleurs CSS. color (les lettres) et background (les parois) acceptent var(--primary), currentColor, oklch(…) : le rendu suit le thème de la page.
  4. Un message important reste en texte simple à côté ou au-dessus : le décor bouge et se lit mal en entier, même s'il reste dans le HTML.

Exemples

Hero en tunnel

Le mot de marque file dans un tunnel aux murs alternés (alternate). Le fond plein du tunnel est une zone calme : avec un champ de vision plus étroit (fov) et un fond plus proche (depth), il accueille le titre et l'action. La projection répète le titre : elle est cachée aux lecteurs d'écran (aria-hidden).

Vortex au défilement

Le vortex reste épinglé (sticky) pendant que sa section défile. Sa phase suit la section, désignée par scrollSubject="section" : il avance quand on descend et recule quand on remonte. stagger décale une rangée sur deux pour dessiner les bras de l'hélice, et shade assombrit le fond.

Bandeau en couloir

Un bandeau d'annonce, large et bas. Le couloir montre un sol, un plafond et un mur du fond dont la rangée défile (drift) ; la caméra plonge et remonte à chaque boucle (sway). Le message est dans une bande unie, en texte simple.

Progression pilotée

Avec drive="manual", l'application fixe la phase : ici, la part des fichiers envoyés. La caméra avance avec l'envoi, et la phase 1 rejoint l'image de départ. C'est l'état de l'application : il est suivi même sous mouvement réduit. Donnez-le aussi en texte, comme le compteur de la démo : l'image seule ne se lit pas.

Personnalisation

Les trois projections

projectionL'espaceLes réglages qui comptent
"tunnel"Quatre murs, chacun lisible vers la profondeur, fermés par un fond plein.alternate, roll, depth
"corridor"Un sol et un plafond qui fuient, un mur du fond avec une seule rangée.drift (le mur du fond), sway, depth
"vortex"Un cylindre : les rangées suivent une hélice, les lettres restent droites.twist, stagger, shade, depth

Le rythme

  • speed : les rangées parcourues par boucle. Plus il est grand, plus le vol est rapide. Négatif, la caméra recule.
  • roll : le roulis de la caméra par boucle, en tours. Un demi-tour adouci (rollEase="inOut") sur un tunnel aux murs alternés donne un mouvement ample qui se pose à chaque boucle.
  • duration : la durée d'une boucle. Les mêmes réglages sur 8 s donnent un fond calme ; sur 2 s, une scène nerveuse.

Des combinaisons prêtes à copier

// Calm background: slow travel, no roll.
<ProjectedText text="Studio" duration={10} speed={2} className="h-96 font-semibold uppercase" />

// Graphic tunnel: alternating walls, half a turn per loop.
<ProjectedText text="Depth" alternate roll={0.5} speed={4} background="var(--primary)" color="var(--primary-foreground)" />

// Spiral: tight helix, staggered rows, a darker depth.
<ProjectedText text="Spin" projection="vortex" twist={8} stagger shade={0.35} />

Intégration

Une boucle sans couture

L'image ne dépend que de la phase, et ProjectedText arrondit ce qui doit l'être pour que la phase 1 redonne l'image de départ : un nombre entier de mots par mur ou par tour (déduit de scale), des rangées et des glissements entiers, un roulis qui ramène la surface sur elle-même (un quart de tour pour le tunnel, un demi avec alternate, un tour entier pour le couloir, une fraction de tour pour le vortex selon ses mots et ses bras), et un nombre pair de bras quand les rangées sont décalées.

Au défilement, dans une section épinglée

<section className="h-[300svh]">
  <ProjectedText
    drive="scroll"
    scrollSubject="section"
    range="contain"
    cycles={1.5}
    className="sticky top-0 h-svh font-black uppercase"
  />
</section>

range="contain" couvre le temps où la section remplit la vue ; cycles dit combien de boucles elle joue sur cette distance. Remonter fait reculer la scène.

Thème clair et sombre

Avec color="var(--primary)" et background="var(--background)", la projection suit le thème : les couleurs sont relues quand la classe, le style ou data-theme de <html> changent, et quand la préférence du système change.

Next.js et Nuxt

Les adaptateurs ne dessinent qu'au montage. Au rendu serveur, l'élément contient le texte HTML, centré, dans color sur background : la page n'est jamais vide, même sans JavaScript.

La pause de la page

En boucle, ProjectedText obéit à la pause commune des animations (FrameLoop) : un interrupteur relié à setAnimationsPaused l'arrête avec les autres. La prop paused l'arrête seul.

API

PropTypeDéfautDescription
textstring"Motion"Le mot ou la phrase répétée, de 1 à 48 caractères, espaces regroupés. Il reste le texte HTML de l'élément ; vide, il revient au défaut, comme le texte HTML.
projectionProjection"tunnel"La surface : "tunnel" (quatre murs et un fond plein), "corridor" (sol, plafond et mur du fond) ou "vortex" (une hélice dans un cylindre).
colorstring"currentColor"La couleur des lettres, toute couleur CSS (var(--primary), currentColor, oklch(…)). Elle suit le thème de la page.
backgroundstring"transparent"La couleur des parois, toute couleur CSS. Transparente, la projection laisse voir ce qu'il y a derrière.
alternatebooleanfalseInverse les deux couleurs une paroi sur deux : les murs du tunnel, le plafond du couloir. Le roulis du tunnel passe alors au demi-tour.
scalenumber0.45La hauteur des lettres par rapport au rayon de l'espace, de 0,1 à 1,5. Le nombre entier de mots par mur ou par tour en découle : un mot court se répète, un mot long tient seul.
leadingnumber1.3Le pas des rangées, en hauteurs de lettre, de 1 (rangées jointives) à 3 (rangées aérées).
gapnumber0.6L'espace après chaque mot, en em, de 0 à 3.
staggerbooleanfalseDécale une rangée sur deux d'un demi-mot. Sur le vortex, il dessine les bras de l'hélice.
speednumber2Les rangées parcourues par boucle, un entier de −12 à 12 ; négatif, la caméra recule. Pair avec stagger, arrondi en s'éloignant de 0.
driftnumber0Les mots glissés le long des rangées par boucle, un entier de −6 à 6, une rangée sur deux en sens inverse. Sur le couloir, il fait aussi défiler le mur du fond.
rollnumber0Le roulis de la caméra par boucle, en tours, de −2 à 2, arrondi à une symétrie de la surface : un quart de tour pour le tunnel (un demi avec alternate), un tour entier pour le couloir et son mur du fond, une fraction de tour pour le vortex, selon ses mots et ses bras.
rollEase"inOut" | "linear""inOut""inOut" : le roulis ralentit à chaque bout de boucle et s'y pose. "linear" : une rotation continue.
twistnumber6Les bras de l'hélice du vortex, un entier de 0 (des anneaux) à 24 ; pair avec stagger ou drift.
fovnumber96Le champ de vision, de 40 à 140 degrés, sur le côté le plus court de l'élément : en portrait, le tunnel tient dans la largeur. Large, les premières rangées sont immenses ; étroit, l'espace paraît plus profond et plus calme.
depthnumber | "auto""auto"La distance du fond plein (tunnel, vortex) ou du mur du fond (couloir), en rayons, de 1 à 40. "auto" : 3, 2,6 et 9. Plus proche, le fond plein grandit.
swaynumber0La plongée de la caméra, en degrés, de 0 à 30 : elle descend puis remonte à chaque boucle.
shadenumber0L'assombrissement des deux couleurs vers la profondeur, de 0 à 1.
drivePhaseDrive"loop"La source de la phase : "loop" (le temps), "scroll" (le défilement) ou "manual" (progress).
durationnumber4La durée d'une boucle, en secondes, de 0,1 à 600 (drive="loop"). Elle est allongée si le mouvement dépassait trois flashs par seconde.
cyclesnumber1Le nombre de boucles sur la plage de défilement, de 0 à 100 (drive="scroll").
rangeViewRangeShorthand"cover"La plage de défilement, comme animation-range en CSS : "cover", "contain", "entry 25% exit 75%"…
scrollSubjectstring""Un sélecteur CSS de l'ancêtre dont le défilement mène la phase (closest()) : la section haute d'une scène épinglée. Vide : l'élément lui-même.
progressnumber0La phase en mode manual, de 0 à 1 ; 1 donne la même image que 0.
posterPhasenumber0.15L'image fixe affichée sous mouvement réduit, et le départ de la boucle, de 0 à 1.
reducedMotion"always" | "never" | "user""user""user" suit la préférence du système ; "always" la force. "never" garde le mouvement : à réserver au cas où la personne a réactivé les animations dans les réglages de votre site.
pausedbooleanfalseArrête la boucle, sans effet au défilement ni en mode manual.
disabledbooleanfalseRetire le rendu WebGL : le texte HTML revient, centré, dans color sur background.

Une valeur hors plage est bornée ; une valeur invalide (NaN, un mauvais type, une couleur que le navigateur ne connaît pas) garde la valeur courante. En React, la ref vise la racine (un span en display: block) ; en Vue, defineExpose({ el }).

Accessibilité

  • Ce que fait ProjectedText : le vrai texte HTML existe dès le rendu serveur et reste lu par les lecteurs d'écran ; il n'est masqué (par l'opacité) qu'après le premier dessin. Le canvas est décoratif (aria-hidden). Sous mouvement réduit, l'élément montre l'image fixe de posterPhase ; en forced-colors, le texte HTML revient à la place de la projection.
  • Ce que vous faites :
    • quand la projection répète un titre voisin, cachez-la aux lecteurs d'écran (aria-hidden="true"), comme dans le hero ;
    • choisissez un contraste d'au moins 3:1 entre color et background pour un mot en grand, 4,5:1 si la projection porte un message ;
    • reliez la pause commune de la page à un interrupteur visible dès qu'une boucle dure plus de cinq secondes (WCAG 2.2.2).
  • Pas plus de trois flashs par seconde (WCAG 2.3.1) : en boucle, ProjectedText rejoue le mouvement tel qu'il le dessine, au format de l'élément, et allonge duration si un dixième de l'image ou plus dépasserait trois flashs dans une même seconde. Chaque rangée ou mot entier qui passe sur un point compte pour un flash : la règle porte sur la luminance d'une zone, et les traits d'une même lettre ne comptent pas à part. Tout s'additionne : les rangées qui défilent (speed), les mots qui glissent (drift), la plongée (sway) et le roulis, qui balaie les rangées et inverse les parois alternées. Un roulis rapide allonge donc nettement la boucle. Au défilement, c'est le visiteur qui règle la vitesse ; évitez tout de même un grand roll sur une plage courte.

Performances

  • Poids : 9,8 ko compressés, sans dépendance ; 19,6 ko avec WebGLSurface, PhaseDriver, ViewProgress et FrameLoop, partagés par les autres éléments de la page.
  • Un seul contexte WebGL pour la page, quel que soit le nombre de projections ; les trois programmes sont compilés en parallèle et partagés.
  • Un dessin seulement quand l'image change : une nouvelle phase, une nouvelle taille, une couleur, une police ou un écran d'une autre densité. Hors écran, en pause ou dans un onglet masqué, rien ne se dessine ; au défilement, rien ne se dessine tant que la page ne bouge pas. En couleurs forcées, sans WebGL ou après une perte de contexte, la boucle s'arrête aussi.
  • Le texte est préparé une fois par texte et par police (un champ de distance), dans une tâche à part, jamais pendant une image, et partagé par les projections du même mot. Un texte long est préparé plus petit, à coût constant (une quinzaine de millisecondes au plus sur un ordinateur récent).
  • Le plancher de flashs est mesuré une fois par réglage et par format, dans une tâche à part : quelques millisecondes, une vingtaine au plus avec un roulis. La boucle attend le résultat sur sa première image, et les instances aux mêmes réglages le partagent.
  • Rien ne se lit pendant une image : couleurs et police sont relues dans une tâche à part, au changement de thème ou de classe, et rien n'est redessiné si elles n'ont pas changé. Seul le programme de la projection affichée est compilé.
  • La taille de dessin est plafonnée à une densité de 2 et à 4 millions de pixels (WebGLSurface).

Problèmes fréquents

  • Rien ne s'affiche : l'élément n'a pas de hauteur. Donnez-lui une classe de hauteur, ou une hauteur à son parent.
  • Le mot paraît dans la police par défaut : la police web n'était pas encore chargée. ProjectedText redessine au chargement des polices ; vérifiez qu'elle est bien appliquée à l'élément lui-même.
  • La scène épinglée ne bouge pas au défilement : sans scrollSubject, l'élément épinglé mesure sa propre position, qui ne change pas. Désignez sa section.

Notes et limites

  • Navigateurs : WebGL2, dans tous les navigateurs récents. Sans lui, ou après une perte du contexte non restaurée, le texte HTML s'affiche.
  • Un texte court : au-delà de quelques mots, les rangées deviennent trop longues pour l'espace ; le texte est borné à 48 caractères.
  • Ne déplacez pas un dossier d'O-UI seul : ProjectedText importe ses primitives voisines (../webgl-surface/, ../phase-driver/).