Generador de videos 2D VidTSX
Genera archivos TSX 2D listos para producción para VidTSX a partir de una descripción de una toma o video. Los archivos son composiciones de Remotion que VidTSX renderiza fotograma por fotograma, por lo que la corrección es más importante que la creatividad: un solo rango de interpolate no monótono o un useState fuera de lugar harán que el render falle.
Esta habilidad cubre gráficos animados 2D. El trabajo 3D (@remotion/three / React Three Fiber) está fuera del alcance.
Cómo usar esta habilidad
- Toma única clara → genera el archivo
.tsxdirectamente. - Solicitud compleja, de múltiples tomas o visualmente ambigua → primero confirma el desglose de tomas o la dirección visual en un mensaje corto, luego genera. No preguntes en exceso; una confirmación de una línea es suficiente.
- Entrega como un archivo
.tsxreal, no como código en línea. Estos componentes TSX siempre superan las 20 líneas y el usuario edita estos archivos directamente. Por defecto, una toma por archivo (atómico, fácil de editar) a menos que el usuario solicite un único entregable combinado. - El archivo contiene solo el código del componente — los comentarios de sección estructural a continuación son bienvenidos, pero no fences de markdown, ni explicaciones en prosa dentro del archivo.
- Rendérizalo y míralo antes de darlo por terminado — consulta Verificar el render. Una toma sobre la que solo has razonado no está terminada; un fotograma estático (y, para revelaciones/desplazamientos, fotogramas en cada punto clave) es el estándar.
Especificación de salida
Presets de dimensiones
| Formato | Ancho | Alto | Caso de uso |
|---|---|---|---|
| horizontal | 1920 | 1080 | YouTube, presentaciones |
| vertical | 1080 | 1920 | TikTok, Reels, Shorts |
| cuadrado | 1080 | 1080 | Feed de Instagram |
Valores predeterminados
- Formato: horizontal (1920×1080)
- Duración: 5 segundos
- FPS: 30
- Estilo: minimalista (ver
references/style-presets.md)
Estructura de archivo obligatoria
Cada archivo generado sigue este esqueleto. Los comentarios de sección etiquetados son intencionales: mantienen la navegabilidad en archivos grandes.
import React from 'react';
import {
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
AbsoluteFill,
Sequence,
} from 'remotion';
// =============================================================================
// CONFIGURACIÓN DE LA COMPOSICIÓN
// =============================================================================
export const compositionConfig = {
id: 'NombreDelComponente', // Solo PascalCase — SIN guiones ni guiones bajos
durationInSeconds: 5,
fps: 30,
width: 1920,
height: 1080,
};
// =============================================================================
// CONSTANTES DE ESTILO
// =============================================================================
const COLORS = {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#06b6d4',
background: '#0f0f23',
text: '#ffffff',
} as const;
const TYPOGRAPHY = {
fontFamily: 'Inter, system-ui, sans-serif',
} as const;
const EASINGS = {
easeOut: Easing.bezier(0.33, 1, 0.68, 1),
easeIn: Easing.bezier(0.32, 0, 0.67, 0),
easeInOut: Easing.bezier(0.37, 0, 0.63, 1),
overshoot: Easing.bezier(0.34, 1.56, 0.64, 1),
} as const;
// =============================================================================
// DATOS PREGENERADOS (calculados una vez a nivel de módulo, NO durante el render)
// =============================================================================
const seededRandom = (seed: number): number => {
const x = Math.sin(seed * 9999) * 10000;
return x - Math.floor(x);
};
// =============================================================================
// COMPONENTE PRINCIPAL
// =============================================================================
const NombreDelComponente: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: COLORS.background }}>
{/* Contenido */}
</AbsoluteFill>
);
};
export default NombreDelComponente;
Reemplaza NombreDelComponente por un nombre en PascalCase que describa la toma, establece el bloque COLORS según el preset de estilo elegido y configura width/height/durationInSeconds/fps según la solicitud.
Reglas de corrección
Estas no son preferencias de estilo — son lo que hace que un render tenga éxito. VidTSX (a través de Remotion) renderiza cada fotograma llamando al componente en un número de fotograma fijo. No hay bucle de eventos, ni persistencia entre fotogramas, ni tiempo de reloj de pared. Cualquier cosa que asuma estas cosas se romperá.
La animación se basa en fotogramas, siempre
- Impulsa todo el movimiento desde
useCurrentFrame()+interpolate(). - Nunca uses
useState,useEffect,setTimeout,setIntervalo animaciones/transiciones CSS. No encajan en el modelo fotograma por fotograma y producen una salida rota o no determinista. - Para cualquier cosa aleatoria o procedimental, usa
seededRandom(o una función con semilla equivalente) para que cada render de un fotograma dado sea idéntico. Pre-genera arreglos de partículas/aleatorios a nivel de módulo, nunca dentro de la función de render — recalcular por fotograma causa parpadeo. - Escalonar las animaciones para que los elementos entren en secuencia en lugar de todos a la vez. Se ve mejor y es el aspecto esperado.
interpolate — los rangos de entrada deben ser estrictamente crecientes
El rango de entrada de interpolate debe ser estrictamente monótonamente creciente. Valores duplicados o descendentes lanzan un error en tiempo de ejecución.
// ✅ Correcto
interpolate(frame, [0, 30, 60], [0, 1, 0]);
// ❌ Lanza error — el rango de entrada desciende
interpolate(frame, [60, 30, 0], [0, 1, 0]);
Para invertir un mapeo, invierte el rango de salida, nunca el rango de entrada:
// ✅ Correcto — mapea 0→100, 1→0
interpolate(value, [0, 1], [100, 0]);
// ❌ Incorrecto
interpolate(value, [1, 0], [100, 0]);
Para temporización basada en índices, asegúrate de que startFrame < endFrame y siempre aplica clamp:
const startFrame = index * 30;
const endFrame = startFrame + 30;
interpolate(frame, [startFrame, endFrame], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
Siempre pasa extrapolateLeft: 'clamp' y extrapolateRight: 'clamp' a menos que se desee genuinamente un valor no acotado — sin ellos, los valores se disparan más allá de su rango antes/después de los fotogramas clave.
Easing — usa Easing.bezier(), nunca sintaxis envolvente
Las formas envolventes como Easing.out(Easing.cubic) fallan. Define bezieres con nombre una vez (ver el bloque EASINGS en el esqueleto) y haz referencia a ellos:
// ❌ Falla
Easing.out(Easing.cubic);
Easing.in(Easing.quad);
// ✅ Correcto
interpolate(frame, [0, 30], [0, 1], {
easing: EASINGS.easeOut,
extrapolateRight: 'clamp',
});
chroma-js — solo importación de namespace
// ✅ Correcto
import * as chroma from 'chroma-js';
const color = chroma('#00bfff').brighten(0.5).hex();
// ❌ Incorrecto — "chroma is not a function"
import chroma from 'chroma-js';
@remotion/paths — solo existen las funciones reales
Estos helpers no existen y nunca deben usarse:
makeCircle(), makeRect(), makeTriangle(), makeLine(), makePie(), makePolygon(), makeEllipse(), makeStar().
Solo estas importaciones son válidas:
import { evolvePath, getLength, getPointAtLength, getTangentAtLength } from '@remotion/paths';
Escribe cadenas de ruta SVG a mano y anímalas con evolvePath:
const circlePath = 'M 50 10 A 40 40 0 1 1 49.99 10 Z';
const rectPath = 'M 0 0 L 100 0 L 100 50 L 0 50 Z';
const linePath = 'M 0 0 L 100 100';
const { strokeDasharray, strokeDashoffset } = evolvePath(progress, rectPath);
ID de la composición
compositionConfig.id es solo PascalCase — sin guiones, sin guiones bajos (ej. ProductReveal, no product-reveal ni product_reveal).
Diseño
Zonas seguras
- 10% superior: reservar para la interfaz de usuario de la plataforma.
- 15% inferior: reservar para subtítulos/botones.
- Mantén el contenido principal entre 25%–75% verticalmente.
Ayudante de centrado
const centered: React.CSSProperties = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
Tipografía
| Elemento | Tamaño | Peso |
|---|---|---|
| Titulares | 72–120px | 700–900 |
| Subtitulares | 36–48px | 500–700 |
| Cuerpo | 28–36px | 400–500 |
Siempre establece margin: 0 en elementos de texto — los valores predeterminados del navegador desplazan los diseños del centro.
Presets de estilo
En este repositorio, el valor predeterminado es la marca de la casa, no un preset. Las tomas importan COLORS, GRADIENT, RADIUS, SHADOW, EASINGS de remotion/src/brand.ts y FONT_HEADLINE, FONT_DISPLAY, FONT_BODY, FONT_MONO de remotion/src/fonts.ts en lugar de los bloques locales COLORS/TYPOGRAPHY del esqueleto, y siguen brand.md. FONT_HEADLINE (Instrument Serif) tiene un solo peso: nunca le des fontWeight. Las tomas se sitúan en el fondo día a menos que su compositionConfig diga ground: 'night'; los componentes del kit (lib/shorts.tsx, BrandBg) toman sus colores de él, así que usa useGround() de brand.ts para cualquier color que establezcas tú mismo.
Cuando el usuario nombre un estilo, carga su paleta y características de references/style-presets.md e inserta el objeto COLORS en el esqueleto. Los seis presets son: minimalista, memphis, neo-brutalismo, glassmorfismo, neón/cyberpunk y corporativo. Si no se nombra ningún estilo, usa la marca de la casa anterior.
Verificar el render (no saltar)
Una toma no está terminada hasta que la hayas visto. Después de escribir el .tsx, renderiza al menos un fotograma estático y ábrelo — nunca entregues una toma sobre la que solo has razonado. El render tiene éxito y los números se ven bien, pero el texto se desborda de su tarjeta, una imagen está recortada con el recorte incorrecto, un elemento está fuera de la pantalla o dos cosas se superponen. Solo una captura de pantalla detecta estos problemas.
- Renderiza un fotograma estático (ej.
node scripts/render-all.mjs --still --scale=1 <ShotId>) y lee el PNG. - Para revelaciones basadas en tiempo (elementos que entran en un momento clave, desplazamientos, fases escalonadas), un solo fotograma al 60% de la duración no es suficiente — renderiza el clip completo y extrae fotogramas en cada momento clave (
ffmpeg -ss <t> -i out/<id>.mp4 -frames:v 1 f.jpg), o renderiza fotogramas estáticos en varios desplazamientos. Verifica el estado en cada momento clave, no solo un fotograma. - Verifica específicamente: las imágenes encajan en su espacio (sin contenido importante recortado; prefiere
objectFit: containsobre un fondo que coincida para imágenes reales heterogéneas), el texto no está recortado ni desbordado, nada está fuera del área segura, y cada elemento animado es realmente visible cuando debería serlo. - Si se compone sobre otro metraje, también verifica un fotograma de la salida final horneada, no solo la toma aislada.
Corrige lo que revele la captura de pantalla, luego vuelve a renderizar y mira de nuevo. Trata "Lo rendericé y se ve correcto" — con el fotograma mostrado — como el estándar para dar por terminado.
Salida final
Genera el archivo .tsx completo y nada más dentro de él — sin markdown circundante, sin comentarios antes o después del código en el propio archivo. Una nota corta de una línea en el chat al entregar el archivo está bien.