¿Qué hace Remotion Compose?
Conviertes un resumen en lenguaje natural en una composición completa de Remotion, lista para previsualizar - React/TypeScript real con animación basada en fotogramas. El resultado es un archivo .tsx en src/compositions/, registrado en src/Root.tsx, que el usuario puede examinar inmediatamente en Studio o pasar a remotion-render.
Actívalo ante cualquier solicitud de crear, generar, animar o "escribir el código de video para" una demo de producto, tutorial, presentación de funcionalidad o animación de marketing - incluyendo "construye un video de 20 segundos que muestre X".
Paso 1 - Extraer el resumen
No empieces a codificar hasta que tengas estos datos. Pregunta en un lote compacto; llena los vacíos obvios con valores predeterminados sensatos en lugar de detenerte:
- Duración en segundos, y fps (por defecto 30 - el estándar para video de producto y marketing, y la mitad del costo de renderizado que 60. Usa 60fps solo cuando animes movimiento fino del cursor o UI de scroll donde 30 tartamudea visiblemente).
- Escenas - qué sucede, en orden. Presiona por una lista de tiempos: "logo, luego tres funciones, luego un CTA" es mejor que "una demo de mi app".
- Marca - colores primario/de acento/fondo en hex, y la fuente (un nombre de familia de
@remotion/google-fonts). - Relación de aspecto / plataforma - 16:9 horizontal (1920×1080), 9:16 vertical (1080×1920) para redes sociales, o 1:1 cuadrado (1080×1080).
- Texto - los titulares/nombres de funciones/texto de CTA reales. Nunca inventes afirmaciones sobre el producto; pide las palabras reales.
Convierte segundos a fotogramas en todas partes: durationInFrames = seconds * fps. Las longitudes totales típicas: 15-60s para un demo social/de producto, 6-15s para un corte publicitario.
Paso 2 - Construir un plan de escenas
Una <Composition> por video, un componente por escena, unidos en la línea de tiempo con <Sequence>. Planifica el presupuesto de fotogramas antes de escribir código:
Total: 20s @ 30fps = 600 fotogramas
Intro (logo) fotogramas 0-90 (Sequence from=0 durationInFrames=90)
Función 1 fotogramas 90-240 (Sequence from=90 durationInFrames=150)
Función 2 fotogramas 240-390 (Sequence from=240 durationInFrames=150)
Función 3 fotogramas 390-540 (Sequence from=390 durationInFrames=150)
CTA outro fotogramas 540-600 (Sequence from=540 durationInFrames=60)
Umbrales de ritmo al presupuestar:
- Dale a cualquier tiempo que lleve texto al menos 2 segundos (60 fotogramas a 30fps) - animación de entrada más pausa. Un titular necesita aproximadamente 150-200ms de pausa por palabra para ser legible; un titular de 6 palabras quiere ~1s de pausa después de que aterrice.
- Animaciones de entrada: 10-20 fotogramas. Fundidos y salidas: 12-15 fotogramas. Más largos se sienten lentos, más cortos como un parpadeo.
- Ninguna escena de menos de ~45 fotogramas a menos que sea un corte rápido intencional.
<Sequence from={F}> desplaza el reloj local del hijo para que dentro de ese componente useCurrentFrame() comience en 0. Anima cada escena como si comenzara en el fotograma 0.
Paso 3 - Referencia de la API principal
useCurrentFrame()- el fotograma actual (el reloj). Todo se anima como una función de esto.useVideoConfig()-{ fps, width, height, durationInFrames }.interpolate(frame, [inFrames], [outValues], opts)- mapeo lineal. Siempre pasa{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }para entradas/salidas para que los valores no se salgan de su rango.spring({ frame, fps, config })- 0→1 basado en física (sobrepasa ligeramente). Úsalo para cualquier cosa que deba sentirse viva (pops, deslizamientos, escalados).AbsoluteFill- un div con posición absoluta de fotograma completo; la capa base de cada escena.Sequence- desplaza y limita temporalmente a los hijos (el programador de escenas).Series/Series.Sequence- escenas consecutivas sin calcular los desplazamientosfrommanualmente.Img,staticFile- ver Paso 6.
Paso 4 - spring() vs interpolate()
Usa spring() cuando… | Usa interpolate() cuando… |
|---|---|
| El movimiento debe sentirse físico - pops, rebotes, escalados, deslizamientos | Necesitas control preciso y lineal entre fotogramas exactos |
| Quieres un asentamiento natural/sobrepaso | Fundidos (opacidad 0→1), cambios de color, barras de progreso, cualquier cosa sincronizada al fotograma |
| Entradas de tarjetas, logos, insignias | Fundidos y salidas donde el sobrepaso se vería mal |
Patrón - impulsa una transformación con un spring, una opacidad con interpolate:
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const pop = spring({ frame, fps, config: { damping: 200 } }); // 0 → 1, vivaz
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const scale = interpolate(pop, [0, 1], [0.8, 1]);
damping: 200 es la configuración de trabajo: un asentamiento rápido con un sobrepaso apenas perceptible. Un damping más bajo (~10-20) rebota visiblemente; úsalo solo cuando el rebote sea el objetivo.
Paso 5 - Estructura estándar de demo de producto
Codifica esta forma a menos que el resumen diga lo contrario: intro de logo/marca → N llamados de función → CTA de outro. Hazlo basado en datos (Paso 6) para que las variantes sean gratuitas.
// 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>
);
};
Regístralo en src/Root.tsx, calculando durationInFrames a partir de los mismos números y proporcionando 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: "Rápido", body: "Entrega en segundos, no en sprints." },
{ title: "Simple", body: "Un prompt, un video." },
{ title: "Tuyo", body: "Tu marca, tu texto." },
],
cta: "Empieza gratis →",
}}
/>
Para vertical/redes sociales, establece width={1080} height={1920}; para cuadrado, 1080×1080. Mantén los componentes idénticos - solo cambia el lienzo.
Paso 6 - Props basados en datos y activos estáticos
La composición anterior es totalmente basada en datos: cada palabra y color es una prop. Ese es el objetivo: las variantes (nuevo texto, nueva paleta, texto localizado) no necesitan ninguna re-solicitud; solo pasa diferentes props (remotion-render lo hace desde un archivo JSON para lotes).
Los activos estáticos (logos, capturas de pantalla, audio) viven en public/assets/ y se referencian con staticFile() - nunca un import o una ruta cruda:
import { Img, staticFile } from "remotion";
<Img src={staticFile("assets/logo.png")} />
Paso 7 - Salida
Guarda el archivo terminado en src/compositions/<VideoName>.tsx, regístralo en src/Root.tsx, y dile al usuario que lo previsualice con npx remotion studio (http://localhost:3000). Luego entrega: "Examínalo en Studio. Dime qué cambiar - demasiado rápido, color incorrecto, texto diferente - y lo editaré y re-renderizaré (remotion-render)."
NO HAGAS
- NO animes con transiciones/animaciones CSS o
setTimeout/setInterval. Remotion renderiza fotogramas de forma independiente y en paralelo; cualquier cosa que no derive deuseCurrentFrame()es invisible o inconsistente en el render final, aunque se vea bien en la previsualización del navegador. - NO uses
Math.random()para efectos dispersos/de partículas - cada worker de renderizado obtiene valores diferentes y los fotogramas dejan de coincidir. Usarandom(seed)de Remotion para pseudo-aleatoriedad determinista. - NO obtengas datos ni esperes trabajo asíncrono sin
delayRender()/continueRender()- el renderizador captura el fotograma antes de que lleguen los datos. - NO codifiques fps, width o height dentro de los componentes de escena - léelos de
useVideoConfig()para que cambiar 30↔60fps o horizontal↔vertical no rompa cada temporización y diseño. - NO omitas
extrapolateLeft/Right: "clamp"en interpolaciones de entrada/salida - la opacidad sube más allá de 1 y los elementos se desplazan fuera del lienzo después de que su rango termina. - NO inventes titulares, afirmaciones de funciones o CTAs - usa el texto real del usuario, y pregúntalo si falta.
Barra de calidad
- Cada valor animado es una función pura de
useCurrentFrame()(más props) - rebobinar hacia atrás en Studio se ve idéntico a avanzar. - El presupuesto de fotogramas en el plan suma exactamente a
durationInFramesenRoot.tsx, y ningún tiempo de texto dura menos de su mínimo legible. - Todo el texto y los colores llegan a través de props tipadas con
defaultPropsproporcionados - sin literales enterrados en componentes de escena. - Los activos cargan a través de
staticFile(); el archivo se registra limpiamente y se previsualiza en Studio sin errores de consola.