> ## Content Index
> Fetch the complete content index at: https://konogaijin.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Motion Graphics, sem complicação: Stitch + Remotion + Cursor
- URL: https://konogaijin.com/pt-br/articles/motion-graphics-sem-complicacao/
- Published: 2026-09-28T00:03:53.000Z
- Updated: 2026-09-28T00:09:48.000Z
- Description: Eu queria overlays que ficassem iguais toda vez que eu soltava num corte. Um modelo de vídeo inventava um card novo a cada render, então montei um pipeline com Stitch, um doc de design, Cursor e Remotion, e renderizei clipes transparentes para a edição.
- Author: Kit
- Tags: #lang-pt-br, #translation-motion-graphics-made-easy, Remotion, Motion graphics, Cursor

Estou montando um canal pequeno no YouTube para registrar minhas próprias aventuras e viagens. Quero colocar valor de verdade nele, com animações, gráficos e Motion Graphics que façam o vídeo ganhar vida.

Só que eu não tenho orçamento nem tempo para contratar alguém, então saí numa busca para deixar isso fácil, rápido e barato.

Eu queria overlays que ficassem iguais toda vez que eu soltava num corte. O mesmo tipo. As mesmas cores. O mesmo movimento. Pedir isso a um modelo de vídeo me dava uma surpresa nova a cada render. Nunca o mesmo card duas vezes, e com um monte de outras limitações (transparência, limite de tempo, interferência na filmagem, limite de resolução, etc.).

Então parei de pedir para um modelo inventar o clipe, e comecei a montar um pipeline para mim.

Vou explicar o meu processo aqui neste post. Eu `não` vou te entregar o código exato do repositório do canal de produção, mas você pode copiar a ideia e o setup, jogar no LLM que preferir, e ter alguma coisa funcionando. Os trechos abaixo são amostras enxutas, não aquele repositório.

