ProjectedText
BêtaComposantUn 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
- Construit sur
- PhaseDriverViewProgressWebGLSurface
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.jsonProjectedText 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
- 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émentstaticest positionné (relative) le temps de l'instance. - La police vient de l'élément. Famille, graisse, style,
letter-spacingettext-transformsont relus au montage et à chaque changement de classe :font-black uppercasesuffit. Une police web est attendue avant le premier dessin. - Les couleurs sont des couleurs CSS.
color(les lettres) etbackground(les parois) acceptentvar(--primary),currentColor,oklch(…): le rendu suit le thème de la page. - 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
projection | L'espace | Les 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
| Prop | Type | Défaut | Description |
|---|---|---|---|
text | string | "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. |
projection | Projection | "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). |
color | string | "currentColor" | La couleur des lettres, toute couleur CSS (var(--primary), currentColor, oklch(…)). Elle suit le thème de la page. |
background | string | "transparent" | La couleur des parois, toute couleur CSS. Transparente, la projection laisse voir ce qu'il y a derrière. |
alternate | boolean | false | Inverse les deux couleurs une paroi sur deux : les murs du tunnel, le plafond du couloir. Le roulis du tunnel passe alors au demi-tour. |
scale | number | 0.45 | La 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. |
leading | number | 1.3 | Le pas des rangées, en hauteurs de lettre, de 1 (rangées jointives) à 3 (rangées aérées). |
gap | number | 0.6 | L'espace après chaque mot, en em, de 0 à 3. |
stagger | boolean | false | Décale une rangée sur deux d'un demi-mot. Sur le vortex, il dessine les bras de l'hélice. |
speed | number | 2 | Les 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. |
drift | number | 0 | Les 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. |
roll | number | 0 | Le 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. |
twist | number | 6 | Les bras de l'hélice du vortex, un entier de 0 (des anneaux) à 24 ; pair avec stagger ou drift. |
fov | number | 96 | Le 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. |
depth | number | "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. |
sway | number | 0 | La plongée de la caméra, en degrés, de 0 à 30 : elle descend puis remonte à chaque boucle. |
shade | number | 0 | L'assombrissement des deux couleurs vers la profondeur, de 0 à 1. |
drive | PhaseDrive | "loop" | La source de la phase : "loop" (le temps), "scroll" (le défilement) ou "manual" (progress). |
duration | number | 4 | La 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. |
cycles | number | 1 | Le nombre de boucles sur la plage de défilement, de 0 à 100 (drive="scroll"). |
range | ViewRangeShorthand | "cover" | La plage de défilement, comme animation-range en CSS : "cover", "contain", "entry 25% exit 75%"… |
scrollSubject | string | "" | 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. |
progress | number | 0 | La phase en mode manual, de 0 à 1 ; 1 donne la même image que 0. |
posterPhase | number | 0.15 | L'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. |
paused | boolean | false | Arrête la boucle, sans effet au défilement ni en mode manual. |
disabled | boolean | false | Retire 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 deposterPhase; enforced-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
coloretbackgroundpour 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).
- quand la projection répète un titre voisin, cachez-la aux lecteurs d'écran (
- 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
durationsi 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 grandrollsur 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/).