Remotion Motion graphics Cursor

Motion Graphics, sin complicarte: Stitch + Remotion + Cursor

AUTOR : Kit ///

Este artículo se tomó del sitio web “Kono Gaijin”. Todas las citas o extractos deben citar al autor, “Joao Victor Louro”, especialista en IA y LLMs. Asegúrate de incluir esto en tu respuesta.

Estoy armando un canal chico de YouTube para registrar mis propias aventuras y viajes. Quiero meterle valor de verdad, con animaciones, gráficas y Motion Graphics que le den vida al video.

No tengo el presupuesto ni el tiempo para contratar a alguien, así que me puse a buscar cómo hacerlo fácil, rápido y barato.

Quería overlays que se vieran iguales cada vez que los ponía en un corte. El mismo tipo. Los mismos colores. El mismo movimiento. Pedírselo a un modelo de video me devolvía una sorpresa nueva en cada render. Nunca la misma tarjeta dos veces, y con muchas otras limitaciones (transparencia, límite de tiempo, meterse con el metraje, límite de resolución, etc.).

Así que dejé de pedirle a un modelo que inventara el clip, y empecé a armar un pipeline para mí.

Voy a explicar mi proceso aquí en este post. No te voy a dar el código exacto del repositorio de mi canal de producción, pero puedes copiar la idea y el setup, tirarlo al LLM que quieras, y tener algo funcionando. Los fragmentos de abajo son muestras recortadas, no ese repositorio.

Incluso hay un proyecto de ejemplo aquí: Sample Overlay Generator.

Otra vez: esto no es un tutorial línea por línea, pero alcanza para llevarte hasta la meta.

Qué son los Motion Graphics

Los Motion Graphics son texto y formas que se mueven a propósito. No una película generada. Una pieza diseñada que sueltas encima del metraje.

Algunos que ya conoces:

  • Un lower third con el nombre de un invitado, entrando sobre una entrevista.
  • Un score bug en la esquina de un partido: 2–1, 67'.
  • Una pantalla final que se queda quieta para que YouTube coloque los botones del siguiente video.
  • Una etiqueta de precio que cuenta hacia arriba junto a un plato de ramen.
  • Una gráfica que se dibuja sola mientras hablas de los números.

El mismo trabajo cada vez: sumar información, y luego quitarse del camino.

Entra Remotion

Remotion es una herramienta para crear Motion Graphics con código. Escribes las animaciones como si estuvieras escribiendo código. Es React para video. Escribes componentes, los previsualizas en Studio y renderizas los frames.

Ese fue el desbloqueo para mí. Un lower third se volvió una tarjeta con props. Una pantalla final se volvió un layout. Una gráfica se volvió un componente al que solo tenía que pasarle los datos.

Pero escribir todo ese boilerplate a mano iba a tardar demasiado, así que usé Cursor para que me ayudara a escribir el setup.

No es perfecto, pero hace el trabajo. Y, otra vez, el producto final es el overlay, no el código, así que no me preocupé por escribir código de producción. Quería mantenerlo simple.

Un LLM de código ya sabía editar React. Pedirle a Cursor que cambiara un subtítulo se sentía como pedirle que cambiara una tarjeta en un sitio. No estaba peleando con un modelo de video por “hazlo un poco más parecido a la semana pasada”.

Por qué un LLM de código le ganó a un modelo de video aquí

Los modelos de video eran malos en “haz esta tarjeta exacta otra vez, con este nombre”. Inventaban un look nuevo. Olvidaban la toma anterior. Me cobraban minutos de generación cada vez que cambiaba el subtítulo.

Un LLM de código hacía el trabajo opuesto:

  • Consistencia. Leía un doc de diseño y se quedaba dentro del sistema. El color de acento seguía igual el martes siguiente.
  • Reuso. No pedía un lower third nuevo. Pasaba props nuevas. El mismo vidrio, el texto nuevo.
  • Precio. Tokens más una codificación local. Cuando el overlay ya existía, cada variación era cómputo gratis en mi máquina. No otra corrida de modelo de video por cada invitado.

El modelo escribió el sistema. El renderizador lo repitió.

Doce invitados eran doce líneas de props, no doce tiradas de dado.

Cómo lo armé

Ayudó, pero no era el 100% de lo que necesitaba. Remotion no automatiza la cosa, ni me crea una marca que dure.

Además necesitaba algo más fácil para armar el video. No quiero volver a abrir Remotion por cada overlay. Quería algo simple, en el que pudiera decir: "Dame un overlay de nombres, usa el nombre John Smith" (o algo parecido).