Tem até um projeto de exemplo aqui: [Sample Overlay Generator](https://github.com/JoaoVictorLouro/sample-overlay-generator?ref=konogaijin.com).

De novo: isto não é um tutorial linha por linha, mas chega para te levar até a linha de chegada.

## O que são Motion Graphics

Motion Graphics são texto e formas que se movem de propósito. Não é um filme gerado. É uma peça desenhada que você solta em cima da filmagem.

Alguns que você já conhece:

- Um **lower third** com o nome de um convidado, entrando por cima de uma entrevista.
- Um **score bug** no canto de uma partida: 2–1, 67'.
- Uma **tela final** que fica parada para o YouTube colocar os botões do próximo vídeo.
- Uma **etiqueta de preço** que conta para cima ao lado de uma tigela de ramen.
- Um **gráfico** que se desenha enquanto você fala dos números.

O mesmo trabalho toda vez: acrescentar informação, e depois sair da frente.

## Entra o Remotion

[Remotion](https://www.remotion.dev/?ref=konogaijin.com) é uma ferramenta para criar Motion Graphics com código. Você escreve as animações como se estivesse escrevendo código. É React para vídeo. Você escreve componentes, pré-visualiza no Studio e renderiza os frames.

Esse foi o desbloqueio para mim. Um lower third virou um card com props. Uma tela final virou um layout. Um gráfico virou um componente em que eu só precisava colocar os dados.

Mas escrever todo esse boilerplate na mão ia demorar demais, então usei o Cursor para me ajudar a escrever o setup.

Não é perfeito, mas dá conta do recado. E, de novo, o produto final é o overlay, não o código, então eu não me preocupei em escrever código de produção. Queria manter simples.

Um LLM de código já sabia editar React. Pedir ao Cursor para mudar uma legenda parecia pedir para mudar um card num site. Eu não estava brigando com um modelo de vídeo por “deixa um pouco mais parecido com a semana passada”.

## Por que um LLM de código ganhou de um modelo de vídeo aqui

Modelos de vídeo eram ruins em “faz este card exato de novo, com este nome”. Inventavam um visual novo. Esqueciam a tomada anterior. Me cobravam minutos de geração toda vez que a legenda mudava.

Um LLM de código fazia o tipo oposto de trabalho:

- **Consistência.** Lia um doc de design e ficava dentro do sistema. A cor de destaque continuava a mesma na terça seguinte.
- **Reuso.** Eu não pedia um lower third novo. Eu passava props novas. O mesmo vidro, o texto novo.
- **Preço.** Tokens mais uma codificação local. Depois que o overlay existia, cada variação era computação grátis na minha máquina. Não mais uma rodada de modelo de vídeo para cada convidado.

O modelo escreveu o sistema. O renderizador repetiu.

Doze convidados significavam doze linhas de props, não doze jogadas de dado.

## Como eu juntei isso

Ajudou, mas não era 100% do que eu precisava. O Remotion não automatiza a coisa, nem cria uma marca duradoura para mim.

Além disso, eu precisava de algo mais fácil para montar o vídeo. Não quero reabrir o Remotion para cada overlay. Queria algo simples, em que eu pudesse só dizer: "Me dá um overlay de nomes, usa o nome John Smith" (ou parecido).

Então juntei um punhado de ferramentas:

```mermaid
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

```

PS: NLE quer dizer `Non-Linear Editing`. É uma faixa de vídeo com transparência, para ficar por cima de outro vídeo.

### Comecei pelo visual, não pelo frame

Comecei usando o [Google Stitch](https://stitch.withgoogle.com/?ref=konogaijin.com) para criar uma identidade de marca simples, o look e a sensação. Eu não estava pedindo um clipe pronto. Queria um estilo e uma linguagem de design que eu pudesse entregar a um LLM de código.

Um vlog de viagem voltou escuro, vidro, neon que emite luz em vez de projetar sombra. Uma série de finanças pessoais voltou papel creme, uma cor de tinta, e um único número em negrito. Um canal de culinária voltou madeira quente, tipo grande, um card de receita.

Baixei o HTML do Stitch e escrevi um `DESIGN.md` curto, para o modelo ter tokens, não só markup:

- `DESIGN.md` — tokens, tipo, espaçamento, clima. Cores, famílias de fonte, como um card podia parecer. Isso importa para criar componentes novos, alinhados ao estilo que já existe.
- HTML — um retrato estático do layout vindo do Stitch. Referência, não o alvo do render. Isso importa porque é o ponto de partida do Cursor, e faz o overlay ficar igual ao que está no Stitch.

O doc de design não precisava ser longo:

```markdown
# 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.

```

Entreguei isso ao LLM junto com o HTML.

Agora mudanças de design e o setup tinham um padrão, estável e bonito, sem mais jogadas aleatórias de dado.

### Ligando isso ao Remotion

Usei o CLI oficial de scaffold. Isso me deu o Studio, uma raiz de `Composition`, e um CLI de render que já sabia achar o arquivo de entrada.

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

```

Aí coloquei esses arquivos no projeto para o Cursor ter o que copiar. Meu `DESIGN.md` foi para a raiz do repositório. O zip de HTML do Stitch eu extraí numa pasta `design-sample`. Pedi ao Cursor para ler os dois: o markdown pelos tokens, o HTML pelo layout (como amostra).

Os arquivos importantes continuavam pequenos:

```text
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` só registrava a raiz. O Studio e o renderizador começam os dois ali.

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

registerRoot(RemotionRoot);

```

`src/Root.tsx` era o catálogo. Largura, altura, fps, props padrão, e um `calculateMetadata` para a duração acompanhar o texto. Usei 1920×1080 enquanto aprendia. Passei para 3840×2160 quando fechei em 4K.

```tsx
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` só precisava de três scripts. Studio para olhar. Render para entregar.

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

```

`remotion.config.ts` eu deixei honesto. Sobrescrever encodes antigos. Não enfiar um fundo JPEG escondido se o arquivo precisa de alfa — isso entra na hora do render.

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

Config.setOverwriteOutput(true);

```

### Fiz um lower third

Apontei o Cursor para o `DESIGN.md` e o HTML em `design-sample/`, e pedi para transformar aquele card estático numa composição do Remotion. O visual já estava nesses arquivos. Eu não reestilizei na mão. Só precisava de uma tela transparente, algumas props, e um fade de entrada e de saída.

Três props bastavam: um kicker, um título, um subtítulo. A duração morava nas props, então o mesmo card podia segurar por três segundos ou por oito. O `AbsoluteFill` ficou vazio — sem fundo — para a filmagem aparecer em tudo que não era o gráfico.

```tsx
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>
  );
};

```

Aí reusei o mesmo componente. O nome de um convidado sobre uma entrevista. O nome de um lugar sobre um plano aberto. Uma nota de “a seguir” antes de uma caminhada noturna. Um preço ao lado de uma tigela de ramen. O mesmo movimento, o mesmo tipo. Na semana seguinte eu mudei duas strings.

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

```

Acertei esse antes de montar uma tela final.

### Renderizei com um buraco para a filmagem

Um lower third com uma caixa preta atrás não era um overlay. Era um carimbo. Eu precisava de um codec que guardasse a transparência, e tinha que renderizar **frames PNG** nesse codec, senão o alfa morria no caminho.

No Mac, fui parar no **ProRes 4444** `.mov`. Os editores gostam dele. Final Cut, Premiere e DaVinci falam essa língua.

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

```

O Windows era menos amigável com ProRes. Usei **VP9 WebM** no lugar, o codec que o Remotion documenta para vídeo transparente que não assume o decoder da Apple.

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

```

VP8 funciona do mesmo jeito se um editor mais antigo preferir. Uma **sequência PNG** ainda fazia sentido quando o After Effects, ou uma máquina Windows que odeia WebM, queria uma pasta de frames.

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

```

Escolhi o container de que o editor gostava. Não pulei o alfa. Sem ele, o lower third não tinha onde sentar.

**O arquivo não basta.** Depois de soltar o clipe na timeline, eu ainda tinha que dizer ao editor que o clipe tem canal alfa. Premiere: Interpret Footage → Alpha. DaVinci: clip attributes → Alpha mode. After Effects: Interpret Footage → Alpha. Vegas, CapCut e o resto escondem o mesmo interruptor em “transparência” ou “composite”. Se eu deixasse isso desligado, um ProRes 4444 perfeito ainda renderizava como uma caixa preta.

### Deixei alguns números chegarem na hora do render

Algum texto eu congelei de propósito. O nome do convidado. A faixa que eu realmente usei. Um horário que escolhi para a cena, `21:47`, porque foi quando eu gravei.

Com esse setup, eu posso buscar e calcular a data durante o render, e evitar calcular e mastigar os dados eu mesmo. Por exemplo, se preciso mostrar quanto uma tigela de ramen de ¥1.500 vale em USD, posso buscar essa informação no render e nunca me preocupar em conferir na mão. (Isso é busca de dados numa API, não um LLM alucinando uma conversão. É seguro!).

Busco a informação dinâmica no `calculateMetadata`, ou antes de chamar o render, e passo como props.

Puxei o clima para um bug de canto: “Shibuya, 18°C, chuva leve.” Mostrei o relógio local da cidade no plano. Converti uma tigela de ¥1.500 para dólares para não fazer a conta na mão. Tentei um ticker de manchetes de um feed RSS embaixo de um talking-head. Carreguei tiles de mapa e dei zoom num templo em que eu estava prestes a entrar.

Props para as palavras que eu queria travar. Um fetch para os dados que eu não queria calcular eu mesmo.

```tsx
<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 },
    };
  }}
/>

```

```ts
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;
}

```

O gráfico não se importava de onde o número vinha. Só contava para cima.

### Coloquei o Highcharts na mesma tela

Eu queria mais. Queria adicionar e animar gráficos de verdade nos meus vídeos, para explicar dados e provar um ponto.

Eu já confiava no [Highcharts](https://www.highcharts.com/?ref=konogaijin.com) na web, então hospedei o mesmo gráfico no Remotion.

Alimentei com o meu próprio arquivo de gráfico: um título, um tipo, uma série. Um mapper pequeno transformava isso em opções do Highcharts. Esperei com `delayRender` até o gráfico ficar pronto, e então rodei um reveal — barras cresciam, linhas se traçavam, um gantt se estendia, eventos apareciam numa timeline.

Isso ganhou lugar em alguns cortes. Um gráfico de colunas do que uma viagem realmente custou, dia a dia. Uma linha de views numa semana de lançamento. Uma pizza do orçamento de uma gravação: viagem, comida, hospedagem. Um gantt da semana: visita de locação, filmagem principal, dia de pickup. Uma timeline de uma caminhada noturna: portão, comida de rua, último trem.

Nada de screenshot de um dashboard. O gráfico é a peça.

```tsx
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}
    />
  );
};

```

```json
{
  "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" }
  ]
}

```

Linha, spline, área, coluna, pizza, timeline, gantt: todos usam o mesmo componente, com um arquivo de gráfico diferente. Desliguei a animação do Highcharts e dirigi o reveal a partir de `useCurrentFrame()`. Senão o Remotion capturava um gráfico pela metade.

### Adicionei 3D sem sair do React

Por que parar nos gráficos? Já que estou nisso, adiciono cenas 3D de verdade para as animações.

3D é só mais uma camada com alfa.

Para isso usei o [BabylonJS](https://www.babylonjs.com/?ref=konogaijin.com), meu padrão para render 3D de alta performance no navegador.

Montei uma cena Babylon e pintei **um frame de cada vez**. Sem `runRenderLoop`. O relógio era do Remotion. Quando eu deixava o Babylon animar sozinho, o renderizador e a cena saíam de sincronia.

```tsx
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' }} />;
};

```

Usei com parcimônia. Um produto num prato giratório: um corpo de câmera, uma tigela, um par de sapatos. Um personagem glTF andando por waypoints enquanto eu falava. Um caminho de câmera que gravei com WASD, reproduzido sobre um modelo. Tiles 3D fotorrealistas de uma rua de verdade, para um fly-through ainda sentar num overlay transparente.

Fica muito bom para coisas como um fly-over simples de um modelo 3D de um prédio e coisas parecidas, e você também pode pedir ao Cursor para gerar o movimento de câmera.

### Embrulhei tudo num CLI

Eu tinha os overlays. Não tinha um jeito de fazê-los rápido. Não queria lembrar das flags do Remotion, nem colar um blob de JSON toda vez que um convidado mudava. Queria digitar um comando e responder algumas perguntas.

Então escrevi um CLI pequeno que fica na frente do `remotion render`. Ele pergunta cada entrada, uma de cada vez. Enter mantém o padrão entre colchetes. Depois codifica com o codec de alfa que eu já tinha escolhido.

```text
$ 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 …

```

Os helpers de prompt eram minúsculos. O arquivo da composição listava as perguntas. O Remotion nunca via um humano — só o objeto de props no final.

```ts
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` ainda chamava o Remotion por baixo. Eu só parei de digitar as flags eu mesmo.

Para um agente, acrescentei uma porta quieta: `--no-prompt` mais um arquivo de props. As mesmas perguntas, já respondidas. O caminho feliz continuou sendo a conversa no terminal.

## Um sistema, não um prompt avulso

Ok, agora eu tenho um CLI e overlays que se repetem. E agora?

Bom, agora posso criar scripts e skills de LLM que geram os overlays a partir de uma ideia ou de um roteiro.

Melhor ainda: posso usar o DaVinci Resolve com MCP e a skill de overlay juntos, posicionando overlays pelos vídeos com a ajuda de um LLM e pouca ou nenhuma intervenção.

## Com o que eu fiquei

Aqui estão quatro overlays desse pipeline. O mesmo visual. Props diferentes. Acabei comprimindo os arquivos aqui para prévia e amostra. Esses renders tendem a ser arquivos grandes, e eu não quero armazenar nem servir isso pelo blog.

Fiz muito mais efeitos, mas vou deixar estes aqui como amostra do que dá para alcançar com este setup.

### Lower third

Uma placa de nome sobre o nada, para você ver o alfa.

### Card de preço

Um número que conta para cima, com uma conversão de cara ao vivo embaixo.

### Gráfico de informação

O Highcharts se desenhando. Sem screenshot de dashboard.

### Mapa do lugar

Um zoom lento até um lugar.