> ## 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 made easy: Stitch + Remotion + Cursor
- URL: https://konogaijin.com/en-us/articles/motion-graphics-made-easy/
- Published: 2026-09-28T00:03:50.000Z
- Updated: 2026-09-28T00:06:23.000Z
- Description: I wanted overlays that look the same every time I drop them on a cut. Prompting a video model kept inventing a new card, so I built a pipeline with Stitch, a design doc, Cursor, and Remotion, and rendered transparent clips for the edit.
- Author: Kit
- Tags: #lang-en-us, #translation-motion-graphics-made-easy, Remotion, Motion graphics, Cursor

I am working on a small YouTube channel for recording my own adventures and travels, I want to bring legit value into it, with nice animations, graphs and motion graphics that bring the video to life.

I don't have the budget or time to hire someone though, so I set on a quest to make it easy, fast and cheap.

I wanted overlays that look the same every time I drop them on a cut. Same type. Same colors. Same motion. Prompting a video model kept giving me a new surprise every new render. Never the same card twice and with many other limitations (transparency, time limits, messing with footage, resolution limits, etc.).

So I stopped asking a model to invent the clip, and started working towards creating a pipeline for myself.

I am going to explain my process here in this post. I am `not` going to give you the exact code from my production channel repo, but you can copy the idea and setup, throw it into your LLM of choice, and have something working. The snippets below are stripped samples, not that repo.

