エンゲージングな説明動画
ワンカットのビジュアルストーリー。1つの主人公要素(参考スタイルでは、トグルのノブとして始まる白いボール)が、小さなインターフェース「ステーション」の連鎖を旅し、各ステーションがそれに反応します。スライダーに沿って転がり100まで押し上げ、端から落ちて通知スタックに跳ね返りながら通過し、キーボードに着地してキーからキーへ跳ねながら単語をタイプし、リターンキーを押して空中に打ち上げられます。カメラは決してカットしません。主人公を画面中央付近に捉え、各インタラクションではズームインし、クライマックスでは大きく引きで全体のコースを一望させ、その後再びスタート地点にダイブします。
商品の場合、ステーションはワークフローの各ステップを1つのキャンバス上に空間的に配置したもので、主人公はそのワークフローを移動するものになります。引きのショットは「これで仕組みがわかった」という瞬間です。
これは1つのHTMLページとして記述し、時間値から任意のフレームをレンダリングし、モーションブラーとサウンドトラック付きでMP4にレンダリングします。
ステップ1:概要を把握する(1回のメッセージで、その後は自律的に)
1回のメッセージですべてを質問し、その後は自分で作業を進めてください。製品情報のみ必須です。それ以外の項目が空欄の場合は、自分で決定し、選択した内容を明示してください。
- 製品:名前、およびそれが何をするかを数文で。WebサイトのURLがあればさらに良いです。
- 視聴者が覚えるべきたった1つのこと(例:「アイデアを入力するだけで、ライブアプリができる」)。
- ブランド:色、フォント、ロゴファイル、または「Webサイトを使用」。指定がない場合は、以下のデフォルトパレットを使用します。
- フォーマット:16:9(1920×1080)または9:16(1080×1920)。デフォルトは16:9です。
- 長さ:デフォルトはこのスタイルについて以下に示されています。
- 音楽:ユーザー自身のトラック(ファイルパス)、またはシンセサイズ。デフォルト:シンセサイズ。
- ディレクション(オプション):希望するシーン、タグライン、コールトゥアクション、避けたいことなど。
URLが指定された場合は、何かを書く前に(Webフェッチで)製品の実際の文言、機能、色、フォント名を読み取ってください。UIラベルやタグラインには、実際の文言を使用してください。
決して事実を捏造しないでください。 でっち上げの顧客数、収益、評価、お客様の声、引用は禁止です。数字は、ユーザーまたはWebサイトが明記している場合のみ使用してください。それ以外の場合は、相対的なバー、進捗、プレースホルダのスケルトン行を使用します。UI内部のサンプルコンテンツ(プロンプト、ファイル名、プロジェクト名、レシートの価格など)は問題なく、信憑性があるべきですが、製品自体に関する主張(統計、ユーザー数、評価、「無料」などのオファー)は許可されません。コールトゥアクションは、ユーザーまたはWebサイトが提供したURLまたはオファーのみを使用します。提供がない場合は、名前とタグラインで終了します。
ロゴ。 ユーザーがファイルを提供した場合、またはそれがユーザー自身の製品でありユーザーが要求した場合にのみロゴを使用します。それ以外の場合は、ブランドフォントで名前をテキストとして記述します。サードパーティのロゴ(連携サービス)は、提供された場合のみ使用します。それ以外の場合は、ニュートラルな丸いタイルに名前を表示します。
また、以下も質問します(オプション):3~6ステップのワークフロー(ユーザーの操作 → 製品の動作 → 結果)。URLが指定された場合は、サイトからそれを導き出します。
このスタイルのデフォルトの長さ:12秒、シームレスループ。範囲は10~30秒(長い = より多くのステーション、ステーションの速度が遅くなるわけではありません)。
ステップ2:コースを設計し、構築する
1. 主人公を選ぶ
主人公は、製品のストーリーの中心となる単純で読みやすいオブジェクトであり、決して消えたり置き換えられたりしません(瞬間的に形状が変わることがあっても、元に戻ります):ボール/ノブ(デフォルト、何にでも使える)、テキストキャレットまたはカーソル、ファイルまたはドキュメントタイル、メッセージバブル、コインまたは注文、データポイント。ステーションのインターフェースに自然に適合する必要があります(ノブ、サム、キーキャップ、ピンなど)。
2. ワークフローをステーションにマッピングする
各ステップは1つのステーションになります。つまり、主人公が物理的にインタラクトするコンパクトな実際のインターフェース要素であり、その結果目に見えて変化するものです。以下のメカニクスから描画します。
| メカニクス | 主人公が... | ステーションの反応 |
|---|---|---|
| トグル / ボタン | ノブになる、または押す | スイッチがオンになり、色が塗りつぶされる |
| スライダー / 進捗 | サムとしてそれに沿って転がる | ツールチップに値がカウントアップされ、トラックが塗りつぶされる |
| 通知スタック | 各カードに落下し跳ね返る | カードがヒットされるたびにポップインし、ストーリーを語るメッセージが表示される |
| キーボード / 入力 | キーからキーへ跳ねる | 各キーが押し下げられ、上の入力欄に文字が表示される |
| リスト / テーブル | 行をスライドダウンする | 各行がハイライト、チェック、または並べ替えられる |
| チャート | 線の先端の点になる | 背後に線が描画され、通過するにつれて棒が上昇する |
| チャット | バブルとして乗り込む | 返信がタイプアウトされる |
| デプロイ / 送信ボタン | それによって打ち上げられる | トーストまたはURLが表示される |
ステーションのラベルとメッセージは、それぞれ数語で、いくらか機知を交えてストーリーを語ります(参考例では Slider · Reached 100、Gravity · Something is falling、Messages · What now? を使用し、その後 retry とタイプします)。製品の場合は、実際のワークフローから作成します(例:コーディングツールの場合:プロンプト入力 → Agent · Writing 12 files → 進捗 → Preview · Ready → デプロイ押下 → Live at myapp.app)。
3. 世界をレイアウトする
主人公が訪れる順にステーションを1つの大きなキャンバス(フレームの約3倍)に配置し、パスがジグザグになるようにします(上部を右に、中央を下に、下部を横切る)。重力は理にかなっている必要があります。落下は下向き、打ち上げは上向きです。各ステーションのスポットはビデオ全体で固定しておきます。世界は暗いキャンバスで、カメラと共に移動するかすかなドットグリッドがあります(このグリッドがカメラの動きをわかりやすくします)。
4. 主人公のパスをスクリプト化する
主人公の位置は時間の純粋な関数であり、接触点間のセグメントから構築されます(各接触はビート上にあります)。
- 転がる/スライドする ステーションに沿って:トラックに沿った
easeまたはスプリングによる位置。ステーションの値は主人公の位置から導出されます。 - 飛び跳ねと落下 接触点間:放物線(
xは時間に対して線形、y = y0 + v·t + ½·g·t²、ビートで次の接触点に着地するようにvを解く)。重力はビデオ全体で一定。 - 衝突: 接触時のスカッシュ(約60ms間、スケール1.25 × 0.8、スプリングで戻る)、飛行中の速度に沿ったストレッチ(最大1.15 × 0.9)、さらに衝突したものへの小さな押し下げ。
- 打ち上げ: 最後のステーションが主人公を打ち上げ、1つの長い弧を描いてスタート地点に向かって戻します。その間、すべてのステーションが背後で逆順にリセットされます(値がカウントダウン、通知が消え、入力がクリアされる)。これによりループが閉じます。打ち上げ弧はスローモーションで実行します。まず希望する頂点の高さ(コースの最上部)を選択すると、重力下での実際の飛行時間が決まります。次に、その飛行が表示ウィンドウを満たすように時間をワープさせ(最も遅いのは頂点で、多くの場合5~10倍; 例:弧上の
1 − 0.9·sin(πu)^0.6の速度)。ステーションのリセットは引きのホールド中、視聴者がコース全体を見た後に行うようにスケジュールし、打ち上げの瞬間に行わないでください。 - シーム: 主人公が最初のステーションに着地する点にループポイントを設定します。
seek(duration)はseek(0)とまったく同じに見える必要があります。着地のスカッシュとキー押下も時刻0で表示されます。サウンドについては、buildAudio内でサウンドトラックを自分自身の少し長いOfflineAudioContextにレンダリングし、durationを超えたテールを開始点に折り返し、そのバッファを指定されたコンテキストで再生します。 - 重力は横から見たもの。 落下が横から見て理にかなうようにステーションをレイアウトします。通知カードは階段状になります(各カードは幅以上の間隔で横にオフセットされ、主人公がカードを通過しないようにする)。キーボードは正面から見え、主人公は小さな弧を描いてキー間を跳ねます。
- 空のスロット: 主人公がステーションの一部(ノブ、サム)だった場所を離れると、ステーションはそれが残したスロットの薄暗い輪郭を保持し、主人公が戻ると再び埋まります。
5. カメラ
カメラが難しい部分です。他の何かを磨く前に、これを正しく設定してください。
- 追従: カメラターゲット = 主人公の位置(その速度の約0.15秒先のリードを加えたもの)と、現在のステーションの固定されたアンカーのブレンドで、臨界減衰スプリング(
w≈ 6)で滑らかにされ、衝突時にガクつかないようにします。インタラクション中はアンカーへの重みを約0.7(ステーションがフレームに収まる)、落下や跳躍中は0.15(主人公が先行する)、ペイオフホールド中は0.85にします。スムージングには履歴が必要なため、ロード時にベイクします(ページコントラクトを参照)。時刻0から最初のステーションの固定フレーミングで開始し、最初の0.8秒間で追従にブレンドします。主人公がフレームの中央半分から外れることはありません。 - ズーム: 各インタラクション中はタイトに:ステーションがフレーム幅の約半分を占め、主人公は画面上で50~70 px(1080pの場合)。落下や跳躍中はやや広めに、次のステーションがすでに見えるようにします。空のキャンバスではなく、ステーションをフレーミングします。
- ペイオフホールド: 打ち上げの前に、最終結果(トースト、ライブURL)を0.4~0.6秒間、フレーム内に完全に収めてホールドします。
- 引きのショット: 打ち上げ時に、カメラは追従を停止し、コース全体が収まる固定フレーミング(約0.3倍)にイーズアウトし、奥行きのためにわずかな3D傾き(
perspective+ 15~25°のrotateX)を加えます。主人公がその上を弧を描く間、約1秒間ホールドします。主人公を拡大し、柔らかなハローを与えて、画面で最も明るく最も目立つもの(少なくとも24 px)にします。その後、主人公がそこに着地するタイミングで最初のステーションの固定フレーミングにダイブし、再び追従にブレンドします。 - 本物のモーションブラー:主人公と世界は速い動きでスミアします。それがルックの一部です。
ルック
- デフォルトパレット(ダークiOS風):キャンバス
#0C0C0E、ドットグリッド#1E1E22、サーフェス#1C1C1F/#2C2C2E、テキスト#F2F2F7、セカンダリ#8E8E93、アクセント#0A84FF、主人公 白#FFFFFF。ブランドカラーがアクセントを置き換えます。主人公は画面上で最も明るいままです。ブランドが明るい場合は、明るいバリエーション(キャンバス#F2F2F4、主人公 = アクセント)でも問題ありません。 - 本物そっくりのシステムUI:トグル、目盛り付きスライダー、アプリアイコン四角、タイトル、本文行、
nowを含む通知カード。キーキャップとリターンキーを備えたキーボード。フォント:SF風(Inter または Geist)。 - カーソルがチェーンを開始する場合があります(最初のステーションをクリックし、その後は邪魔にならないようにします)。
- 禁止: カット、フェードトゥブラック、何が起こっているかを説明するテキストオーバーレイ(ステーション自体のラベルのみがテキスト)、複数の主人公、反応しないステーション、浮遊感のある物理演算。
サウンド
タイトでミニマルなグルーヴ、110~120 BPM(柔らかなキック、クリッキーなハイハット、ミュートされたプラックベース)、そして主人公の接触が主要な楽器です。各衝突は短いピッチのチックまたはマリンバノートを再生し(ストーリーが進むにつれてピッチが上がる)、キー押下はクリック音、スライダーの動きは値と共に速くなる細かいチック音、通知は柔らかなポップ音、打ち上げは上昇のウィッシュ音、ダイブは下降のウィッシュ音を再生します。すべての音が視覚的な接触と正確に一致します。
ページの構築方法
1つの自己完結型HTMLファイル(インラインCSSとJS。画像はユーザーが提供した場合のみ)。DOM + CSSトランスフォーム、SVG、または1つのフルスクリーンキャンバスで描画します。
ページコントラクト
window.VIDEO = {w, h, duration}およびwindow.seek(t)、時間の純粋な関数:すべての要素を時間t(秒)に対して設定します。CSSトランジションやアニメーション、setTimeout、前のフレームからの状態の引き継ぎは禁止です。任意のフレームが、任意の順序で、単独で正しくレンダリングされる必要があります。seek内でのレイアウト測定(例:offsetLeft、measureText)は問題ありません。決定論的です。- ランダム性(グレイン、パーティクル、オーディオのノイズ)は、シード生成ジェネレータから取得し、決して
Math.randomからは取得しません。 - 履歴を必要とするもの(動く物体に遅れて追従するカメラ、シミュレーション)は、ロード時に一度ベイクします。0から
durationまで1/240秒刻みでステップ実行し、値を保存し、seekが補間してそれらを読み戻すようにします。ループの場合、ループが開始する状態からシミュレーションを開始します(スタイルのシームルールを参照)。 - ループ:
seek(t)はt % durationを使用します。シームをまたがるエフェクト(着地のスカッシュ、サウンドテール)は両端で描画またはミックスされます。 ?renderがURLにある場合:コントロールなし、プレビュースケーリングなし。ステージは正確にw × hで左上に配置されます。それ以外の場合:ステージはCSSでウィンドウに合わせてスケーリングされ、プレビューコントロールが表示されます。- フォント:埋め込んでください。 woff2ファイル(Google Fontsまたはブランドのもの)をダウンロードし、base64
@font-faceとしてインライン化します。これにより、レンダリングが静かにフォールバックすることがなくなります。ウェイトの変形には、可変フォント([email protected])を使用します。 - レイヤー: シーンレイヤーに透明な背景を与え、その背後にある1つのステージ背景がベースカラーを提供するようにします。これにより、後のレイヤーが誤って前のレイヤーを隠すことを防ぎます。絶対配置のテキストには
white-space: nowrapを設定します。 - 速度: 大きいCSS
filter: blur()と大きなぼかしグラデーションは、スクリーンショットの速度を非常に低下させます。グラデーションとグローは、ロード時にキャンバスに一度描画するか(または画像として)、それを移動させます。
ヘルパー:以下を貼り付け、その上にすべてを構築します
// damped spring step response 0 -> 1. w = speed (8 slow, 14 normal, 22 snappy), z = damping (1 = no overshoot)
function spring(t, w = 14, z = 0.82) {
if (t <= 0) return 0;
if (z >= 1) return 1 - Math.exp(-w * t) * (1 + w * t);
const wd = w * Math.sqrt(1 - z * z);
return 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + (z * w / wd) * Math.sin(wd * t));
}
// a value that changes target many times, springing to each: track(start, [[time, value, w?, z?], ...]) -> f(t)
const track = (v0, keys) => t => { let v = v0, p = v0; for (const [tk, val, w, z] of keys) { v += (val - p) * spring(t - tk, w, z); p = val; } return v; };
// precise eased keyframes (best for camera moves): kf([[t0, v0], [t1, v1, easeFn?], ...]) -> f(t)
const kf = keys => t => { if (t <= keys[0][0]) return keys[0][1]; for (let i = 1; i < keys.length; i++) { const [a, va] = keys[i - 1], [b, vb, e = ease] = keys[i]; if (t <= b) return lerp(va, vb, e((t - a) / (b - a))); } return keys[keys.length - 1][1]; };
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
const ease = x => (x = clamp(x), x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); // easeInOutCubic
const lerp = (a, b, k) => a + (b - a) * k;
const seg = (t, a, b) => clamp((t - a) / (b - a)); // 0..1 progress of t through [a, b]
const rng = s => () => (s = (s +