> ## 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を、手軽に: Stitch + Remotion + Cursor
- URL: https://konogaijin.com/ja-jp/articles/motion-graphics-tegaru-ni/
- Published: 2026-09-28T00:03:52.000Z
- Updated: 2026-09-28T00:09:47.000Z
- Description: カットに載せるたびに同じ見た目のオーバーレイが欲しかった。動画モデルは毎回別のカードを作ってくるので、Stitch、デザインドキュメント、Cursor、Remotionでパイプラインを組んで、編集用の透過クリップを書き出した。
- Author: Kit
- Tags: #lang-ja-jp, #translation-motion-graphics-made-easy, Remotion, Motion graphics, Cursor

自分の冒険や旅を記録する、小さなYouTubeチャンネルを作っています。ちゃんとした価値を載せたくて、動画が生きるようなアニメーション、グラフ、Motion Graphicsを入れたいんです。

ただ、人を雇う予算も時間もないので、簡単で、速くて、安いやり方を探すことにしました。

カットに載せるたびに同じ見た目のオーバーレイが欲しかった。同じ書体。同じ色。同じ動き。動画モデルに頼むと、レンダーのたびに新しい驚きが返ってきました。同じカードは二度と出ないし、他にも制限が多い（透過、時間制限、映像への干渉、解像度制限、など）。

そこで、モデルにクリップを発明させるのはやめて、自分用のパイプラインを作る方向に切り替えました。

この投稿では、その手順を説明します。本番チャンネルのリポジトリのコードを、そのまま渡すことは`しません`。考え方とセットアップをコピーして、好きなLLMに投げれば、動くものは作れます。下のスニペットは削ったサンプルで、あのリポジトリそのものではありません。

