2026.10.01

ヘッドレスブラウザでCanvasアニメーションの決定的瞬間を撮影する方法

ヘッドレスブラウザでCanvasアニメーションの決定的瞬間を撮影する方法

Canvasアニメーションの「決めの瞬間」をヘッドレスブラウザで撮影しようとすると、待てど暮らせどその瞬間が来ない——原因は、ヘッドレス環境ではrequestAnimationFrame(rAF)が極端に間引かれ、アニメーションがほぼ進まないことです。この記事では、当社サイトのヒーローアニメーション(粒子が「D」の形に集まる演出)をOGP画像や記事のアイキャッチ用に撮影するために編み出した、rAFを止めて目的の状態を手で作り、1フレームだけ手動で描く方法を紹介します。

背景: スクショが欲しいのは「一番いい瞬間」

当社サイトのトップページは、260個の粒子が漂いながら約5秒かけて「D」の形に自己組織化するCanvasアニメーションで始まります。SNSでシェアされたときに表示されるOGP画像には、当然「Dが完成した瞬間」を使いたい。そこでPlaywrightのヘッドレスブラウザでページを開き、5秒待ってスクリーンショット——としたのですが、撮れた画像の粒子はバラバラのままでした。

原因: ヘッドレスではrAFがまともに回らない

rAFのコールバックは描画機会に合わせて呼ばれるため、画面を持たないヘッドレス環境では呼び出しが大幅に間引かれます。実時間で5秒待っても、アニメーション内部の経過はほんのわずか。「もっと待つ」方向で粘っても、今度はアニメーションが形成後の漂流フェーズに進んでしまうなど、狙った瞬間を実時間の待ち時間で捕まえるのは事実上不可能でした。

解決: 時間を止めて、状態を直接作る

発想を変えて、「アニメーションを走らせて待つ」のをやめました。手順は5つです。

  1. rAFを無効化する: window.requestAnimationFrame = () => 0 で以後のループを空回りさせ、こちらの作った状態が上書きされないようにする
  2. Webフォントの読み込みを待つ: await document.fonts.ready。「D」の形は書体(Syne)の字形から点をサンプリングしているため、フォント確定前だと形そのものが変わる
  3. レイアウトを再計算する: 実装内のsize()を呼び、現在のビューポートに合わせて目標座標を作り直す
  4. 粒子を目標座標へスナップする: 各粒子の現在位置を、Dを構成する目標位置へ直接代入する
  5. draw()を手動で1回呼ぶ: 完成状態の1フレームだけをCanvasに描画する

実際にPlaywrightのevaluateで流したコードがこちらです(size/draw/粒子配列は当社実装の関数・変数名)。

async () => {
  window.requestAnimationFrame = () => 0;   // 1. ループ停止
  await document.fonts.ready;                // 2. フォント確定を待つ
  size();                                    // 3. 目標座標を再計算
  Aphase = 1; At0 = performance.now();       //    「形成完了」フェーズへ
  An.forEach((p, i) => {                     // 4. 粒子を目標へスナップ
    p.x = Atargets[i][0];
    p.y = Atargets[i][1];
  });
  draw(performance.now());                   // 5. 1フレーム手動描画
}

このあとスクリーンショットを撮れば、何度実行しても同じ「完成したD」が写ります。待ち時間はフォント読み込みだけなので、撮影は数秒で終わり、再現性も完全です。OGP画像(1200×630)と、リニューアルお知らせ記事のアイキャッチ(1600×900)はこの方法で撮影しました。

ハマりポイント

まとめ

ヘッドレスでCanvasアニメーションの決定的瞬間を撮るコツは、「待つ」のではなく「時間を止めて状態を作る」こと。rAF無効化→fonts.ready→レイアウト再計算→状態スナップ→手動draw()の5手順で、狙ったフレームを確実に、何度でも撮影できます。CI上でのビジュアルリグレッションテストなど、アニメーションを含むページの自動撮影全般に応用が利く手法です。

Webサイトの構築や、こうした表現と実装のご相談は、SERVICESからお気軽にどうぞ。

← TECH-BLOG一覧へ