← リールに戻る

How it works

動画ではなく、
プログラムです。

このリールは MP4 ではなく、1つの HTML ファイルです。映像は Canvas に、音は Web Audio に、見ている人の端末がその場で描いて鳴らしています。画像も音声ファイルも読み込んでいません(外から取ってくるのは Google Fonts の2書体だけ)。

25秒版のサイズ
89 KB
配信時(gzip)
31 KB
行数
1,450
粒子の数
1,400

01時間は1本だけ

全部の絵は render(t) という1つの関数が、経過秒 t だけから描いています。前のコマの状態を持ち越さないので、どの瞬間でも同じ絵を何度でも作り直せます。一時停止も、途中からの再生も、1コマずつの書き出しも、この性質だけで成り立っています。

function tick() {
  const t = (performance.now() - perfStart) / 1000; // 音が聞こえた瞬間を 0 とした経過秒
  render(t);                                         // 絵は t だけで決まる
  requestAnimationFrame(tick);                       // 60fps で呼ばれる
}

URL に ?t=秒 を付けると、その瞬間で止まった1枚が出ます。

02等速の動きを使わない

動きはすべて「入りは速く、止まりは柔らかく」。指数カーブと、行き過ぎて揺れ戻るばね(減衰振動)の式で動かしています。主な動きは 120BPM の拍(0.5秒)の頭で始まり、見せ場の直前には 0.4 秒の無音の「溜め」を置いています。

// ばね: 経過秒 s に対して 0→1(行き過ぎて揺れ戻る)
function spring(s, w = 20, z = 0.42) {
  if (s <= 0) return 0;
  const wd = w * Math.sqrt(1 - z * z);
  return 1 - Math.exp(-z * w * s) * (Math.cos(wd * s) + (z * w / wd) * Math.sin(wd * s));
}

03音もその場で合成している

キックは周波数を急降下させたサイン波、ハイハットは高音だけ通したノイズ、残響は乱数で作ったインパルス応答への畳み込み、和音はわずかにずらしたノコギリ波。再生ボタンを押した瞬間に、映像と同じ時刻表から数百の音をまとめて予約します。

kick(at, v = 1) {
  const o = osc('sine', 150, at, 0.5), g = ac.createGain();
  o.frequency.exponentialRampToValueAtTime(46, at + 0.12); // 150Hz → 46Hz で「ドン」
  env(g, at, v, 0.003, 0.42); o.connect(g); out(g);
}

04Canvas 2D だけで立体に見せる

05重くなったところと直し方

実測: GPU(Radeon 780M)での実再生で 60fps・25ms を超えるコマ 0。

06それでも動画にできる

render(t) が決定的なので、ヘッドレスの Chromium で1コマずつ描いて撮り、ffmpeg に流し込めば MP4 になります。音は同じ時刻表を OfflineAudioContext で合成して WAV にし、映像と重ねます。

for (let i = 0; i <= dur * fps; i++) {
  await page.evaluate(t => window.__seek(t), i / fps); // その瞬間を描く
  await write(await page.screenshot({ type: 'png' })); // ffmpeg の標準入力へ
}

07ソースを読む

全部そのまま読めます。PC ならリールを開いて開発者ツールで __seek(13.7) と打つと、その瞬間に飛べます。