サンプルプロジェクトはここにあります: [Sample Overlay Generator](https://github.com/JoaoVictorLouro/sample-overlay-generator?ref=konogaijin.com)。

繰り返しますが、一行ずつのチュートリアルではありません。それでも、ゴールまで連れていくには十分です。

## Motion Graphicsとは

Motion Graphicsは、意図して動く文字と図形です。生成された映画ではありません。映像の上に載せる、デザインされた部品です。

すでに知っているものがいくつかあります:

- インタビューの上に滑り込む、ゲスト名の**ローワーサード**。
- 試合の隅にある**スコアバグ**: 2–1、67'。
- YouTubeが次の動画ボタンを置けるように、静止したままの**エンドスクリーン**。
- ラーメンの横で数字が増えていく**値札**。
- 数字を話しながら、自分で描かれていく**チャート**。

仕事は毎回同じです。情報を足して、それから邪魔にならないところへ退く。

## Remotionの登場

[Remotion](https://www.remotion.dev/?ref=konogaijin.com)は、コードでMotion Graphicsを作るツールです。アニメーションを、コードを書くように書きます。動画のためのReactです。コンポーネントを書いて、Studioでプレビューして、フレームをレンダーします。

これが自分にとっての突破口でした。ローワーサードはpropsを持つカードになった。エンドスクリーンはレイアウトになった。チャートは、データを渡せばいいコンポーネントになった。

ただ、このボイラープレートを全部手で書くのは時間がかかりすぎるので、セットアップを書くのにCursorを使いました。

完璧ではありません。それでも仕事は終わります。そして繰り返しますが、最終的な成果物はオーバーレイであってコードではないので、プロダクション品質のコードを書くことにはこだわりませんでした。シンプルに保ちたかったんです。

コーディングLLMは、すでにReactの編集の仕方を知っていました。Cursorに字幕を変えてと頼むのは、ウェブサイトのカードを変えてと頼むのと同じ感覚でした。「先週にもう少し近づけて」と動画モデルと格闘していたのではありません。

## ここではコーディングLLMが動画モデルに勝った

動画モデルは「このカードを、この名前で、まったく同じようにもう一度」が苦手でした。新しい見た目を発明する。前のテイクを忘れる。字幕が変わるたびに、生成の分数だけ課金される。

コーディングLLMは、逆の種類の仕事をしました:

- **一貫性。**デザインドキュメントを読んで、そのシステムの中に留まりました。次の火曜でも、アクセントカラーは同じでした。
- **再利用。**新しいローワーサードをプロンプトしたのではありません。新しいpropsを渡しただけです。同じガラス、新しい文言。
- **価格。**トークンと、ローカルのエンコード。オーバーレイができてしまえば、バリエーションは自分のマシン上の無料の計算です。ゲストごとに動画モデルをもう一周、ではありません。

モデルがシステムを書き、レンダラーがそれを繰り返しました。

ゲスト12人は、サイコロを12回振るのではなく、propsの12行でした。

## どう組み合わせたか

役には立ちました。でも必要なものの100%ではありませんでした。Remotionは、作業を自動化してくれないし、自分のブランドを長く保ってくれるわけでもありません。

その上、動画を組み立てるときに、もっと扱いやすいものが必要でした。オーバーレイのたびにRemotionを開き直したくない。こう言えるだけでよかったんです。「名前用のオーバーレイを作って。名前はJohn Smith」（みたいな）。

そこで、いくつかのツールを組み合わせました:

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

```

追記: NLEは`Non-Linear Editing`の意味です。透過を持った映像トラックで、別の映像の上に載せるものです。

### フレームではなく、見た目から始めた

最初に[Google Stitch](https://stitch.withgoogle.com/?ref=konogaijin.com)を使って、シンプルなブランドのアイデンティティ、見た目と雰囲気を作りました。完成したクリップは頼んでいません。コーディングLLMに渡せる、デザインのスタイルと言語が欲しかったんです。

旅のブイログは、暗い、ガラス、影を落とすのではなく光を放つネオン、で返ってきました。家計のシリーズは、クリームの紙、インクは一色、太い数字が一つ。料理チャンネルは、暖かい木、大きい文字、レシピカード。

StitchからHTMLをダウンロードして、短い`DESIGN.md`を書きました。モデルがマークアップだけでなく、トークンを持てるようにです:

- `DESIGN.md` — トークン、書体、余白、雰囲気。色、フォントファミリー、カードがどう見えてよいか。新しいコンポーネントを、既存のスタイルに揃えて作るために、ここが大事です。
- HTML — Stitchから来たレイアウトの静的なスナップショット。参照であって、レンダー対象ではありません。Cursorが起点にするのはこれなので、オーバーレイがStitchと同じ見た目になるために大事です。

デザインドキュメントは、長くある必要はありませんでした:

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

```

それをHTMLと一緒にLLMへ渡しました。

これでデザインの変更もセットアップも基準ができて、安定して、きれいです。もうサイコロを振るようなランダムはありません。

### Remotionにつなぐ

公式のスキャフォールダーCLIを使いました。Studio、`Composition`のルート、エントリファイルの場所をすでに知っているレンダーCLIが手に入りました。

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

```

それから、Cursorがコピーできるものを渡すために、それらのファイルをプロジェクトへ置きました。`DESIGN.md`はリポジトリのルートです。StitchのHTMLのzipは`design-sample`フォルダに展開しました。Cursorには両方を読むように頼みました。トークンはマークダウン、レイアウトはHTML（サンプルとして）。

大事なファイルは、それでも小さかったです:

```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`はルートを登録するだけです。Studioもレンダラーも、ここから始まります。

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

registerRoot(RemotionRoot);

```

`src/Root.tsx`がカタログでした。幅、高さ、fps、デフォルトのprops、そして尺が文言に追従するための`calculateMetadata`。学んでいるあいだは1920×1080を使いました。4Kで仕上げる段になって、3840×2160に切り替えました。

```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`に必要だったスクリプトは三つだけです。見るためのStudio。書き出すためのRender。

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

```

`remotion.config.ts`は正直に保ちました。古いエンコードは上書きする。アルファが必要なファイルに、JPEGの背景をこっそり入れない。それはレンダー時の話です。

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

Config.setOverwriteOutput(true);

```

### ローワーサードを一つ作った

Cursorに`DESIGN.md`と`design-sample/`のHTMLを指して、その静的なカードをRemotionのコンポジションにしてと頼みました。見た目はすでにそのファイルの中にありました。手でスタイルを作り直してはいません。必要だったのは、透過のキャンバスと、いくつかのpropsと、フェードインとフェードアウトだけです。

propsは三つで足ります。キッカー、タイトル、サブタイトル。尺はpropsに載せたので、同じカードを3秒でも8秒でも保てます。`AbsoluteFill`は空のまま、背景なし、です。グラフィック以外のところからは、映像が透けて見えます。

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

```

それから、同じコンポーネントを使い回しました。インタビューの上のゲスト名。引きの画の上の地名。夜の散歩の前の「このあと」のノート。ラーメンの横の値段。同じ動き、同じ書体。翌週は文字列を二つ変えただけです。

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

```

エンドスクリーンを作る前に、これ一つをちゃんと仕上げました。

### 映像のための穴を開けてレンダーした

後ろに黒い箱があるローワーサードは、オーバーレイではありません。スタンプです。透過を保つコーデックが必要で、そのコーデックへ**PNGフレーム**をレンダーしないと、途中でアルファが死にます。

Macでは**ProRes 4444**の`.mov`に落ち着きました。編集ソフトが好きな形式です。Final Cut、Premiere、DaVinciのどれも、これを話せます。

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

```

WindowsはProResとあまり相性がよくありませんでした。代わりに**VP9 WebM**を使いました。Appleのデコーダを前提にしない、透過動画用としてRemotionがドキュメントしているコーデックです。

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

```

古い編集ソフトがそちらを好むなら、VP8でも同じです。After Effectsや、WebMを嫌うWindowsマシンがフレームのフォルダを欲しがるときは、**PNGシーケンス**がまだ意味を持ちます。

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

```

編集ソフトが好きなコンテナを選びました。アルファは飛ばしませんでした。それがないと、ローワーサードの座る場所がありません。

**ファイルだけでは足りません。**クリップをタイムラインに載せたあとでも、このクリップにはアルファチャンネルがあると、編集ソフトに伝える必要がありました。Premiere: Interpret Footage → Alpha。DaVinci: clip attributes → Alpha mode。After Effects: Interpret Footage → Alpha。Vegas、CapCut、その他は、「transparency」や「composite」の下に同じスイッチを隠しています。それをオフのままにすると、完璧なProRes 4444でも黒い箱として書き出されます。

### 一部の数字はレンダー時に届くようにした

文言の一部は、わざと固定しました。ゲストの名前。実際に使ったトラック。シーンのために選んだ時刻、`21:47`。その時刻に回したからです。

このセットアップなら、レンダー中に日付を取得して計算できます。自分で計算して数字を砕く必要がありません。たとえば、¥1,500のラーメンがUSDでいくらかを見せたいとき、レンダーの中でその情報を取得すれば、自分で確認しなくて済みます。（これはAPIでデータを取っているのであって、LLMが換算を幻覚しているのではありません。安全です）。

動的な情報は`calculateMetadata`の中で取得するか、renderを呼ぶ前に取得して、propsとして渡します。

コーナーバグ用に天気を引きました。「渋谷、18°C、小雨。」ショットの中の都市の現地時刻を見せました。¥1,500の一杯をドルに換算して、手で計算しなくて済むようにしました。トーキングヘッドの下に、RSSフィードの見出しティッカーも試しました。マップタイルを読み込んで、これから歩く寺へズームしました。

固定したい言葉はprops。自分で計算したくないデータはfetchです。

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

```

グラフィックは、数字がどこから来たかを気にしません。ただカウントアップするだけです。

### 同じキャンバスにHighchartsを載せた

もっと欲しくなりました。データを説明して、主張を裏づけるために、動画へ本格的なチャートを足して動かしたかったんです。

ウェブではすでに[Highcharts](https://www.highcharts.com/?ref=konogaijin.com)を信頼していたので、同じチャートをRemotionの中で動かしました。

自分のチャートファイルを渡しました。タイトル、種類、系列。小さなマッパーがそれをHighchartsのオプションにします。`delayRender`でチャートの準備ができるまで待ってから、リビールを走らせます。バーが伸びる、線がなぞられる、ガントが延びる、タイムラインにイベントが出る。

いくつかのカットで、居場所を確保しました。旅の実費を日ごとに並べた棒グラフ。ローンチ週の再生数の折れ線。撮影予算の円グラフ: 移動、食事、宿泊。週のガント: ロケハン、本番、ピックアップの日。夜の散歩のタイムライン: 門、屋台、終電。

ダッシュボードのスクリーンショットではありません。チャートそのものがグラフィックです。

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

```

折れ線、スプライン、エリア、カラム、円、タイムライン、ガント。どれも同じコンポーネントで、チャートファイルが違うだけです。Highchartsのアニメーションは切り、リビールは`useCurrentFrame()`で駆動しました。そうしないと、Remotionは描きかけのチャートを撮ってしまいます。

### Reactから出ずに3Dを足した

チャートで止まる理由がありますか。アニメーション用に、本格的な3Dシーンも足してしまいましょう。

3Dは、アルファを持ったもう一つのレイヤーにすぎません。

それには[BabylonJS](https://www.babylonjs.com/?ref=konogaijin.com)を使いました。ブラウザで高性能な3Dを描くときの、自分の定番です。

Babylonのシーンを組んで、**1フレームずつ**描きました。`runRenderLoop`は使いません。時計はRemotionが持ちます。Babylonに自分でアニメーションさせると、レンダラーとシーンがずれます。

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

```

使うのは控えめにしました。ターンテーブルの上の製品: カメラ本体、丼、靴の一足。話しながらウェイポイントを歩くglTFのキャラクター。WASDで録画したカメラパスを、モデルの上で再生する。実在の通りのフォトリアルな3Dタイル。フライスルーが、それでも透過オーバーレイの上に載ります。

建物の3Dモデルを軽くフライオーバーする、みたいなものに本当に向いています。カメラの動きも、Cursorに作らせられます。

### 全部をCLIで包んだ

オーバーレイはありました。速く作る方法はありませんでした。Remotionのフラグを覚えたくないし、ゲストが変わるたびにJSONの塊を貼り付けたくもない。コマンドを一つ打って、質問に少し答えたかったんです。

そこで、`remotion render`の前に座る小さなCLIを書きました。入力を一つずつ聞きます。Enterを押すと、括弧の中のデフォルトを使います。それから、すでに選んであるアルファのコーデックでエンコードします。

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

```

プロンプトのヘルパーは小さかったです。質問の一覧はコンポジションのファイルにあります。Remotionが見るのは人間ではなく、最後のpropsオブジェクトだけです。

```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`は、中では相変わらずRemotionを呼んでいます。フラグを自分で打つのをやめただけです。

エージェント用には、静かな入り口を足しました。`--no-prompt`と、propsファイルです。同じ質問が、すでに答えられた状態です。うまくいく道は、ターミナルでの会話のままです。

## 一回きりのプロンプトではなく、システム

さて、CLIと、繰り返せるオーバーレイができました。次は何でしょう。

アイデアや台本を渡せばオーバーレイを生成する、スクリプトやLLMスキルを作れるようになりました。

もっとよいのは、DaVinci ResolveのMCPとオーバーレイのスキルを一緒に使えることです。LLMの助けで、ほとんど手を入れずに、動画のあちこちにオーバーレイを置けます。

## 結局、手元に残ったもの

そのパイプラインから出たオーバーレイを四つ、ここに置きます。見た目は同じ。propsが違います。プレビューと見本のために、ここのファイルは圧縮しました。この種のレンダーは大きくなりがちで、ブログに保存したり配信したりしたくないんです。

エフェクトはもっとたくさん作りました。ここでは、このセットアップでどこまで到達できるかのサンプルとして、これだけ残します。

### ローワーサード

何もない上に置いた名札なので、アルファが見えます。

### 価格カード

カウントアップする数字と、その下の、ライブに見える換算です。

### 情報チャート

Highchartsが自分で描いていきます。ダッシュボードのスクリーンショットではありません。

### ロケーションマップ

場所への、ゆっくりしたズームです。