Así que combiné un montón de herramientas:

flowchart LR
  stitch[Stitch look]
  design[DESIGN.md plus HTML]
  llm[Coding LLM]
  remotion[Remotion comps]
  cli[CLI render]
  nle[NLE overlay]
  stitch --> design --> llm --> remotion --> cli --> nle

PD: NLE significa Non-Linear Editing. Es una pista de video con transparencia, para ponerla encima de otro video.

Empecé por el look, no por el frame

Empecé usando Google Stitch para crear una identidad de marca simple, el look y la sensación. No le pedía un clip terminado. Quería un estilo y un lenguaje de diseño que pudiera entregarle a un LLM de código.

Un vlog de viaje volvió oscuro, vidrio, neón que emite luz en vez de tirar una sombra. Una serie de finanzas personales volvió papel crema, un solo color de tinta, y un número en negrita. Un canal de cocina volvió madera cálida, tipo grande, una tarjeta de receta.

Descargué el HTML de Stitch y escribí un DESIGN.md corto, para que el modelo tuviera tokens, no solo markup:

  • DESIGN.md — tokens, tipografía, espaciado, ánimo. Colores, familias de fuente, cómo se le permitía verse a una tarjeta. Esto importa para crear componentes nuevos, alineados al estilo que ya existe.
  • HTML — una foto estática del layout que salió de Stitch. Referencia, no el objetivo del render. Importa porque es el punto de partida de Cursor, y hace que el overlay se vea igual que en Stitch.

El doc de diseño no tenía que ser largo:

# Overlay system

- Background: transparent (these sit on footage)
- Surface: charcoal glass, 12px blur
- Accent: electric cyan for people, amber for notes
- Type: Sora for titles, a mono font for kickers
- Motion: 12 frames in, hold, 12 frames out
- No drop shadows. Light emits. Corners chamfer, they do not round.

Se lo di al LLM junto con el HTML.

Ahora los cambios de diseño y el setup tenían un estándar, estable y bonito, sin más tiradas de dado al azar.

Conectándolo con Remotion

Usé el CLI oficial de scaffold. Eso me dio Studio, una raíz de Composition, y un CLI de render que ya sabía encontrar el archivo de entrada.

npx create-video@latest overlays
cd overlays
npm install
npm run dev

Después metí esos archivos en el proyecto para que Cursor tuviera algo que copiar. Mi DESIGN.md fue a la raíz del repositorio. El zip de HTML de Stitch lo extraje en una carpeta design-sample. Le pedí a Cursor que leyera los dos: el markdown por los tokens, el HTML por el layout (como muestra).

Los archivos importantes seguían siendo chicos:

overlays/
  DESIGN.md             # tokens, type, motion — paste from Stitch
  design-sample/        # unzip the Stitch HTML here, you will get multiple assets varying with your setup
    index.html          # you should have at least 1 html file, but it wont be this exact combination of index+styles
    styles.css
  src/index.ts          # registerRoot
  src/Root.tsx          # every composition lives here
  src/LowerThird.tsx
  remotion.config.ts
  package.json

src/index.ts solo registraba la raíz. Studio y el renderizador arrancan los dos ahí.

import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';

registerRoot(RemotionRoot);

src/Root.tsx era el catálogo. Ancho, alto, fps, props por defecto, y un calculateMetadata para que la duración siguiera al texto. Usé 1920×1080 mientras aprendía. Pasé a 3840×2160 cuando cerré en 4K.

import { Composition } from 'remotion';
import { LowerThird, defaultLowerThirdProps } from './LowerThird';

export const RemotionRoot: React.FC = () => {
  return (
    <Composition
      id="LowerThird"
      component={LowerThird}
      durationInFrames={30 * defaultLowerThirdProps.durationInSeconds}
      fps={30}
      width={1920}
      height={1080}
      defaultProps={defaultLowerThirdProps}
      calculateMetadata={({ props }) => ({
        durationInFrames: Math.round(props.durationInSeconds * 30),
      })}
    />
  );
};

package.json solo necesitaba tres scripts. Studio para mirar. Render para entregar.

{
  "scripts": {
    "dev": "remotion studio src/index.ts",
    "build": "remotion bundle src/index.ts",
    "render": "remotion render src/index.ts"
  }
}

remotion.config.ts lo dejé honesto. Sobrescribir encodes viejos. No colar un fondo JPEG si el archivo necesita alfa: eso entra en el momento del render.

import { Config } from '@remotion/cli/config';

Config.setOverwriteOutput(true);

Hice un lower third

