Que fait Remotion Compose ?
Vous transformez un brief en langage naturel en une composition Remotion complète et prête à être prévisualisée – du vrai React/TypeScript avec animation pilotée par les images. Le résultat est un fichier .tsx dans src/compositions/, enregistré dans src/Root.tsx, que l'utilisateur peut immédiatement parcourir dans Studio ou confier à remotion-render.
Déclenchez-vous sur toute demande de création, génération, animation ou "écris le code vidéo pour" une démo produit, une présentation, un callout de fonctionnalité ou une animation marketing – y compris "construis une vidéo de 20 secondes montrant X".
Étape 1 – Extraire le brief
Ne commencez pas à coder avant d'avoir ces éléments. Posez-les en un seul lot compact ; comblez les lacunes évidentes avec des valeurs par défaut sensées plutôt que de bloquer :
- Durée en secondes, et fps (par défaut 30 – le standard pour les vidéos produit et marketing, et la moitié du coût de rendu du 60. Utilisez 60fps uniquement pour animer des mouvements fins de curseur ou des UI défilantes où le 30 bégaye visiblement).
- Scènes – ce qui se passe, dans l'ordre. Poussez pour une liste de beats : "logo, puis trois fonctionnalités, puis un CTA" plutôt que "une démo de mon app".
- Marque – couleurs hexadécimales primaire/accent/arrière-plan, et la police (un nom de famille
@remotion/google-fonts). - Rapport hauteur/largeur / plateforme – 16:9 paysage (1920×1080), 9:16 portrait (1080×1920) pour les réseaux sociaux, ou 1:1 carré (1080×1080).
- Texte – les véritables titres/noms de fonctionnalités/texte CTA. N'inventez jamais de revendications produit ; demandez les vrais mots.
Convertissez les secondes en images partout : durationInFrames = secondes * fps. Longueurs totales typiques : 15-60s pour une démo sociale/produit, 6-15s pour une coupure pub.
Étape 2 – Construire un plan de scènes
Une <Composition> par vidéo, un composant par scène, assemblés sur la timeline avec <Sequence>. Établissez le budget d'images avant d'écrire le code :
Total : 20s @ 30fps = 600 images
Intro (logo) images 0-90 (Sequence from=0 durationInFrames=90)
Fonctionnalité 1 images 90-240 (Sequence from=90 durationInFrames=150)
Fonctionnalité 2 images 240-390 (Sequence from=240 durationInFrames=150)
Fonctionnalité 3 images 390-540 (Sequence from=390 durationInFrames=150)
CTA outro images 540-600 (Sequence from=540 durationInFrames=60)
Seuils de rythme lors de l'établissement du budget :
- Donnez à tout beat qui porte du texte au moins 2 secondes (60 images à 30fps) – animation d'entrée plus maintien. Un titre a besoin d'environ 150-200ms de maintien par mot pour être lisible ; un titre de 6 mots veut ~1s de maintien après son arrivée.
- Animations d'entrée : 10-20 images. Fondu enchaîné et sorties : 12-15 images. Plus long semble lent, plus court semble un glitch.
- Aucune scène plus courte qu'environ 45 images sauf s'il s'agit d'un flash délibéré.
<Sequence from={F}> décale l'horloge locale des enfants de sorte qu'à l'intérieur de ce composant useCurrentFrame() commence à 0. Animez chaque scène comme si elle commençait à l'image 0.
Étape 3 – Référence API principale
useCurrentFrame()– l'image courante (l'horloge). Tout s'anime en fonction de cela.useVideoConfig()–{ fps, width, height, durationInFrames }.interpolate(frame, [imagesEntree], [valeursSortie], opts)– mappage linéaire. Passez toujours{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }pour les entrées/sorties afin que les valeurs ne dépassent pas leur plage.spring({ frame, fps, config })– 0→1 basé sur la physique (légèrement overshoot). Utilisez pour tout ce qui doit sembler vivant (pops, glissements, zooms).AbsoluteFill– un div plein écran positionné en absolu ; la couche de base de chaque scène.Sequence– décale temporellement et limite temporellement les enfants (le planificateur de scènes).Series/Series.Sequence– scènes consécutives sans calcul manuel des décalagesfrom.Img,staticFile– voir Étape 6.
Étape 4 – spring() vs interpolate()
Utilisez spring() quand… | Utilisez interpolate() quand… |
|---|---|
| Le mouvement doit sembler physique – pops, rebonds, zooms, glissements | Vous avez besoin d'un contrôle linéaire précis entre des images exactes |
| Vous voulez un atterrissage naturel / overshoot | Fondu (opacité 0→1), changements de couleur, barres de progression, tout ce qui est chronométré à l'image |
| Entrées de cartes, logos, badges | Fondu enchaîné et sorties où l'overshoot semblerait faux |
Schéma – pilotez une transformation avec un spring, une opacité avec interpolate :
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const pop = spring({ frame, fps, config: { damping: 200 } }); // 0 → 1, vivant
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const scale = interpolate(pop, [0, 1], [0.8, 1]);
damping: 200 est la configuration de travail – un atterrissage rapide avec un overshoot à peine perceptible. Un damping plus bas (~10-20) rebondit visiblement ; utilisez-le uniquement lorsque le rebond est l'objectif.
Étape 5 – Structure standard de démo produit
Encodez cette forme sauf indication contraire du brief : introduction logo/marque → N callouts de fonctionnalités → outro CTA. Rendez-le piloté par les données (Étape 6) pour que les variantes soient gratuites.
// src/compositions/ProductDemo.tsx
import {
AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig,
interpolate, spring,
} from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
export type Feature = { title: string; body: string };
export type ProductDemoProps = {
brand: string;
colors: { bg: string; fg: string; accent: string };
logoText: string;
features: Feature[];
cta: string;
};
const Intro: React.FC<{ text: string; colors: ProductDemoProps["colors"] }> = ({ text, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = spring({ frame, fps, config: { damping: 200 } });
const scale = interpolate(s, [0, 1], [0.7, 1]);
const opacity = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", alignItems: "center" }}>
<h1 style={{ fontFamily, fontSize: 120, color: colors.fg, transform: `scale(${scale})`, opacity }}>
{text}
</h1>
</AbsoluteFill>
);
};
const FeatureCallout: React.FC<{ feature: Feature; colors: ProductDemoProps["colors"] }> = ({ feature, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const slide = spring({ frame, fps, config: { damping: 200 } });
const x = interpolate(slide, [0, 1], [-60, 0]);
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", paddingLeft: 160 }}>
<div style={{ transform: `translateX(${x}px)`, opacity }}>
<div style={{ fontFamily, fontSize: 32, color: colors.accent, fontWeight: 700, letterSpacing: 2 }}>
{feature.title.toUpperCase()}
</div>
<div style={{ fontFamily, fontSize: 64, color: colors.fg, maxWidth: 1100, marginTop: 16 }}>
{feature.body}
</div>
</div>
</AbsoluteFill>
);
};
const Outro: React.FC<{ cta: string; colors: ProductDemoProps["colors"] }> = ({ cta, colors }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 18], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.accent, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ fontFamily, fontSize: 84, color: colors.bg, opacity }}>{cta}</h2>
</AbsoluteFill>
);
};
export const ProductDemo: React.FC<ProductDemoProps> = ({ colors, logoText, features, cta }) => {
const intro = 90;
const per = 150;
const outro = 60;
return (
<AbsoluteFill>
<Sequence durationInFrames={intro}>
<Intro text={logoText} colors={colors} />
</Sequence>
{features.map((f, i) => (
<Sequence key={i} from={intro + i * per} durationInFrames={per}>
<FeatureCallout feature={f} colors={colors} />
</Sequence>
))}
<Sequence from={intro + features.length * per} durationInFrames={outro}>
<Outro cta={cta} colors={colors} />
</Sequence>
</AbsoluteFill>
);
};
Enregistrez-le dans src/Root.tsx, en calculant durationInFrames à partir des mêmes nombres et en fournissant defaultProps :
<Composition
id="ProductDemo"
component={ProductDemo}
durationInFrames={90 + 3 * 150 + 60}
fps={30}
width={1920}
height={1080}
defaultProps={{
brand: "Acme",
colors: { bg: "#0B0B0F", fg: "#FFFFFF", accent: "#6C5CE7" },
logoText: "Acme",
features: [
{ title: "Rapide", body: "Livrez en secondes, pas en sprints." },
{ title: "Simple", body: "Un prompt, une vidéo." },
{ title: "Vôtre", body: "Votre marque, votre texte." },
],
cta: "Commencez gratuitement →",
}}
/>
Pour le portrait/réseaux sociaux, définissez width={1080} height={1920} ; pour le carré, 1080×1080. Gardez les composants identiques – seul le canevas change.
Étape 6 – Props pilotées par les données et ressources statiques
La composition ci-dessus est entièrement pilotée par les données : chaque mot et chaque couleur est une prop. C'est le but – les variantes (nouveau texte, nouvelle palette, texte localisé) ne nécessitent aucune nouvelle invite ; passez simplement des props différentes (remotion-render le fait à partir d'un fichier JSON pour les lots).
Les ressources statiques (logos, captures d'écran, audio) se trouvent dans public/assets/ et sont référencées avec staticFile() – jamais une importation ou un chemin brut :
import { Img, staticFile } from "remotion";
<Img src={staticFile("assets/logo.png")} />
Étape 7 – Sortie
Enregistrez le fichier final dans src/compositions/<NomVideo>.tsx, enregistrez-le dans src/Root.tsx, et dites à l'utilisateur de prévisualiser avec npx remotion studio (http://localhost:3000). Puis passez le relais : "Parcourez-le dans Studio. Dites-moi ce qui doit changer – trop rapide, mauvaise couleur, texte différent – et je modifie et relance le rendu (remotion-render)."
À NE PAS FAIRE
- N'animez PAS avec des transitions/animations CSS ou
setTimeout/setInterval. Remotion rend les images indépendamment et en parallèle ; tout ce qui n'est pas dérivé deuseCurrentFrame()est invisible ou incohérent dans le rendu final même si cela peut sembler correct dans l'aperçu du navigateur. - N'utilisez PAS
Math.random()pour les effets de particules dispersés – chaque worker de rendu obtient des valeurs différentes et les images cessent de correspondre. Utilisezrandom(seed)de Remotion pour un pseudo-aléatoire déterministe. - Ne récupérez PAS de données et n'attendez PAS de travail asynchrone sans
delayRender()/continueRender()– le renderer capture l'image avant l'arrivée des données. - Ne codez PAS en dur fps, width ou height dans les composants de scène – lisez-les depuis
useVideoConfig()pour que le passage de 30↔60fps ou paysage↔portrait ne casse pas chaque timing et disposition. - N'omettez PAS
extrapolateLeft/Right: "clamp"sur les interpolations d'entrée/sortie – l'opacité monte au-dessus de 1 et les éléments dérivent hors du canevas après la fin de leur plage. - N'inventez PAS de titres, de revendications de fonctionnalités ou de CTA – utilisez le vrai texte de l'utilisateur, et demandez-le s'il manque.
Barre de qualité
- Chaque valeur animée est une fonction pure de
useCurrentFrame()(plus les props) – parcourir en arrière dans Studio est identique à avancer. - Le budget d'images dans le plan correspond exactement à
durationInFramesdansRoot.tsx, et aucun beat textuel ne tient moins que son minimum lisible. - Tout le texte et les couleurs arrivent via des props typées avec
defaultPropsfournies – aucun littéral enterré dans les composants de scène. - Les ressources sont chargées via
staticFile(); le fichier s'enregistre proprement et s'affiche dans Studio sans erreur console.