demo-reel は何をしますか?
Webアプリのスクリプト化された操作手順を、洗練された画面収録風の動画に変換します:スムーズな60fpsの動き、表示可能なカーソル+ハロー、クリックリップル、キャプション。実際のアプリをヘッドレスChromiumで駆動します — 映像は製品そのものであり、モックアップではありません。
なぜ動画がスムーズなのか
通常の画面収録はブラウザがペイントするタイミングでフレームをキャプチャするため、遅いマシン(またはソフトウェアWebGL)では約10〜15fpsになりカクつきます。demo-reelは代わりに仮想クロック(scripts/inject.js)を注入します:requestAnimationFrame、performance.now、Dateはレコーダーが指示したときだけ進み、CSS/Webアニメーションはdocument.getAnimations()でステップ実行されます。各フレームは正確に1/fpsだけ進められ、スクリーンショットされ、ffmpegにパイプされます。フレームのレンダリングにどれだけ時間がかかっても、結果は完全にスムーズに再生されます。
タイマー(setTimeout/setInterval)は意図的に実際のままにしています — それらを偽装すると、ローダーやスケジューラのゼロ遅延タイマーチェーンが永遠に回り続けるからです(これがPlaywright独自のpage.clockが多くのWebGLページでハングする理由です)。
必要条件
- Node 18以降、ffmpegがPATHに含まれていること。
- 一度だけ、このスキルのディレクトリで:
npm install && npx playwright install chromium。
ワークフロー
- アプリをサーブ — ユーザーが見る方法で。理想的には本番ビルド(
vite build && vite preview、next build && next start)をローカルまたはモックデータに対して。開発サーバーでも動作しますが、遅く、開発用オーバーレイが表示される場合があります。実際のデータや機密データを決して録画しないでください — デモデータをシードします。 - シナリオを書く(
*.scenario.mjs、以下のAPI参照)。examples/tasks.scenario.mjsから始めてください。ストーリーとして計画:20〜60秒、キャプションごとに1つのアイデア、各結果の後に1〜2秒ホールドして視聴者が読めるように。 - 録画:
node scripts/record.mjs my.scenario.mjs --out demo.mp460秒のクリップ(1080p)は約3〜10分かかります(重いWebGLページが最も遅い)。バックグラウンドで実行してください。 - 品質確認:
scripts/sheet.sh demo.mp4でコンタクトシート(2.5秒ごとに1フレーム)を作成。確認事項:キャプションが画面の内容と一致している、ローディングスピナーがない、キャプションで隠れているものがない、バッジ/カーソルが表示されている。連続するフレームをいくつか抽出してスムーズさをスポットチェック — 動きのあるときは各フレームが少しずつ異なるはず。 - 納品(「動画の使い方」参照)。
シナリオAPI
export default {
size: "1920x1080", // ビューポート = 動画サイズ
fps: 60,
theme: { captionPosition: "bottom", haloFill: "rgba(250,204,21,.30)" }, // オプション
async run(d) {
await d.open("http://localhost:4173/"); // ナビゲート+安定化(録画されない)
d.badge("NEW", "#15803d"); // コーナーバッジ(nullで非表示)
d.caption("新しいダッシュボード"); // キャプションピル(nullで非表示)
await d.hold(1500); // 1.5秒そのまま録画
await d.click(d.page.getByRole("button", { name: "Filters" }));
d.caption("フィルターは1つのパネルに"); // アクションの後にキャプションを設定
await d.hold(1200);
await d.type("Stockholm"); // 人間的な速度でタイピング
await d.press("Enter");
await d.scroll(600); // スムーズなホイールスクロール
await d.step("オプショナルパート", async () => { /* エラーはログに記録され、録画は継続 */ });
},
};
| コール | 動作 |
|---|---|
d.open(url, { settle }) | ナビゲート、ページが安定するまで待機(録画されない、デフォルト2500ms)。 |
d.hold(ms) | ページをそのまま録画。 |
d.settle(ms) | 時間を録画せずに進める — 表示したくないローディング状態、遅延チャンク、レイアウトアニメーションをスキップ。 |
d.move(target, { ms }) / d.hover | カーソルを(イージング付きで)ターゲットにグライド。 |
d.click(target, { ms, button }) | 移動、リップル付きでクリック。button: "right"でコンテキストメニュー。 |
d.doubleClick(target) | ダブルクリック。 |
d.type(text, { delay }) | 一度に1キーずつ入力(デフォルト90ms/キー)。 |
d.press(key) | キーボードショートカット、例:"Escape"、"Control+K"。 |
d.scroll(dy, { ms }) | スムーズなホイールスクロール。 |
d.caption(text) / d.badge(text, color) / d.cursor(bool) | オーバーレイの状態;フルページナビゲーション後も持続。 |
d.step(name, fn) | 名前付きステップ;エラーはログに記録され、致命的ではない。 |
d.page、d.point(target)、d.width、d.height | エスケープハッチ。 |
ターゲットはPlaywrightのLocator、CSSセレクター文字列、{ x, y }、または非同期関数(page) => ({ x, y })です。
CanvasレンダリングUI(グラフ、地図、チャート)にはアイテムごとのDOM要素がありません — ライブラリに画面座標を尋ねる関数を渡してください。Cytoscape.jsの例:
const node = (label) => (page) => page.evaluate((label) => {
const el = [...document.querySelectorAll("div")].find((e) => e._cyreg?.cy);
const n = el._cyreg.cy.nodes().filter((n) => n.data("label") === label)[0];
const r = el.getBoundingClientRect(), p = n.renderedPosition();
return { x: r.left + p.x, y: r.top + p.y };
}, label);
await d.click(node("Server A"));
良いデモを書くために
- キャプションは真実を伝える。 映像に映っていることだけを主張してください。取り直しで再現しないバグを見せたい場合、キャプションを変更してください — 保持しないで。
- アクションの後にキャプションを設定し、前には設定しない — そうしないと次の画面のキャプションが前の画面に重なります。
- クリックではなく結果を見せる。 各結果で1〜2秒ホールド;
d.settleでローディングをカット。 - Before/After:同じストーリーとステップで2つのシナリオを録画し、バッジ
BEFORE/AFTER(赤/緑)を付け、別々のファイルとオプションの連結を保持します。 - Heroモーメント(ランディングページ、アニメーション):5〜10秒与える — 視聴者は理解する時間が必要。
- カーソルは落ち着かせる:移動あたり600〜900ms、ホールド中はコンテンツから離れた場所に置く。
- 実際のタイマーはフレームレンダリング中も動き続ける(フレームあたり約50〜300msの実時間)ため、タイマー駆動のUI(トースト、デバウンス検索、自動消滅ツールチップ)は現実よりも早く動画から消える可能性があります。画面上に表示したい直前にトリガーするか、ホールド時間を短くしてください。
動画の使い方
- PowerPoint:挿入 → ビデオ → このデバイス。再生タブ:開始:自動的 および 停止するまでループ。H.264
yuv420pMP4(demo-reelが書き出すもの)はWindowsとmacOSで再生できます。 - Keynote:ドラッグ&ドロップ;ムービー → 繰り返し:ループ。
- Web:
<video src="demo.mp4" autoplay loop muted playsinline></video>。 - GIF(README、チャット):
ffmpeg -i demo.mp4 -vf "fps=20,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" demo.gif - 同じサイズ/fpsのクリップを連結:
printf "file 'a.mp4'\nfile 'b.mp4'\n" > l.txt && ffmpeg -f concat -safe 0 -i l.txt -c copy both.mp4
トラブルシューティング
| 症状 | 修正方法 |
|---|---|
| 空白 / 「Loading…」フレーム | d.openでsettleを長くする、またはナビゲーションクリック後にawait d.settle(1500)。 |
| ターゲットが表示されない | 画面外または閉じたメニュー内 — 先に開く/スクロール;Canvasアイテムにはポイント関数を使用。 |
| WebGL/3Dが黒くレンダリングされる | デフォルトの起動フラグ(--use-angle=swiftshader)を維持;遅い(約0.25秒/フレーム)が正しく動作。 |
| 動画が計画より短い/長い | 時間は正確:hold + 移動の合計。settle時間は決して録画されない。 |
| PowerPointで色が薄くなる | -pix_fmt yuv420pなしで再エンコードしない;demo-reelの出力は既に正しい。 |
| テキストがわずかにぼやける | --pngでロスレスフレームをキャプチャ(遅い、ファイルサイズ大)。 |