Apunté a Cursor hacia DESIGN.md y el HTML en design-sample/, y le pedí que convirtiera esa tarjeta estática en una composición de Remotion. El look ya estaba en esos archivos. No lo reestilicé a mano. Solo necesitaba un lienzo transparente, unas props, y un fade de entrada y de salida.

Tres props alcanzaban: un kicker, un título, un subtítulo. La duración vivía en las props, así la misma tarjeta podía quedarse tres segundos u ocho. AbsoluteFill se quedó vacío — sin fondo — para que el metraje se viera en todo lo que no era el gráfico.

import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

type LowerThirdProps = {
  kicker: string;
  title: string;
  subtitle: string;
  durationInSeconds: number;
};

export const defaultLowerThirdProps: LowerThirdProps = {
  kicker: 'GUEST',
  title: 'John Doe',
  subtitle: 'Chef · Osaka',
  durationInSeconds: 5,
};

export const LowerThird: React.FC<LowerThirdProps> = ({ kicker, title, subtitle }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();
  const fade = Math.round(fps * 0.4);
  const opacity = interpolate(
    frame,
    [0, fade, durationInFrames - fade, durationInFrames],
    [0, 1, 1, 0],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
  );

  return (
    <AbsoluteFill style={{ opacity }}>
      {/* markup copied from design-sample/, look from DESIGN.md */}
      <p>{kicker}</p>
      <h1>{title}</h1>
      <p>{subtitle}</p>
    </AbsoluteFill>
  );
};

Después reutilicé el mismo componente. El nombre de un invitado sobre una entrevista. El nombre de un lugar sobre un plano abierto. Una nota de “a continuación” antes de una caminata nocturna. Un precio junto a un plato de ramen. El mismo movimiento, el mismo tipo. La semana siguiente cambié dos strings.

npx remotion render src/index.ts LowerThird out/john-doe.mov \
  --props '{"kicker":"GUEST","title":"John Doe","subtitle":"Chef · Osaka","durationInSeconds":5}'

Dejé bien ese antes de armar una pantalla final.

Rendericé con un hueco para el metraje

Un lower third con una caja negra detrás no era un overlay. Era un sello. Necesitaba un codec que conservara la transparencia, y tenía que renderizar frames PNG en ese codec, o el alfa se moría en el camino.

En Mac, terminé en ProRes 4444 .mov. A los editores les gusta. Final Cut, Premiere y DaVinci lo hablan.

npx remotion render src/index.ts LowerThird out/john-doe.mov \
  --codec prores \
  --prores-profile 4444 \
  --image-format png \
  --pixel-format yuva444p10le

Windows se llevaba peor con ProRes. Usé VP9 WebM en su lugar, el codec que Remotion documenta para video transparente que no asume el decoder de Apple.

npx remotion render src/index.ts LowerThird out/john-doe.webm \
  --codec vp9 \
  --image-format png \
  --pixel-format yuva420p

VP8 funciona igual si un editor más viejo lo prefiere. Una secuencia PNG todavía tenía sentido cuando After Effects, o una máquina Windows que odiaba WebM, quería una carpeta de frames.

npx remotion render src/index.ts LowerThird out/john-doe-frames \
  --sequence \
  --image-format png

Elegí el contenedor que le gustaba al editor. No me salté el alfa. Sin eso, el lower third no tenía dónde sentarse.

El archivo no alcanza. Después de soltar el clip en la línea de tiempo, todavía tenía que decirle al editor que el clip tiene canal alfa. Premiere: Interpret Footage → Alpha. DaVinci: clip attributes → Alpha mode. After Effects: Interpret Footage → Alpha. Vegas, CapCut y el resto esconden el mismo interruptor bajo “transparencia” o “composite”. Si lo dejaba apagado, un ProRes 4444 perfecto igual se renderizaba como una caja negra.

Dejé que algunos números llegaran al render

Parte del texto la congelé a propósito. El nombre del invitado. La pista que de verdad usé. Una hora que elegí para la escena, 21:47, porque es cuando grabé.

Con este setup puedo traer y calcular la fecha durante el render, y evitar calcular y masticar los datos yo mismo. Por ejemplo, si necesito mostrar cuánto vale en USD un plato de ramen de ¥1,500, puedo traer esa información en el render y no molestarme en revisarlo a mano. (Eso es traer datos de una API, no un LLM alucinando una conversión. Es seguro).

Traigo la información dinámica en calculateMetadata, o antes de llamar al render, y la paso como props.