You can even find a sample project here: [Sample Overlay Generator](https://github.com/JoaoVictorLouro/sample-overlay-generator?ref=konogaijin.com).

Again, this is not a line by line tutorial, but it's good enough to get you across the finishing line.

## What motion graphics are

Motion graphics are text and shapes that move on purpose. Not a generated movie. A designed piece you drop on footage.

A few you already know:

- A **lower third** with a guest name, sliding in over an interview.
- A **score bug** in the corner of a match: 2–1, 67'.
- An **end screen** that holds still so YouTube can place the next-video buttons.
- A **price tag** that counts up next to a bowl of ramen.
- A **chart** that draws itself while you talk through the numbers.

Same job every time: add information, then get out of the way.

## Enter Remotion

[Remotion](https://www.remotion.dev/?ref=konogaijin.com) is a tool for creating motion graphics through code, you write the animations as if you were writing code, it's React for video. You write components, preview them in Studio, and render frames.

That was the unlock for me. A lower third became a card with props. An end screen became a layout. A chart became a component I just needed to feed the data into.

But writing all this boilerplate code would take too long by hand, so I used cursor to help me write the setup.

It's not perfect, but it gets the job done, and again, the final product is the overlay, not the code, so I didn't bother writing production level code, I wanted to keep it simple.

A coding LLM already knew how to edit React. Asking Cursor to change a subtitle felt like asking it to change a card on a website. I was not fighting a video model for “make it a bit more like last week.”.

## Why a coding LLM beat a video model here

Video models were bad at “make this exact card again, with this name.” They invented a new look. They forgot the last take. They charged me for minutes of generation every time the subtitle changed.

A coding LLM did the opposite kind of work:

- **Consistency.** It read a design doc and stayed inside the system. The accent color was still the same next Tuesday.
- **Reuse.** I did not prompt a new lower third. I passed new props. Same glass, new copy.
- **Price.** Tokens plus a local encode. After the overlay existed, every variation was free compute on my machine. Not another video-model run for every guest.

The model wrote the system. The renderer repeated it.

Twelve guests meant twelve lines of props, not twelve rolls of the dice.

## How I put it together

It helped, but wasn't 100% what I needed, Remotion does not make the thing automated or create a lasting branding for me.

More over I needed something easier to work with for putting my video together, I don't want to reopen remotion for every overlay, I wanted something simple that I could just say: "Give me an overlay for names, use the name John Smith" (or similar).

So I combined a bunch of tools:

```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 means `Non-Linear Editing`: It's a video track with transparency to be placed on top of another video.

### I started with a look, not a frame

I started using [Google Stitch](https://stitch.withgoogle.com/?ref=konogaijin.com) to create a simple brand identity, look and feel. I was not asking it for a finished clip. I wanted a design style and language I could hand to a coding LLM.

A travel vlog came back dark, glass, neon that emits light instead of dropping a shadow. A personal-finance series came back cream paper, one ink color, and a single bold number. A cooking channel came back warm wood, big type, a recipe card.

I downloaded the HTML from Stitch and wrote a short `DESIGN.md` so the model had tokens, not just markup:

- `DESIGN.md` — tokens, type, spacing, mood. Colors, font families, how a card was allowed to look, this is important to create new components, aligned to the the existing style.
- HTML — a static snapshot of layout from Stitch. Reference, not the render target, this is important because it's what Cursor will use as it's starting point, making the overlay look the same as it does in Stitch.

The design doc did not need to be long:

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

```

I gave that to the LLM with the HTML.

Now design changes and setup all had a standard, stable and pretty, no more random dice rolls.

### Connecting it to Remotion

I used the official scaffolder CLI. That gave me Studio, a `Composition` root, and a render CLI that already knew how to find the entry file.

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

```

Then I dropped those files into the project so Cursor had something to copy. My `DESIGN.md` went at the repo root. The HTML zip from Stitch I extracted into a `design-sample` folder. I asked Cursor to read both: the markdown for tokens, the HTML for layout (as a sample).

The important files were still small:

```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` only registered the root. Studio and the renderer both start there.

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

registerRoot(RemotionRoot);

```

`src/Root.tsx` was the catalog. Width, height, fps, default props, and a `calculateMetadata` so duration could follow the copy. I used 1920×1080 while I learned. I switched to 3840×2160 when I finished in 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` only needed three scripts. Studio for looking. Render for shipping.

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

```

`remotion.config.ts` I kept honest. Overwrite old encodes. Do not sneak in a JPEG background if the file needs alpha — that comes at render time.

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

Config.setOverwriteOutput(true);

```

### I made one lower third

I pointed Cursor at `DESIGN.md` and the HTML in `design-sample/`, and asked it to turn that static card into a Remotion composition. The look was already in those files. I did not restyle it by hand. I just needed a transparent canvas, a few props, and a fade in and out.

Three props were enough: a kicker, a title, a subtitle. Duration lived on the props so the same card could hold for three seconds or eight. `AbsoluteFill` stayed empty — no background — so footage showed through everything that was not the graphic.

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

```

Then I reused the same component. A guest name over an interview. A place name over a wide shot. A “coming up” note before a night walk. A price next to a bowl of ramen. Same motion, same type. Next week I changed two strings.

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

```

I got that one right before I built an end screen.

### I rendered with a hole for the footage

A lower third with a black box behind it was not an overlay. It was a stamp. I needed a codec that kept transparency, and I had to render **PNG frames** into that codec or the alpha died on the way out.

On a Mac, I landed on **ProRes 4444** `.mov`. Editors like it. Final Cut, Premiere, and DaVinci all speak it.

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

```

Windows was less friendly with ProRes. I used **VP9 WebM** instead, the codec Remotion documents for transparent video that does not assume Apple’s decoder.

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

```

VP8 works the same way if an older editor prefers it. A **PNG sequence** still made sense when After Effects, or a Windows box that hated WebM, wanted a folder of frames.

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

```

I picked the container the editor liked. I did not skip the alpha. Without it, the lower third had nowhere to sit.

**The file is not enough.** After I dropped the clip on the timeline, I still had to tell the editor the clip has an alpha channel. Premiere: Interpret Footage → Alpha. DaVinci: clip attributes → Alpha mode. After Effects: Interpret Footage → Alpha. Vegas, CapCut, and the rest hide the same switch under “transparency” or “composite.” If I left that off, a perfect ProRes 4444 still rendered as a black box.

### I let some numbers arrive at render time

Some copy I froze on purpose. The guest’s name. The track I actually used. A time I chose for the scene, `21:47`, because that is when I rolled.

With this setup, I can fetch and calculate date during render, I can avoid calculating and crunching data myself. For example, if I need to show how much a ¥1,500 bowl of ramen is in USD, I can fetch that information in the render and never bother checking myself. (That's API data fetching, not an LLM hallucinating a conversion, it's safe!).

I fetch the dynamic information in `calculateMetadata`, or before I called render, and passed them in as props.

I pulled weather for a corner bug: “Shibuya, 18°C, light rain.” I showed the local clock for the city in the shot. I converted a ¥1,500 bowl into dollars so I did not do the math by hand. I tried a headline ticker from an RSS feed under a talking-head. I loaded map tiles and zoomed into a temple I was about to walk into.

Props for the words I wanted to lock. A fetch for the data that I wanted to avoid calculating myself.

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

```

The graphic did not care where the number came from. It just counted up.

### I dropped Highcharts on the same canvas

I wanted more, I wanted to add and animate full on charts for my videos, to explain data and prove points.

I already trusted [Highcharts](https://www.highcharts.com/?ref=konogaijin.com) on the web, so I hosted the same chart in Remotion.

I fed it my own chart file: a title, a type, a series. A small mapper turned that into Highcharts options. I waited with `delayRender` until the chart was ready, then ran a reveal — bars grew, lines traced, a gantt extended, events appeared on a timeline.

That earned its place on a few cuts. A column chart of what a trip actually cost, day by day. A line of views across a launch week. A pie of a shoot budget: travel, food, stays. A gantt of the week: recce, principal, pickup day. A timeline of a night walk: gate, street food, last train.

No screenshot of a dashboard. The chart is the graphic.

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

```

Line, spline, area, column, pie, timeline, gantt, they all use same component but with a different chart file. I turned Highcharts animation off and drove the reveal from `useCurrentFrame()`, otherwise Remotion captured a half-drawn chart.

### I added 3D without leaving React

Why stop on charts? Might as well add full on 3D scenes for animations!

3D is just another layer with alpha.

For that I used [BabylonJS](https://www.babylonjs.com/?ref=konogaijin.com), my go-to for high-performance 3D rendering in the browser.

I built a Babylon scene and painted **one frame at a time**. No `runRenderLoop`. Remotion owned the clock. When I let Babylon animate on its own, the renderer and the scene drifted.

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

```

I used it sparingly. A product on a turntable: a camera body, a bowl, a pair of shoes. A glTF character walking waypoints while I talked. A camera path I recorded with WASD, played back over a model. Photorealistic 3D tiles of a real street, so a fly-through still sat on a transparent overlay.

It's really nice for things like a simple fly-over a 3D model of a building and similar stuff, and you can also get Cursor to generate the camera moment for you!

### I wrapped the whole thing in a CLI

I had the overlays. I did not have a way to make them fast. I did not want to remember Remotion flags, or paste a JSON blob every time a guest changed. I wanted to type one command and answer a few questions.

So I wrote a small CLI that sits in front of `remotion render`. It asks for each input, one at a time. Enter keeps the default in brackets. Then it encodes with the alpha codec I already picked.

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

```

The prompt helpers were tiny. The composition file listed the questions. Remotion never saw a human — only the props object at the end.

```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` still called Remotion under the hood. I just stopped typing the flags myself.

For an agent I added a quiet door: `--no-prompt` plus a props file. Same questions, already answered. The happy path stayed the conversation in the terminal.

## A system, not a one-off prompt

Ok, so now I have a CLI and my repeatable overlays, what's next?

Well, now I can create scripts and LLM skills that will generate the overlays given an idea or script!

Even better, I can use DaVinci Resolve with MCP and the overlay skill together, placing overlays across videos easily with the help of an LLM and little to no intervention!

## What I ended up with

Here are four overlays from that pipeline. Same look. Different props. I ended up compressing the files here for preview and showcase. These renders tend to be large files and I don't want to deal with storing or serving them through my blog.

I made a lot more effects, but I will leave these here as a sample of what you can achieve with this setup.

### Lower third

A name plate over nothing, so you can see the alpha.

### Price card

A number that counts up, with a live-looking conversion under it.

### Info chart

Highcharts drawing itself. No screenshot of a dashboard.

### Location map

A slow zoom into a place.