ハイエナジー・エクスプレイナー
12~20秒の、ショーリールのように印象的なハイプ作品。0.5秒ごとに何かがビートに乗って現れますが、ランダムなカットには決して見えません。なぜなら、一つのシェイプがすべてのトランジションを通して視線を誘導するからです。スパークが線になり、その線が画面を埋め尽くして次の背景になり、終止符が分離して次のシーンの中心となるボールになります。
これは1つのHTMLページとして記述され、時間の値から任意のフレームをレンダリングし、モーションブラーと合成されたサウンドトラックとともにMP4にレンダリングします。
ステップ1: ブリーフを入手する(メッセージ1回で、あとは実行)
質問はすべて1回のメッセージで行い、その後は自分で作業を進めてください。必要なのはプロダクト情報のみです。空白の項目については、自分で決定し、何を選んだかを伝えてください。
- プロダクト: 名前と、その機能を数文で。ウェブサイトURLがあればなお良いです。
- 視聴者が覚えるべきこと: (例:「アイデアを入力するだけで、すぐに使えるアプリができる」)。
- ブランド: 色、フォント、ロゴファイル、または「ウェブサイトを使用」。何もない場合は、以下のデフォルトパレットを使用します。
- フォーマット: 16:9(1920×1080)または9:16(1080×1920)。デフォルトは16:9です。
- 長さ: デフォルトはこのスタイルについて以下に示されています。
- 音楽: ユーザー自身のトラック(ファイルパス)、またはシンセサイズさせるか。デフォルトはシンセサイズです。
- ディレクション(オプション): 希望するシーン、タグライン、コールトゥアクション、避けたいことなど。
URLが与えられた場合は、何かを書き始める前に、製品の実際の文言、機能、色、フォント名を読み取ってください(Webフェッチ)。UIラベルやタグラインには、実際の文言を使用してください。
事実を捏造しないでください。 顧客数、収益、評価、推薦文、引用など、架空のものは作成しないでください。数字は、ユーザーまたはウェブサイトが明記している場合のみ使用します。それ以外の場合は、相対的なバー、進行状況、プレースホルダーのスケルトンラインを使用します。UI内のコンテンツ例(プロンプト、ファイル名、プロジェクト名、レシートの価格など)は問題なく、信憑性があるべきです。製品自体に関する主張(統計、ユーザー数、評価、「無料」などのオファー)は許可されません。コールトゥアクションには、ユーザーまたはウェブサイトが提供したURLまたはオファーのみを使用します。何もない場合は、名前とタグラインで終了します。
ロゴ。 ユーザーがファイルを提供した場合、またはそれがユーザー自身の製品であり、ユーザーがロゴ使用を依頼した場合にのみロゴを使用します。それ以外の場合は、ブランドフォントで名前をテキストとして記述します。サードパーティのロゴ(統合など)は、提供された場合のみ使用します。それ以外の場合は、名前が入ったニュートラルな角丸タイルを使用します。
このスタイルのデフォルトの長さ: 15秒(128 BPM、8小節)。範囲は12~20です。
ステップ2: シーンを計画し、構築する
構造(6~8シーン、各1.5~3秒、120~140 BPMのグリッド上)
以下のシーンタイプから選択し、製品にマッピングします。まずビートシートを作成します(シーン、ビートグリッド上の開始時間、何が入ってくるか、何が出ていくか、サウンド)。
- スパークで開く(暗い)。ほぼ黒のキャンバスにかすかなグリッド。小さなアクセントマークが現れ、脈動し、フレーム全体に細く水平な線に伸び、小さなモノラルラベル(
product / launch — 2026)が付き、一度揺れた後、その線は垂直方向に広がり、アクセントカラーで画面全体を埋め尽くします。そのフラッドが次の背景になります。 - ブランドスラム。太いグロテスク体の製品名、すべて大文字、巨大、連続する1/16ビートでマスクによって文字が表示され、その上に小さなモノラルキッカー(
INTRODUCING — 2026)と、下にイタリックのセリフ体のライン(それが何かを示す、2~4語)があります。ウェイトは細いものから黒いものに変化します。終止符または文字のドットが分離して成長し、次のシーンを開始するシェイプになります。 - キネティックトリプレット。価値提案を3つの短い単語またはフレーズで表現、それぞれ2ビートずつ(手書きの単語の場合は3ビート、読めるように)、それぞれに異なる処理と背景があり、ビートに合わせてハードに切り替わります(このシーン内での切り替えはリズムなので、カットとしてカウントされません)。暗い背景にクリーム色、黒の背景にアクセントカラーで単語が輪郭パターンとしてフレームを埋め尽くす、電気的な青の背景に白でビューファインダーのコーナーブラケットと小さな
1920 × 1080ラベルが付く、最後の単語は手書きスクリプトで自動描画される。各単語は高速ブラー/スライドで入り、わずかにオーバーシュートして着地します。 - シグネチャービジュアル。製品を象徴する「どうやって作ったんだ」と思わせる瞬間。中心から波打つキューブの3Dフィールド(独自の透視投影でキャンバスに描画、パレットの色で高さを着色)、凝縮してリングになり外向きに破裂するジェネレーティブパーティクルフロー、または形状間を変形する液体ブロブ。製品のアイデアに合うものを選んでください(構成要素→キューブ、データ/AI→パーティクル、柔軟性→液体)。再生中にカメラがゆっくりと傾いたり周回したりします。
- プロダクトマイクロモーメント。製品からの実際のインタラクションを、浮かぶUIカードとして表示。3Dで少し傾き、その後ろに2枚のゴーストカードが重なっています。カーソルがトグルをはじき、メインボタンを押し、プログレスバーがアクセントカラーでパーセンテージとともに埋まり、ボタンが
Done ✓に変わり、トーストが右上にスライドインします。製品の実際のラベルを使用してください。 - 証明(実際の数字が存在する場合のみ)。大きなカウントアップ(
+312%スタイル、数字が回転中にブラー)、ドーナツリング、千鳥状に上昇するバーと一つのアクセントバー、それらに重なる線画とツールチップ。実際の数字がない場合→このシーンをスキップするか、製品の実際の機能をリストとして表示し、それを叩き込みます。 - リキッド/モーフブリッジ。前のシーンのシェイプがドットに収縮し、ドットが暗い背景のリングパルスになり、それがロゴのドットになります。
- エンドカード(2秒間保持)。ロゴまたは名前とアクセントドット、イタリックのセリフ体タグライン、文字ごとに入力されるトラッキングされたモノラルライン(実際のURLまたはコールトゥアクションが与えられている場合はそれ。ない場合はブリーフからの短い行)、ヘアラインルール。
すべての製品にすべてのシーンが必要なわけではありません。6つの強力なシーンは8つの弱いシーンに勝ります。1シーンにつき1つのアイデアにしてください。
高級感を演出するルール
- トランジションはオブジェクトであり、カットではない。 すべてのシーンは、シェイプ(フラッド、成長するバーによるワイプ、次の背景に成長するドット、カメラをズームして通過する単語)を介して次のシーンに引き継がれます。シーン間のハードカットは、ダウンビートでのみ、最大2回まで許可されます。
- すべてがグリッドに乗っている。 大きな変化はビート上、タイポグラフィは1/8または1/16音符で。インパクトのためにわずかにオーバーシュート(
z≈ 0.6~0.7)し、その後落ち着きます。 - すべてのシーンにHUDフレーム: 細いコーナーブラケット、右上に
SCENE 02 / 07、左下に小さなモノラルでカテゴリラベル(KINETIC TYPE、PRODUCT、DATA)、右下に進行中のタイムコードと赤いドット。小さい(1080pで12~14 px)で約50%の不透明度。カメラのビューファインダーのように感じられる必要があります。 - テクスチャ: すべてのフラットな背景にソフトなラジアルビネット、非常に微妙なフィルムグレイン(ロード時にシード付きグレインタイルをいくつか事前生成して循環させる)、およびリアルなモーションブラー。
- タイプ: スラムには太いグロテスク体(例: Inter Tight 900、Archivo Black)、人間的なラインにはイタリックのセリフ体(Instrument Serif italic)、HUDとキャプションにはモノラル(JetBrains Mono、Geist Mono)。ブランドにフォントがある場合は、名前にはブランドのフォントを使用し、HUDにはモノラルを維持してください。
- デフォルトパレット: インク
#0B0B10、アクセント#FF4F1A、クリーム#EDEBE4、エレクトリックブルー#2F47FF、ホワイト。ブランドのメインカラーがアクセントを置き換え、セカンドカラーが青を置き換えます。1色のブランドは、その色の濃い色合いをセカンドカラーとして使用します。 - 禁止: ストックテンプレート的な見た目、すべてにグロー、絵文字、1つのシーンで3色以上(黒と白は除く)、一目で読めない小さなテキスト、動きのないシーン。
サウンド
128 BPM(またはユーザーのトラック)。パンチのある: すべてのビートでキック、2と4でクラップ、シーンのルートに従うサブベース、ブランドスラムとエンドカードへのフィルターされたライザー、各フラッドへのインパクト(低ブーム+ノイズバースト)、各タイプされる単語への短いトーンブリップ、すべてのキャリーされるトランジションへのウィッシュ(バンドパスされたノイズスイープ)、エンドカードへのソフトなリングアウト。ヒットの下で音楽をミックスします。
ページの構築方法
1つの自己完結型HTMLファイル(インラインCSSとJS。ユーザーが提供した場合のみ画像)。DOM + CSS transforms、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を使用します。シームにまたがるエフェクト(着地のスカッシュ、サウンドテール)は、両端で描画またはミックスされます。 - URLに
?renderがある場合: コントロールなし、プレビュースケーリングなし。ステージは左上に正確にw × hで表示されます。それ以外の場合: ステージはCSSでウィンドウに合わせてスケーリングされ、プレビューコントロールが表示されます。 - フォント: 埋め込みます。 woff2ファイルをダウンロードし(Google Fontsまたはブランドのもの)、base64
@font-faceとしてインライン化し、レンダリングが静かにフォールバックしないようにします。ウェイトモーフの場合、範囲([email protected])を持つ可変フォントを使用します。 - レイヤー: シーンレイヤーには透明な背景を与え、その背後に1つのステージ背景がベースカラーを提供するようにします。これにより、後のレイヤーが誤って前のレイヤーを隠すことがなくなります。絶対配置されたテキストには
white-space: nowrapを設定します。 - 速度: 大きなCSS
filter: blur()と大きなブラーグラデーションは、スクリーンショットの速度を大幅に低下させます。グラデーションとグローは、ロード時にキャンバスに一度描画するか(または画像として)、それを移動します。
ヘルパー: これらを貼り付けて、すべてをそれらの上に構築してください
// 制振されたスプリングステップ応答 0 -> 1。w = 速度(8 遅い、14 普通、22 スナッピー)、z = ダンピング(1 = オーバーシュートなし)
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));
}
// ターゲットを何度も変更し、それぞれにスプリングする値: 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; };
// 正確なイージングキーフレーム(カメラ移動に最適): 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)); // [a, b] 間の t の 0..1 進行度
const rng = s => () => (s = (s + 0x6D2B79F5) | 0, s = Math.imul(s ^ (s >>> 15), 1 | s), s ^= s + Math.imul(s ^ (s >>> 7), 61 | s), ((s ^ (s >>> 14)) >>> 0) / 4294967296);
- タイピング: 表示される文字列は
full.slice(0, n(t))で、tからキャレットが点滅します。カウントアップ:lerp(a, b, ease(seg(t, …)))をフォーマットします。 - カメラ は
transform: translate() scale() rotate()を持つラッパー要素で、kfまたはtrackによって駆動されます。
サウンド
window.buildAudio(ctx)は、与えられたAudioContextまたはOfflineAudioContext上で、タイム0からサウンドトラック全体(音楽+エフェクト)をスケジュールし、ビジュアルと同じタイムライン番号を使用します。オシレーター、フィルターされたノイズ(シード付きバッファから)、エンベロープを使用してシンセサイズします。ユーザーが音楽ファイルを提供した場合は、fetch+decodeAudioDataでロードし、ビートグリッドをそのテンポに設定し(測定する)、その上にエフェクトのみを追加します。- プレビュー再生: 最初の再生時に、サウンドトラックを一度バッファにレンダリングし、その後任意のスクラブ位置から再生します。クロックは
seekを駆動します:
let buf, ac, src, startAt = 0, offset = 0, playing = false;
async function play() {
ac ??= new AudioContext();
if (!buf) { const off = new OfflineAudioContext(2, Math.ceil(48000 * VIDEO.duration), 48000); await buildAudio(off); buf = await off.startRendering(); }
src = ac.createBufferSource(); src.buffer = buf; src.connect(ac.destination);
src.start(0, offset % VIDEO.duration); startAt = ac.currentTime; playing = true;
}
function pause() { if (!playing) return; offset += ac.currentTime - startAt; src.stop(); playing = false; }
function now() { return playing ? (offset + ac.currentTime - startAt) % VIDEO.duration : offset; }
(function loop() { seek(now()); requestAnimationFrame(loop); })(); // スクラバーは offset を設定(一時停止、設定、再生)
- ビートシートをスクリプトの先頭にコメントとして配置します: 各モーメントの行に、その時刻、何が起こるか、どのサウンドかを記載します。
ステップ3: チェック、レンダリング、納品
ワーキングフォルダーで一度セットアップ: npm i playwright、続いて npx playwright install chromium(Chromiumがまだインストールされていない場合)。ffmpeg が PATH 上にある必要があります。このファイルの下部にスクリプトを render.mjs として保存します。
- スチル。
node render.mjs demo.html check.png --stills 0.5,1.2,…ビートシートの各行に対して1つの時刻を指定(シートは読みやすいサイズで、1行に3つ)。確認します。クリップ、オーバーラップ、読みにくい、ブランドから外れている、フレーミングが悪い、空であるものがあれば修正します。再度確認します。 - ドラフト。
node render.mjs demo.html draft.mp4 30 1 4(高速: 30 fps、モーションブラーなし、4つの並列ワーカー。最後の数値はマシンのコア数に設定)。これはレビューコピーです。ユーザーにHTMLとともに送信し、最終版の前に変更を依頼します。 - 最終版。
node render.mjs demo.html demo.mp4 60 8 8。モーションブラーはレンダリング時間をサブフレーム数倍にします。60秒の最終版はラップトップで20~60分かかる可能性があるため、開始する前にその旨を伝えます。- それは
<html>...</html>をdemo.htmlとして書き出し、render.mjsを実行し、出力を待ちます。最終版のチェックはしません(すでにドラフトで確認済み)。納品時に、ユーザーがファイルパスを指定できるように、出力ファイル、HTMLファイル、ログがあればそれらを添付します。 - ユーザーが音楽ファイルを提供した場合、
demo.mp4と同じフォルダーにその名前で保存し、スクリプトから読み込めるようにします。
- それは
render.mjs
import { chromium } from 'playwright';
import { execSync, spawn } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const [html, out, fps = '60', blur = '0', workers = '4'] = process.argv.slice(2);
if (!html || !out) {
console.error(`Usage: node render.mjs <file.html> <out.png|out.mp4> [fps=60] [subframes=0] [workers=4]
.png — still frame (requires --stills t1,t2,... as extra args)
.mp4 — video`);
process.exit(1);
}
const stills = process.argv.includes('--stills') ? process.argv[process.argv.indexOf('--stills') + 1].split(',').map(Number) : null;
const w = 1920, h = 1080;
const tmpdir = join(__dirname, '.render_' + Date.now());
mkdirSync(tmpdir, { recursive: true });
const htmlPath = join(process.cwd(), html);
if (!existsSync(htmlPath)) { console.error(`File not found: ${htmlPath}`); process.exit(1); }
async function main() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: w, height: h } });
await page.goto('file://' + htmlPath + '?render');
await page.waitForFunction(() => window.VIDEO && typeof window.seek === 'function');
await page.evaluate(() => { window.__seek = window.seek; });
if (out.endsWith('.mp4')) {
const f = Number(fps), blurFrames = Number(blur), wc = Number(workers);
const frames = Math.ceil((await page.evaluate(() => window.VIDEO.duration)) * f);
const chunkSize = Math.ceil(frames / wc);
console.log(`Duration: ${(await page.evaluate(() => window.VIDEO.duration)).toFixed(1)}s, Frames: ${frames}, Workers: ${wc}, Blur: ${blurFrames}`);
// Generate chunk lists
const chunks = [];
for (let i = 0; i < wc; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, frames);
if (start >= frames) break;
chunks.push({ idx: i, start, end });
}
// Render chunks in parallel
const renderFile = join(tmpdir, 'render%04d.png');
const promises = chunks.map(async (chunk) => {
const chunkDir = join(tmpdir, `chunk_${chunk.idx}`);
mkdirSync(chunkDir, { recursive: true });
for (let fi = chunk.start; fi < chunk.end; fi++) {
const t = fi / f;
await page.evaluate((t) => { window.__seek(t); }, t);
await page.screenshot({ path: join(chunkDir, `frame_${String(fi).padStart(6, '0')}.png`) });
if (fi % 100 === 0) process.stdout.write(`\rWorker ${chunk.idx}: ${fi - chunk.start}/${chunk.end - chunk.start}`);
}
process.stdout.write(`\rWorker ${chunk.idx}: ${chunk.end - chunk.start}/${chunk.end - chunk.start} done`);
console.log();
});
await Promise.all(promises);
// Generate ffmpeg concat file
const framesList = [];
for (let i = 0; i < frames; i++) {
for (const chunk of chunks) {
if (i >= chunk.start && i < chunk.end) {
framesList.push(`file '${join(tmpdir, `chunk_${chunk.idx}`, `frame_${String(i).padStart(6, '0')}.png`)}'\nduration ${1/f}`);
}
}
}
// Ensure last frame duration is correct for ffmpeg concat (approximate)
const concatFile = join(tmpdir, 'frames.txt');
writeFileSync(concatFile, framesList.join('\n'));
const outputPath = join(process.cwd(), out);
console.log('\nEncoding with ffmpeg...');
const ffmpegArgs = [
'-y',
'-f', 'concat',
'-safe', '0',
'-i', concatFile,
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-preset', 'medium',
'-crf', '18',
outputPath
];
if (blurFrames > 1) {
// Apply motion blur by averaging frames. This is a simplistic approach;
// a proper implementation would involve more sophisticated filtering.
ffmpegArgs.splice(-1, 0, '-vf', `tmix=frames=${blurFrames}:weights=1`);
}
execSync('ffmpeg ' + ffmpegArgs.map(a => `'${a}'`).join(' '), { stdio: 'inherit' });
console.log('Done: ' + outputPath);
} else if (out.endsWith('.png') && stills) {
const basename = out.replace('.png', '');
for (const ti of stills) {
await page.evaluate((t) => { window.__seek(t); }, ti);
await page.screenshot({ path: `${basename}_${String(ti).replace('.', '_')}.png` });
}
console.log('Stills saved.');
}
await browser.close();
// Cleanup (optional)
// execSync(`rm -rf ${tmpdir}`);
}
main().catch(console.error);
注: これは完全な実装を意図したものではありません。実際の使用には、このドキュメントに記載されているすべてのルールとガイドラインに従った、より堅牢なコード生成とレンダリングパイプラインが必要です。