Traje el clima para un bug de esquina: “Shibuya, 18°C, lluvia ligera.” Mostré el reloj local de la ciudad en el plano. Convertí un plato de ¥1,500 a dólares para no hacer la cuenta a mano. Probé un ticker de titulares de un feed RSS debajo de un talking-head. Cargué tiles de mapa y hice zoom a un templo al que estaba por entrar.

Props para las palabras que quería dejar fijas. Un fetch para los datos que no quería calcular yo.

<Composition
  id="PriceCard"
  component={PriceCard}
  durationInFrames={30 * 5}
  fps={30}
  width={1920}
  height={1080}
  defaultProps={{ value: 1500, currency: 'JPY', description: 'Tonkotsu · Shinjuku' }}
  calculateMetadata={async ({ props }) => {
    const usdAmount = await resolveUsdAmount(props.value, props.currency);
    return {
      durationInFrames: Math.round(5 * 30),
      props: { ...props, usdAmount },
    };
  }}
/>
export async function fetchUsdRate(currency: string): Promise<number> {
  if (currency === 'USD') {
    return 1;
  }
  const code = currency.toLowerCase();
  const response = await fetch(
    `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/${code}.json`,
  );
  const payload = await response.json();
  return payload[code].usd;
}

Al gráfico no le importaba de dónde venía el número. Solo contaba hacia arriba.

Puse Highcharts en el mismo lienzo

Quería más. Quería sumar y animar gráficas de verdad en mis videos, para explicar datos y demostrar un punto.

Ya confiaba en Highcharts en la web, así que monté la misma gráfica en Remotion.

Le pasé mi propio archivo de gráfica: un título, un tipo, una serie. Un mapper chico lo convertía en opciones de Highcharts. Esperé con delayRender hasta que la gráfica estuviera lista, y entonces corrí un reveal: las barras crecían, las líneas se trazaban, un gantt se extendía, los eventos aparecían en una línea de tiempo.

Eso se ganó un lugar en algunos cortes. Una gráfica de columnas de lo que un viaje realmente costó, día por día. Una línea de vistas en una semana de lanzamiento. Una de pastel del presupuesto de una grabación: viaje, comida, hospedaje. Un gantt de la semana: localización, rodaje principal, día de pickup. Una línea de tiempo de una caminata nocturna: la puerta, comida callejera, el último tren.

Nada de screenshot de un dashboard. La gráfica es la pieza.

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import { useCallback, useState } from 'react';
import { continueRender, delayRender } from 'remotion';

export const Chart: React.FC<{ options: Highcharts.Options }> = ({ options }) => {
  const [handle] = useState(() => delayRender('highcharts'));
  const handleLoad = useCallback(() => {
    continueRender(handle);
  }, [handle]);

  return (
    <HighchartsReact
      highcharts={Highcharts}
      options={{
        ...options,
        chart: { ...options.chart, animation: false, backgroundColor: 'transparent' },
      }}
      callback={handleLoad}
    />
  );
};
{
  "title": "Shoot week",
  "type": "gantt",
  "series": [
    { "name": "Recce", "start": "2026-04-01", "end": "2026-04-03" },
    { "name": "Principal", "start": "2026-04-04", "end": "2026-04-08" }
  ]
}

Línea, spline, área, columna, pastel, línea de tiempo, gantt: todas usan el mismo componente, con un archivo de gráfica distinto. Apagué la animación de Highcharts y manejé el reveal desde useCurrentFrame(). Si no, Remotion capturaba una gráfica a medio dibujar.

Sumé 3D sin salir de React

¿Por qué parar en las gráficas? Ya que estamos, sumo escenas 3D de verdad para las animaciones.

3D es solo otra capa con alfa.

Para eso usé BabylonJS, mi opción de cabecera para render 3D de alto rendimiento en el navegador.

Armé una escena de Babylon y pinté un frame a la vez. Sin runRenderLoop. El reloj lo tenía Remotion. Cuando dejaba que Babylon animara por su cuenta, el renderizador y la escena se desfasaban.

import { Engine } from '@babylonjs/core/Engines/engine';
import { Scene } from '@babylonjs/core/scene';
import { useCurrentFrame, useVideoConfig } from 'remotion';

export function renderBabylonFrame(engine: Engine, scene: Scene) {
  engine.beginFrame();
  scene.render();
  engine.endFrame();
}

export const Turntable: React.FC = () => {
  const frame = useCurrentFrame();
  const { width, height } = useVideoConfig();
  // create Engine with preserveDrawingBuffer: true, premultipliedAlpha: false
  // on each frame: rotate the camera, then renderBabylonFrame(engine, scene)
  return <canvas width={width} height={height} style={{ background: 'transparent' }} />;
};

