HyperFrames Core
El framework de salida es Remotion. Una composición es un árbol de componentes React registrado con <Composition>; el fotograma actual es el único reloj; la reproducción de medios pertenece a los componentes <Video> / <Audio> de Remotion, no al DOM.
Este skill es el contrato técnico: cómo construir un proyecto. El cuerpo a continuación es la guía de construcción; los detalles por tema viven en references/, léelos bajo demanda. Otras preocupaciones viven en los skills de dominio hermanos: hyperframes-animation, hyperframes-creative, media-use, hyperframes-cli, hyperframes-registry. El mapa de capacidades en /hyperframes dice lo que cubre cada uno.
Nota de migración. Este contrato reemplazó un contrato HTML más
data-*más línea de tiempo GSAP pausada. Si encuentras un proyecto cuyoindex.htmlllevadata-composition-id/data-start/data-duration, o un archivo de referencia que describewindow.__timelines, estás viendo el contrato antiguo. Leereferences/legacy-html-contract.mdantes de modificarlo.
Referencias
| Archivo | Léelo para… |
|---|---|
references/minimal-composition.md | empezar desde el esqueleto de proyecto renderizable más pequeño |
references/project-structure.md | diseñar src/, dividir escenas en componentes, decidir monolítico vs modular |
references/timing.md | convertir segundos a fotogramas, colocar un <Sequence>, usar <Series> / <TransitionSeries>, temporizar una escena relativa a otra |
references/layers-and-order.md | apilar capas, controlar el orden z, superponer dos escenas en el mismo intervalo |
references/props-and-schemas.md | declarar props, escribir el esquema Zod, conectar defaultProps y calculateMetadata |
references/media.md | colocar <Video> / <OffthreadVideo> / <Audio> / <Img>, recortar, establecer volumen, usar staticFile |
references/determinism-rules.md | mantener el render reproducible y buscable; las prohibiciones; carga asíncrona de assets mediante delayRender |
references/editing-recipes.md | recetas de corte / recorte / reordenamiento / retemporización / congelación / cámara / máscara / encadenado / crossfade / audio y sus límites |
references/legacy-html-contract.md | reconocer y migrar un proyecto que aún usa el antiguo contrato HTML + data-* |
references/tailwind.md | trabajar en un proyecto con Tailwind v4 |
references/storyboard-format.md | redactar un plan STORYBOARD.md (+ el manifiesto analizado) |
references/review-loop.md | ejecutar las pasadas de revisión planificar → bosquejar → construir en un tablero en vivo |
references/production-loop.md | llevar un plan aprobado a un video entregado |
references/brief-contract.md | las reglas base del brief: derivación de modo, registro de campos compartidos, invariantes de preguntas |
references/brief-format.md | redactar BRIEF.md |
references/script-format.md | redactar el opcional SCRIPT.md de narración fijada |
references/subagent-dispatch.md | mapear verbos de despacho de subagentes a tu harness |
references/frame-worker-core.md | el contrato compartido del rol frame-worker |
Para detalles de animación (configuraciones de resorte, easing, adaptadores de tiempo de ejecución) ve a hyperframes-animation.
Construyendo una composición
La forma de un proyecto
package.json # remotion, react, react-dom; scripts para studio/render
remotion.config.ts # configuración de render (codec, formato de imagen, anulaciones de Tailwind/webpack)
tsconfig.json
public/ # todo contra lo que resuelve staticFile()
src/
index.ts # registerRoot(Root)
Root.tsx # cada <Composition> vive aquí
<Nombre>/
index.tsx # el componente principal de la composición
scenes/*.tsx # un componente por escena
schema.ts # esquema Zod + tipo de props inferido
src/index.ts es el punto de entrada y hace exactamente una cosa:
import { registerRoot } from "remotion";
import { Root } from "./Root";
registerRoot(Root);
Registrar la composición
Cada cosa renderizable es un <Composition> en Root.tsx. El id es lo que el CLI renderiza por nombre.
import { Composition } from "remotion";
import { LaunchVideo } from "./LaunchVideo";
import { launchSchema } from "./LaunchVideo/schema";
const FPS = 30;
export const Root: React.FC = () => (
<>
<Composition
id="launch"
component={LaunchVideo}
durationInFrames={45 * FPS}
fps={FPS}
width={1920}
height={1080}
schema={launchSchema}
defaultProps={{ productName: "Acme", accent: "#5B8CFF" }}
/>
</>
);
durationInFrames es la longitud del render. Son fotogramas, no segundos, y equivocarse es el error de autoría más común. Derívalo de una constante FPS con nombre en lugar de escribir 1350.
El fotograma es el único reloj
Cada valor animado es una función pura de useCurrentFrame(). Este es el contrato que hace que un render sea buscable: el renderizador salta al fotograma 812 en frío y debe obtener los mismos píxeles que obtendría reproduciendo de principio a fin.
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from "remotion";
export const Title: React.FC<{ text: string }> = ({ text }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({ frame, fps, config: { damping: 18 } });
const y = interpolate(enter, [0, 1], [40, 0]);
return (
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
<h1 style={{ opacity: enter, transform: `translateY(${y}px)` }}>{text}</h1>
</AbsoluteFill>
);
};
Siempre sujeta un interpolate que mapea un rango de fotogramas, o el valor seguirá extrapolando más allá del rango y un fundido de entrada se convertirá en opacidad 4:
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
La temporización es <Sequence>
<Sequence> desplaza el tiempo de sus hijos: dentro de él, useCurrentFrame() devuelve 0 al inicio de la propia secuencia. Ese reajuste local es el punto clave, porque un componente de escena nunca necesita saber dónde se encuentra en la línea de tiempo maestra.
<AbsoluteFill>
<Sequence durationInFrames={90}>
<Hook />
</Sequence>
<Sequence from={90} durationInFrames={150}>
<Feature />
</Sequence>
<Sequence from={240} durationInFrames={120}>
<Close />
</Sequence>
</AbsoluteFill>
Para una serie de escenas consecutivas, <Series> calcula los desplazamientos para que un cambio de duración no genere una cascada de correcciones manuales en cada from posterior:
<Series>
<Series.Sequence durationInFrames={90}><Hook /></Series.Sequence>
<Series.Sequence durationInFrames={150}><Feature /></Series.Sequence>
<Series.Sequence durationInFrames={120}><Close /></Series.Sequence>
</Series>
<Series.Sequence offset={-15}> adelanta una escena para superponerla a su predecesora, que es como se consigue un intervalo de crossfade sin aritmética. Detalle completo, más <TransitionSeries> para transiciones basadas en presentación, en references/timing.md.
Las capas reemplazan a las pistas
No hay índice de pista. El apilamiento es el orden del DOM dentro de un <AbsoluteFill>: los hermanos posteriores pintan encima. Una pista de voz en off, una base musical y tres escenas visuales son cinco hermanos.
<AbsoluteFill>
<Audio src={staticFile("musica.mp3")} volume={0.18} />
<Audio src={staticFile("vo.mp3")} />
<Sequence durationInFrames={90}><Hook /></Sequence>
<Sequence from={90}><Feature /></Sequence>
<Overlay /> {/* pinta encima de todo lo anterior */}
</AbsoluteFill>
<Sequence layout="none"> elimina el envoltorio implícito <AbsoluteFill>. Úsalo cuando el hijo deba participar en el diseño flex o grid del padre en lugar de estar posicionado absolutamente.
Los medios pertenecen a Remotion
Nunca uses una etiqueta <video>, <audio> o <img> nativa. Los componentes de Remotion son lo que permite al renderizador buscar y extraer fotogramas de manera determinista.
| Necesidad | Usa |
|---|---|
| Video en el render | <OffthreadVideo src> (preferido) |
| Video que necesita control a nivel DOM | <Video src> |
| Audio | <Audio src volume trimBefore trimAfter> |
| Imagen estática | <Img src> |
| Página embebida | <IFrame src> |
Cualquier archivo en public/ | staticFile("dir/nombre.ext") |
<OffthreadVideo> extrae fotogramas con FFmpeg fuera del navegador, lo que es más rápido y preciso para el render; recurre a <Video> solo cuando necesitas el elemento real (por ejemplo, para conducir algo desde su currentTime). staticFile() es obligatorio para assets locales. Una cadena desnuda "/musica.mp3" funciona en Studio y luego falla en un render Lambda. Recorte, rampas de volumen y velocidad de reproducción están en references/media.md.
Props y esquema
Las entradas de la composición son props de React, tipadas por un esquema Zod para que Studio renderice controles editables y una carga útil --props incorrecta falle estrepitosamente en lugar de renderizar mal.
import { z } from "zod";
import { zColor } from "@remotion/zod-types";
export const launchSchema = z.object({
productName: z.string(),
accent: zColor(),
features: z.array(z.object({ title: z.string(), body: z.string() })),
});
export type LaunchProps = z.infer<typeof launchSchema>;
Cuando la duración depende de las props (una lista de características de longitud desconocida, un archivo de audio de longitud desconocida), calcúlala en calculateMetadata en lugar de codificar una estimación. Ve a references/props-and-schemas.md.
Reglas no negociables
Errores silenciosos que una verificación de tipos no detectará. Justificación completa en references/determinism-rules.md.
- Sin reloj de pared. Sin
Date.now(), sinnew Date(), sinperformance.now(). El tiempo viene deuseCurrentFrame(). - Sin aleatoriedad sin semilla.
Math.random()da un valor diferente por fotograma, lo que produce parpadeo. Usarandom(seed)de Remotion: misma semilla, mismo valor, cada fotograma y cada render. - Sin animación basada en estado.
useState/useReducer/useEffectno debe impulsar nada visual. El renderizador monta fotogramas fuera de orden y en workers paralelos; el estado acumulado al reproducir hacia adelante no existe. Deriva delframeen su lugar. - Sin animaciones CSS, transiciones ni
requestAnimationFrame. Se ejecutan en el propio reloj del navegador, que el renderizador no avanza. Cada una renderiza como un primer fotograma congelado. - Espera el trabajo asíncrono con
delayRender. Una fetch, una fuente, un diseño medido: llama adelayRender()antes y acontinueRender(handle)después, o el fotograma se captura antes de que el trabajo llegue. - Sujeta tus interpolaciones.
interpolatesin sujeción extrapola fuera del rango de entrada. - Posicionamiento absoluto para cualquier cosa animada. Un elemento transformado dentro del flujo normal arrastra a sus hermanos a medida que se mueve.
Editando composiciones existentes
- Lee los archivos primero. Conserva la temporización, props, ids y rutas de assets no relacionados.
- Coincide con el
iddel<Composition>existente. El CLI, cualquier script de render y CI abordan las composiciones por él. Renombrar uno es un cambio radical. - Agregar una escena: prefiere
<Series>para que los desplazamientos se recalculen. En una lista de<Sequence>con desplazamiento manual, corrige cadafromposterior. - Cambiar una duración: verifica también
durationInFramesen el<Composition>. Una escena que se extiende más allá de la longitud de la composición se trunca silenciosamente. - Agregar un componente: mantenlo como una función pura de props y
frame.
Validación
El detalle de los comandos está en hyperframes-cli.
-
npx tsc --noEmit, para que el esquema y los tipos de props realmente coincidan -
npx remotion still <id> out/probe.png --frame <n>en algunos puntos intermedios de escena, y mira cada uno -
npx remotion studiopara revisión, para que el usuario pueda desplazarse y ajustar props por sí mismo -
npx remotion render <id>solo después de que el usuario lo apruebe