製品デモ動画 は何をしますか?
静的な製品スクリーンショットやデザインを、洗練されたアニメーションデモに変換します — スクリーンレコーダー不要、不安定なリアルカーソルなし、UIバージョンのずれなし。各画面をフレームに収め、合成カーソルを正確なピクセルまで滑らかに移動させ、クリックでリップルを発生させ、重要な部分へズームし、機能をキャプションで説明します。入力が静的でモーションがコードで記述されているため、すべての値は決定論的であり、スクリーンショットが変更されても同一に再レンダリングされます。
使用するタイミング
- アプリ / SaaSデモ、機能発表、オンボーディングウォークスルー(15〜90秒)
- スクリーンショットやデザインしかないUIのクリックパスを示す場合
- 情報量の多いダッシュボードの1つの焦点領域を強調する場合(領域へのズーム+コールアウト)
スクリーンショットがスクリーンレコーディングより優れている理由
| スクリーンレコーディング | スクリーンショット駆動(このスキル) |
|---|---|
| 実際のカーソルがぶれる、行き過ぎる、ジッターする | カーソルはイージングされたパスに従って正確なターゲットに移動する |
| 1つのステップを修正するために全体を再録画する必要がある | 1つのPNGを交換し、同じコードを再レンダリングするだけでよい |
| 途中でUIが変わる、マウスが迷う | 各画面はクリーンな最終状態のアセットである |
| ピクセル化せずにズームするのは難しい | スクリーンショットを2倍(DPR 2)で書き出す → 鮮明なズーム |
入力は表示サイズの2倍(Retina / deviceScaleFactor: 2)でキャプチャし、2倍ズームでも鮮明に保ちます。Playwrightのキャプチャスクリプトについては references/screenshot-demo.md を参照してください。
デモのループ
すべてのデモは同じビートを、機能ごとに繰り返します。十分に読める時間保持し、意図を持って動き、クリックを決着させ、結果を表示します。
| ビート | 時間配分 | 役割 |
|---|---|---|
| 画面に落ち着く | 0.5〜1秒 | 何かが動く前に、視聴者が方向を確認できるようにする |
| カーソルがターゲットに移動 | 0.4〜0.8秒 | イージングされた移動(イーズインアウト)、線形ではない |
| クリックリップル+状態変化 | 0.2〜0.3秒 | 触覚的なフィードバック。同じフレームで画面が反応する |
| 焦点領域にズーム | 1〜2秒(イン)、2〜4秒(保持) | 2倍が最適 — 詳細を十分に表示し、コンテキストを保持する |
| 機能をキャプション | ズームと同時に保持 | 1つのベネフィットフレーズ、例:「ワンクリックエクスポート」 |
カメラまたはカーソルのいずれかを動かし、両方を同時に動かすことは避けてください — 2つの同時モーションは注意力を分散させます。
スクリーンショットをフレームに収める
生のスクリーンショットはバグレポートのように見えますが、フレームに収めると製品のように見えます。すべての画面を、ブランドカラーの背景上にブラウザまたはデバイスのクロムでラップします。
// ブラウザフレーム — スクリーンショットを挿入すると、信頼できるウィンドウになる
const BrowserFrame = ({ url, children }) => (
<div style={{ borderRadius: 12, overflow: "hidden", background: "#1e1e22",
boxShadow: "0 40px 80px rgba(0,0,0,.35)" }}>
<div style={{ height: 40, display: "flex", alignItems: "center", gap: 8, padding: "0 14px" }}>
{["#ff5f57", "#febc2e", "#28c840"].map((c) => (
<span key={c} style={{ width: 12, height: 12, borderRadius: "50%", background: c }} />
))}
<div style={{ marginLeft: 12, flex: 1, height: 24, borderRadius: 6, background: "#2b2b30",
color: "#9aa", fontSize: 13, display: "flex", alignItems: "center", padding: "0 10px" }}>
{url}
</div>
</div>
<div style={{ background: "#fff" }}>{children}</div>
</div>
);
アドレスバーに実際のURLを入れ(信頼性)、ソフトシャドウとグラデーション背景を追加します。モバイルの場合は、ノッチと丸みを帯びたコーナーを備えた電話フレームに置き換えます。すべての画面が一致するように、フレームスタイルを1つのテーマオブジェクトに保持します。
アニメーションカーソル+クリックリップル(中核動作)
位置は、CSSトランジションや実時間ではなく、常に現在のフレームから駆動します — 動画レンダラーはすべてのフレームがフレーム番号のみの純粋関数である必要があります。そうしないと、カーソルが同期せずちらつきます。
import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
// `from` から `to` へのイージングされた移動(startFrameからstartFrame+durの間)
const useCursor = (from, to, startFrame, dur) => {
const frame = useCurrentFrame();
const t = interpolate(frame, [startFrame, startFrame + dur], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => x * x * (3 - 2 * x) }); // smoothstep
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
};
export const Cursor = ({ from, to, startFrame, dur, clickAt }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { x, y } = useCursor(from, to, startFrame, dur);
// リップルはクリックフレームで1回だけ拡張
const since = frame - clickAt;
const showRipple = since >= 0 && since < 18;
const r = spring({ frame: since, fps, config: { damping: 18 }, durationInFrames: 18 });
return (
<>
{showRipple && (
<div style={{ position: "absolute", left: x, top: y, transform: "translate(-50%,-50%)",
width: 12 + r * 60, height: 12 + r * 60, borderRadius: "50%",
border: "2px solid rgba(80,140,255,.9)", opacity: 1 - r }} />
)}
<svg width="28" height="28" viewBox="0 0 28 28"
style={{ position: "absolute", left: x, top: y, filter: "drop-shadow(0 2px 3px rgba(0,0,0,.4))" }}>
<path d="M3 2 L3 21 L9 15 L13 24 L16 23 L12 14 L20 14 Z" fill="#fff" stroke="#222" strokeWidth="1.4" />
</svg>
</>
);
};
カーソルはスムーズステップを使用するため、静止状態から加速し、ターゲットに減速しながら到達します(線形カーソルはロボットのように見えます)。同じ clickAt フレームでリップルと画面の状態変化の両方を発生させ、原因と結果を一致させます。複数の画面にわたって複数のクリックを配線する完全なコンポジションについては、references/screenshot-demo.md を参照してください。
領域へのズーム(UI用Ken Burns効果)
焦点要素にズームインするには、画面を拡大し、transform-origin をその要素の中心に設定します。これにより、フレームの中心ではなく、その要素に焦点が合ったズームになります。
const ZoomToRegion = ({ target, scale = 2, startFrame, dur, children }) => {
const frame = useCurrentFrame();
const s = interpolate(frame, [startFrame, startFrame + dur], [1, scale],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => 1 - Math.pow(1 - x, 3) }); // easeOutCubic
return (
<div style={{ transform: `scale(${s})`, transformOrigin: `${target.x}px ${target.y}px`,
willChange: "transform" }}>
{children}
</div>
);
};
ズームは2倍程度に制限します(3倍を超えるとコンテキストが失われ、方向感覚を失います)。ズーム状態を2〜4秒保持し — 詳細を読むのに十分な時間 — その後、次の画面に移動する前にイージングして戻します。ズームは必ずコールアウトと組み合わせ、単独では使用しません。
コールアウト注釈と機能キャプション
変化点に注釈を付け、その後消去します。コールアウトは、ラベルと、焦点要素を指すコネクタ(矢印、円、または下線)で構成されます。
- 要素を強調するには、ソフトなリングまたは暗くした周囲を使用します(
box-shadow: 0 0 0 9999px rgba(0,0,0,.45)でスポットライトの穴を開けます)。 - ビートごとに1つのベネフィットをキャプションします — 短い名詞句(「リアルタイム分析」)、文ではありません。ズームと同時に表示され、次のステップの前に消えます。
- キャプションは画面下部3分の1、かつタイトルセーフ領域内に配置し、9:16へのクロップに耐えられるようにします。
スポットライトマスク、矢印コネクタ、タイプ付きキャプショントラックについては references/annotation-and-transitions.md を参照してください。
画面遷移
ステップ間では、1つの一貫した言語で画面を移動します。1つを選択し、それを維持します。
| トランジション | 印象 | 用途 |
|---|---|---|
| プッシュ / スライド | 「次のステップ」への前進感 | 線形ウォークスルー |
| クロスズーム | 勢い、アプリらしさ | 詳細ビューへのジャンプ |
| クロスフェード | 穏やか、ニュートラル | コンテキスト切り替え(設定→ダッシュボード) |
| 共通要素を使用したマッチカット | シームレス | 同じカードが画面間で持続する |
遷移元の画面は、そのクリックが解決されるまで保持し、その後トランジションします — 動きの途中でカットしないでください。references/annotation-and-transitions.md を参照してください。
出力チェックリスト
- 入力は2倍(DPR 2)でキャプチャされ、2倍ズームでも鮮明さを保つ。
- すべてのカーソル/ズーム値は
useCurrentFrame()の純粋関数 — アニメーション状態にCSSトランジションを使用しない。 - カーソルはイージングされたパス(スムーズステップ)で移動。リップルと状態変化は同じクリックフレームで発生。
- 各画面はフレーム(ブラウザ/デバイス)に収められ、実際のURLと一貫したテーマを持つ。
- 一度に1つのモーション(カメラまたはカーソル)。ズームは2倍以下、2〜4秒保持。
- ビートごとに1つのベネフィットキャプションを、タイトルセーフな画面下部3分の1に配置。トランジションは1つの一貫した言語を使用。
納品と検証(レンダリング静止画 → MP4)
パッケージ化されたヘルパー(
scripts/):scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.pngで静止画をタイル表示し、scripts/probe-mp4.sh out.mp4 [WxH] [fps]でエンコードを検証します。詳細はscripts/README.mdを参照してください。
これは重い作業です。成果物はMP4であり、ライブページではありません。デモはユーザーのアセット(スクリーンショット/デザイン、オプションでロゴ)を取り込むため、検証は主に 正しい画像が実際に読み込まれ、フレーム内に収まっているか を確認することになります。
出力契約:
- コンポジションが登録されたRemotionプロジェクト(
<Composition>+ zodschema+defaultProps)。すべてのカーソル/ズーム/トランジション値はuseCurrentFrame()の純粋関数(CSSトランジション、タイマー、Date.now()は不可)。 - スクリーンショットは
staticFile()を介して読み込まれ、delayRender/continueRenderでゲート制御され、2倍のPNG(および任意のWebフォント)がフレームレンダリング前に存在することを保証します。そうしないと、デモ中に画面が空白でポップインします。 - 成果物 = レンダリングされた
out/*.mp4とプロジェクト(スクリーンショット変更時に再レンダリング可能)。
検証ループ — 静止画レンダリング → 検査 → エンコード。 まずは軽量なPNGで確認し、正しくなってから動画をエンコードします。
# 実際に使用するプロパティ(実際のスクリーンショットパス)でフレーム正確な静止画を生成。defaultProps は使用しない。
npx remotion still ProductDemo out/f-settle.png --frame=15 --props=demo.json # 画面フレーム、移動前
npx remotion still ProductDemo out/f-click.png --frame=90 --props=demo.json # カーソルがターゲット上+リップル
npx remotion still ProductDemo out/f-zoom.png --frame=150 --props=demo.json # 焦点領域にズーム+キャプション
# 最終フレーム = durationInFrames - 1(npx remotion compositions が読み取る)
各PNGを以下の点について検査します:忠実度(正しいスクリーンショットが読み込まれているか、カーソルが正確なピクセル上にあるか、クリックリップルと状態変化が同じフレームで発生しているか、キャプションテキストが正しいか)およびアーティファクト(画像が空白/読み込まれていない、画面がキャンバス外、ズームがピクセル化またはフレームを超えてオーバーシュートしている、キャプションがタイトルセーフな画面下部3分の1の外にある、アスペクト比/レターボックスが間違っている)。
# 静止画がクリーンになってからのみ:
npx remotion render ProductDemo out/demo.mp4 --props=demo.json
npx remotion render ProductDemo out/demo.gif --props=demo.json --codec=gif # READMEの最初の画面の証拠
バッチ処理(1つのテンプレート、多数の製品): 製品ごとにデモの外観を変更する場合、カタログを一括レンダリングする前に、代表的な1つの製品のプロパティを静止画で検証します — 空白画像やキャンバス外のバグをN回ではなく1回でキャッチします。
終了前の確認事項:
- 静止画がスムーズにレンダリングされる(settle/click/end) — エラーなし、スクリーンショットが実際に読み込まれている(空白ではない)。
- カーソルが正確なターゲットに到達している。リップル+状態変化が同じフレームで発生。キャプションテキストが正しく、タイトルセーフな画面下部3分の1にある。
- すべてのモーションがフレーム駆動 — CSSトランジション/タイマー/
Date.now()/Math.random()は使用されていない。 - 出荷するプロパティ(実際のスクリーンショットパス)が正しくレンダリングされる(
defaultPropsだけでなく)。 - 完全なMP4がエンコードされ再生される。(オプション)README用にGIFがレンダリングされている。
参考ファイル
references/screenshot-demo.md— 完全な実行可能なRemotionデモ:Playwrightを使用した2倍スクリーンショットのキャプチャ、ブラウザ/電話フレームコンポーネント、複数の画面にわたってカーソルをクリックリップルとともに移動させるマルチステップコンポジション、およびレンダリングコマンド。references/annotation-and-transitions.md— スポットライトマスク、矢印/円のコールアウトコネクタ、タイプ付き機能キャプショントラック、4つの画面遷移(フレーム駆動実装付き)、およびマルチアスペクト(16:9 / 9:16)セーフエリアに関する注意事項。