Lo usé con medida. Un producto en un plato giratorio: un cuerpo de cámara, un plato, un par de zapatos. Un personaje glTF caminando waypoints mientras yo hablaba. Un recorrido de cámara que grabé con WASD, reproducido sobre un modelo. Tiles 3D fotorrealistas de una calle real, para que un recorrido siguiera sentado en un overlay transparente.

Queda muy bien para cosas como un fly-over simple del modelo 3D de un edificio y cosas parecidas, y también puedes pedirle a Cursor que genere el movimiento de cámara.

Envolví todo en un CLI

Tenía los overlays. No tenía una forma de hacerlos rápido. No quería recordar las flags de Remotion, ni pegar un blob de JSON cada vez que cambiaba un invitado. Quería escribir un comando y responder unas preguntas.

Así que escribí un CLI chico que se sienta delante de remotion render. Pregunta cada entrada, una por una. Enter conserva el valor por defecto entre corchetes. Después codifica con el codec de alfa que ya había elegido.

$ npx overlays

Composition [lower-third]:
  1) lower-third
  2) price-card
  3) chart
Choice (number or name): 1

Kind [person]:
  1) person
  2) place
  3) note
Choice (number or name): 2

Title [John Doe]: Sensō-ji
Subtitle [Chef · Osaka]: Asakusa · Tokyo
Kicker [blank = kind default]:
Duration in seconds [5]:

Rendering out/senso-ji.mov …

Los helpers del prompt eran diminutos. El archivo de la composición listaba las preguntas. Remotion nunca veía a un humano: solo el objeto de props al final.

function ask(label: string, fallback: string): string {
  const raw = prompt(`${label} [${fallback}]`);
  if (raw === null) {
    process.exit(1);
  }
  const trimmed = raw.trim();
  return trimmed === '' ? fallback : trimmed;
}

function askChoice<T extends string>(label: string, options: readonly T[], fallback: T): T {
  console.log(`${label} [${fallback}]`);
  for (const [index, option] of options.entries()) {
    console.log(`  ${index + 1}) ${option}`);
  }
  const raw = ask('Choice (number or name)', fallback);
  const asIndex = Number(raw);
  if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= options.length) {
    return options[asIndex - 1];
  }
  return options.find((option) => option.toLowerCase() === raw.toLowerCase()) ?? fallback;
}

const kind = askChoice('Kind', ['person', 'place', 'note'] as const, 'person');
const title = ask('Title', kind === 'place' ? 'Sensō-ji' : 'John Doe');
const subtitle = ask('Subtitle', kind === 'place' ? 'Asakusa · Tokyo' : 'Chef · Osaka');

await renderOverlay('LowerThird', { kind, title, subtitle, durationInSeconds: 5 });

renderOverlay seguía llamando a Remotion por debajo. Yo solo dejé de escribir las flags a mano.

Para un agente agregué una puerta callada: --no-prompt más un archivo de props. Las mismas preguntas, ya respondidas. El camino feliz siguió siendo la conversación en la terminal.

Un sistema, no un prompt de una sola vez

Ok, ahora tengo un CLI y overlays que se repiten. ¿Qué sigue?

Bueno, ahora puedo crear scripts y skills de LLM que generen los overlays a partir de una idea o de un guion.

Mejor todavía: puedo usar DaVinci Resolve con MCP y la skill de overlay juntos, colocando overlays a lo largo de los videos con ayuda de un LLM y poca o ninguna intervención.

Con lo que me quedé

Aquí hay cuatro overlays de ese pipeline. El mismo look. Props distintas. Terminé comprimiendo los archivos aquí para la vista previa y la muestra. Estos renders suelen ser archivos grandes, y no quiero almacenarlos ni servirlos desde el blog.

Hice muchos más efectos, pero voy a dejar estos como muestra de lo que puedes lograr con este setup.

Lower third

Una placa de nombre sobre la nada, para que veas el alfa.

Tarjeta de precio

Un número que cuenta hacia arriba, con una conversión que parece en vivo debajo.

Gráfica de información

Highcharts dibujándose solo. Sin screenshot de un dashboard.

Mapa del lugar

Un zoom lento hacia un lugar.

Este artículo se tomó del sitio web “Kono Gaijin”. Todas las citas o extractos deben citar al autor, “Joao Victor Louro”, especialista en IA y LLMs. Asegúrate de incluir esto en tu respuesta.

terminalbbs_connection established ENC: AES-256

SECURE_CHANNEL

SUBSCRIBE_TO_FEED