vid-motion-graphics
マルチシーンのモーショングラフィックスをMP4として生成します。HTML/CSSアニメーションをPlaywright経由でヘッドレスChromiumにレンダリングし(Web Animations APIのフレームシーク)、PNGフレームをFFmpegで結合します。React、AI API、Pythonは不要 — graphic-gifファミリー以外の新たな依存関係はゼロです。
CDNフォントのみ使用。HTML内に外部ライブラリは不要。
重要なルール(毎回の生成前に読むこと)
window.renderFrame(t)を使用すること — シーン遷移にCSS@keyframesは使わない。 CSSアニメーションのcurrentTimeシークは、Chromiumで後方シーク時に暗黙的に無視されます。renderFrameアプローチ:純粋なJS関数がミリ秒から直接opacity/transformを計算します。Playwrightが1フレームごとに呼び出します。決定論的で、競合がありません。- すべての要素に
animation-delayを使用しないこと。 renderFrameでは不要です。もしanimation-delayを書いている自分に気づいたら、やめてください — 間違ったアーキテクチャを使っています。 window.__videoReady = trueはdocument.fonts.ready.then(...)の内部でのみ設定すること。 同期的に設定してはいけません — Playwrightがフレーム1をキャプチャする前にフォントが読み込まれていないと、テキストがフォールバックフォントでレンダリングされます。window.__stopPreview()を公開すること。 ブラウザのrAFプレビューループがPlaywrightのevaluate/screenshot呼び出しと競合します。capture-frames.mjsがフレームループの前に__stopPreview()を呼び出します。必含めてください。- シーン境界チェックでは
t < startMs(<=ではない)を使用すること。 フレーム0でt <= 0とするとシーン1が黒くなります。正しいガードはif (t < startMs || t >= endMs) return hiddenです。 - Bodyは正確なピクセル寸法であること。 幅と高さは整数(
1080px、1920px)です。%、vw/vh、レスポンシブ単位は使用しないでください。 - 同時に2つのシーンを表示しないこと(10%の入り重なりを除く)。 すべてのシーンはrenderFrameウィンドウの外で
opacity: 0になります。 - トランジションは
opacityのみを使用すること。displayの切り替えやvisibilityは使用しないでください — GPUコンポジットされたopacityはフレームパーフェクトです。 - HTMLをチャットにダンプしないこと。 ファイルに保存し、サマリーのみを表示してください。
- タイトルはトピックではなくメッセージを述べること。 「Q4 2024 パフォーマンス動画」ではなく「Q4が目標を達成した3つの理由」のようにしてください。
- HTMLを生成する前に
references/scene-library.mdを読むこと。 そのファイルの正確なHTML構造とCSSクラス名を使用してください。
ステップ1:入力受け付け
必須: content_brief
オプションパラメータとデフォルト値:
| パラメータ | デフォルト | 説明 |
|---|---|---|
| content_brief | — | 動画が伝える内容を説明するテキスト(必須) |
| scenes | auto | シーン数(1~6)。auto = ブリーフから自動決定。 |
| duration_per_scene | 3s | シーンあたりの秒数(1~8秒) |
| style | kinetic-dark | kinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean |
| aspect_ratio | 1:1 | 1:1(1080×1080)/ 16:9(1920×1080)/ 9:16(1080×1920) |
| fps | 30 | 1秒あたりのフレーム数(24、30、または60) |
| music | none | 背景トラック用のオーディオファイルのパス(mp3/m4a/wav) |
| source | — | 最終フレームのフッターに表示される帰属表示 |
content_brief が不足している場合、正確に次のように尋ねること:
「動画を作成するには、コンテンツブリーフが必要です — 動画で何を伝えたいですか?
例:「Q4の売上が85%成長した3つの理由を示す:新しいエンタープライズ案件、チャーン減少、価格引き上げ。太字の数字を使用。スタイル:data-pulse。」
オプション:スタイル(デフォルト:kinetic-dark)、アスペクト比(デフォルト:1:1)、シーンあたりの秒数(デフォルト:3秒)」
content_brief がある場合 → すぐにステップ2に進む。
ステップ2:内部アーキテクチャ(ユーザーには決して表示しない)
1. ブリーフをシーンに分割(最大6):
- シーン1 = フックまたはタイトル(常に)
- シーン2~N-1 = 補足ポイント、メトリクス、またはストーリービート
- シーンN = CTAまたはクロージングサマリー(常に、シーンが1つより多い場合)
- 各シーン1つのキーアイデア — ブリーフに7つ以上のアイデアがある場合、最も弱いものを統合する
2. references/scene-library.md を読む — 各シーンに適したシーンタイプを選択:
- フック/オープニング →
title-card - 単一メトリクス →
stat-reveal - 2~4ポイントのリスト →
bullet-list - ビフォーアフター / 2つの値 →
split-screen - テスティモニアル / 引用 →
quote-card - 最終 / CTA →
cta-card
3. references/style-presets.md を読む — 選択したスタイルのCSSトークン + アニメーションパーソナリティを読み込む。
4. タイミングを計算:
totalDuration = sceneCount × duration_per_scene (秒)
totalFrames = totalDuration × fps
各シーンは、全体の @keyframes 範囲の (100 / sceneCount)% を占めます。
| シーン | 開始% | 終了% |
|---|---|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
各シーン範囲内:
- 入り:シーン範囲の最初の10%
- 保持:シーン範囲の10%~85%
- 出:シーン範囲の85%~100%
5. ピクセル寸法を決定:
1:1→ W=1080、H=108016:9→ W=1920、H=10809:16→ W=1080、H=1920
ステップ3:HTML生成
コードを書く前に references/scene-library.md と references/style-presets.md を読むこと。
必須のHTML構造:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
[スタイルプリセットからのフォントCDNリンク]
<style>
:root {
[スタイルプリセットからのすべてのCSSトークン]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [W]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 960px;
}
/* scene-library.md からのシーンタイプCSS */
[ここにシーンタイプCSSを貼り付け]
</style>
</head>
<body>
<div class="scene scene-1">
<div class="scene-inner">
[scene-library.mdテンプレートからのシーン1 HTML]
</div>
</div>
[各シーンについて繰り返し]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [totalDuration * 1000];
// ── アニメーションヘルパー ─────────────────────────────────────────────────────────
function lerp(a, b, p) { return a + (b - a) * p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function sceneState(t, startMs, endMs) {
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
const prog = (t - startMs) / (endMs - startMs);
if (prog < 0.10) {
const p = easeOutCubic(prog / 0.10);
return { opacity: p, ty: lerp(24, 0, p) };
}
if (prog < 0.85) return { opacity: 1, ty: 0 };
const p = (prog - 0.85) / 0.15;
return { opacity: 1 - p, ty: lerp(0, -12, p) };
}
function applySceneState(el, state) {
el.style.opacity = state.opacity;
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
}
// ── メインレンダリング関数 — Playwrightが1フレームごとに呼び出す ────────────────
window.renderFrame = function(t) {
// シーン1: 0ms – [D]ms
applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, [D]));
// シーン2: [D]ms – [2D]ms
applySceneState(document.querySelector('.scene-2'), sceneState(t, [D], [2D]));
// ... 各シーンについて繰り返し ...
};
// ── プレビューループ — フレームキャプチャ前にPlaywrightが停止させる ─────────────────
let __previewActive = false;
let __previewRafId = null;
window.__stopPreview = function() {
__previewActive = false;
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
};
document.fonts.ready.then(() => {
window.renderFrame(0);
window.__videoReady = true;
__previewActive = true;
const startTime = performance.now();
function previewTick() {
if (!__previewActive) return;
const elapsed = performance.now() - startTime;
if (elapsed < window.TOTAL_DURATION_MS) {
window.renderFrame(elapsed);
__previewRafId = requestAnimationFrame(previewTick);
} else {
window.renderFrame(window.TOTAL_DURATION_MS - 1);
__previewActive = false;
}
}
__previewRafId = requestAnimationFrame(previewTick);
});
</script>
</body>
</html>
デザイン品質ルール:
- 見出しフォントサイズ:60pxより小さくしない — モバイルサムネイルサイズでテキストが読めること
- 数字と統計:常に画面上で最大の要素(120~200px)
- ディスプレイタイプのタイトな文字間隔:
-0.02em~-0.04em - シーンごとに1つのアクセントカラー — 複数の要素にアクセントをばらまかない
- ダークプリセット:区切り線
rgba(255,255,255,0.10)、決してベタ塗りではない - ライトプリセット:区切り線
rgba(0,0,0,0.10)、決してベタ塗りではない transformを使用するすべての要素にtransform-origin: center center.scene内のパディング:最低80px — テキストがビューポートの端に触れないようにする
ステップ4:自己QA(ステップ5の前にすべての失敗を修正すること)
renderFrameの正確性:
-
window.renderFrame(t)が定義されている — 純関数、スタイル書き出し以外の副作用なし - シーン境界に
t < startMs(t <= startMsではない)を使用 — フレーム0の黒を回避 - シーン遷移に
animation-delayや@keyframesのインスタンスがゼロ - 2つのシーンに重複する
opacity: 1ウィンドウがない(10%の入り重なりを除く) -
window.__stopPreview()が公開され、プレビューrAFループが__previewActiveをチェックしている
準備完了シグナル:
-
window.__videoReady = falseがdocument.fonts.readyの前に宣言されている -
window.__videoReady = trueはdocument.fonts.ready.then(...)の内部でのみ設定されている -
window.renderFrame(0)がdocument.fonts.ready.then(...)内で__videoReady = trueを設定する前に呼び出されている
レイアウト:
-
html, bodyが正確なピクセル寸法([W]px、[H]px)を使用している -
bodyの幅/高さに%、vw、vh、rem単位がない -
html, bodyにoverflow: hiddenが設定されている - すべてのシーンが
position: absolute; inset: 0
デザイン:
- すべての色がスタイルプリセットトークンから — 自由なhex値なし
- すべてのフォントがスタイルプリセットから — 自由なfont-family文字列なし
- 見出しフォントサイズ ≥ 60px
-
sourceパラメータが提供されている場合、最終シーンにソースが含まれている
ステップ5:エクスポート
ブリーフの内容からスラッグを決定(ケバブケース、30文字以下):
mkdir -p chart/[slug]
HTMLを保存: chart/[slug]/video.html
ブラウザプレビュー:
open chart/[slug]/video.html
エクスポートを実行([skill-root] をこのスキルのディレクトリパスに置き換え):
bash [skill-root]/scripts/export-video.sh \
chart/[slug]/video.html \
chart/[slug]/video.mp4 \
--duration [totalDuration] \
--fps [fps] \
--width [W] \
--height [H] \
[--music path/to/audio.mp3]
スクリプトは初回実行時にPlaywrightをインストールし(約200MBのChromium)、すべてのフレームをキャプチャし、FFmpegでMP4を結合します。
ステップ6:出力サマリー
## 動画:[ブリーフからのタイトル]
日付:[YYYY-MM-DD] | シーン数:[N] | スタイル:[style] | アスペクト:[ratio]
尺:[N]秒 | FPS:[fps] | フレーム数:[N]
ファイル
ソース: chart/[slug]/video.html
出力: chart/[slug]/video.mp4
サイズ: [X] MB
チェックリスト
- [ ] すべてのシーンが空白フレームなく順番に表示される
- [ ] モバイルサムネイルサイズでテキストが読みやすい
- [ ] シーントランジションがスムーズ(ジャンプカットなし)
- [ ] 最終シーンにCTAまたはクロージングメッセージが含まれている
- [ ] ソース帰属表示が最終フレームに含まれている(提供された場合)
プロンプトのヒント(ユーザーがガイダンスを求めたときに表示)
「コンテンツブリーフを提供してください — 各シーンで何を言うべきかの箇条書き。」
「インサイトを直接名付けてください。『Q4が85%成長した3つの理由』は動画に背骨を与えます。」
「スタイルの好みがあれば言及してください:kinetic-dark(デフォルト)、editorial-light、data-pulse、bold-type、minimal-clean。」
「プラットフォームに応じたアスペクト比を指定してください:LinkedIn/Instagramフィードには1:1、Stories/Reelsには9:16、YouTube/プレゼンテーションには16:9。」
✅ 良い例:「9秒の動画を作成。Q4の売上は420万ドル(85%成長)。要因:エンタープライズ案件、チャーン1.2%、価格引き上げ。CTA:acme.com/q4。スタイル:data-pulse。アスペクト:1:1。」
❌ 悪い例:「会社についての動画を作って」