本文へ

開発記録

静的な Astro ページに Canvas、GSAP、局所的なインタラクションを残す

保持している Canvas の実装、GSAP によるページ遷移演出、作品カテゴリを例に、静的 HTML とブラウザー側の処理をどう分担し、Vue アプリ全体を戻さずに操作を実現するかを整理する。

林予晴
公開
  • Astro
  • JavaScript
  • GSAP
  • プログレッシブエンハンスメント

サイトを静的出力に切り替えても、作品カテゴリの切り替え、画像の拡大、ページを離れる前のアニメーションは実現できる。これらにはブラウザー側のスクリプトが必要だが、そのためにページ全体を再び Vue アプリにする必要はない。

この構成で私が判断基準にしたのは、まずドキュメントでコンテンツを届け、そのうえで各インタラクションに必要な DOM とリソースを渡すことだった。何を残すかは、その効果がどの機能に依存しているかで決める。以前どの種類のコンポーネントに書かれていたかではない。

このノートは、2026年10月20日に確認した実装を対象にしている。最初に区別しておきたいのは、その時点でサイト共通の背景に使われていたのは CSS/SVG による製図風のグリッドで、ブラウザー側のスクリプトが CSS 変数を更新していたことだ。以前の Canvas による流線コンポーネントはコードに残っているが、共通レイアウトには組み込まれていない。以下では、独立させられる描画処理の例として扱い、保持されたファイルを実際の表示と混同しない。

静的出力が決めるのはコンテンツの生成時点であり、ブラウザー側の処理を禁じるものではない

Astro コンポーネントは、先に HTML を生成し、コンポーネント内の script からブラウザー側のスクリプトを読み込んで、イベントやアニメーションを処理できる。そのために UI フレームワークを先に導入する必要はない。[1]

この二つは、実行されるタイミングも、扱えるデータも異なる。このサイトでは、作品タイトル、画像、カテゴリの選択項目、Demo リンクを、データとコンポーネントから先に生成する。その後、作品ページのスクリプトがページのルートノードを探し、カテゴリの表示や画像グリッドの配置を処理する。ブラウザーが空のコンテナーから始めて、スクリプトの実行が終わるまで作品の内容を把握できない構成ではない。

この分担により、スクリプトが動かなかった場合の影響も明確になる。JavaScript が実行されなければ、カテゴリは一つのセクションだけを表示する切り替え UI にはならず、画像グリッドのスクリプトによる配置も行われない。それでも、初期ドキュメントには作品のテキスト、画像、主要なリンクが残る。一方、画像の拡大など、スクリプトで実現する操作については、サムネイルが存在するだけで JavaScript なしでも使えるとは言えない。

Canvas の初期化関数は、サイト全体を知る必要がない

保持している流線の実装では、initVectorFlow(background) が背景のルートノードを受け取り、その中から canvas を探して描画 context を取得し、軌跡と粒子のデータを作る。利用するのは、ブラウザーが提供するフレーム処理、サイズ、表示状態であり、Vue store から作品、記事、現在のルートを取得する必要はない。

この形なら、描画アルゴリズムとサイトのコンテンツを別々に保守できる。渡されたルートノードが要素を探す範囲を決める。戻り値のクリーンアップ関数は、自分で登録したリスナーを削除し、フレームをキャンセルし、任意で使うグローアニメーションも片付ける。外側のコンポーネントは、初期化とクリーンアップをいつ呼ぶかを担当する。

残すべきなのは、このインターフェースだ。構造が明確なルートノードを入力し、リソースを解放する関数を返す。以前の onMounted の名前を変えただけでは、移行を終えたことにはならない。

たとえば、コンポーネントのライフサイクルから処理を切り出しても、canvas が存在するか、context の取得に失敗したらどう終了するか、アニメーション用パッケージを非同期で読み込んでいる間に元のページを離れていないかは確認する必要がある。これらは以前から存在した問題であり、ブラウザー側の処理が直接責任を持つ形になっただけだ。

以上は保持された実装を読んで確認した内容であり、10月20日の確認時に Canvas を再び組み込んで計測したり、動作を検証したりした結果ではない。流線の過去の計測については、背景の流線に関するノートを参照してほしい。

GSAP は遷移を演出しても、遷移先の管理までは引き受けない

確認した実装のページ遷移演出は、別の構成になっている。まず共通の操作処理を初期化し、その後にモーション用モジュールを非同期で読み込む。このモジュールは GSAP でブラインド状のマスクを動かし、最後に location.assign() でリンク先へ移動する。

遷移先は、あくまで実際の a 要素から取得する。コードでは、外部 URL、query や hash を含むリンク、ダウンロード用リンク、別ウィンドウで開くリンク、修飾キーを使ったクリックを対象外にしている。動きを減らす設定が有効な場合も、このマスク処理は通らない。

役割は明確だ。HTML がリンクを提供し、スクリプトは条件に合う場合だけページを離れる際の演出に介入する。クライアントルーターを作ったり、次のページの内容を現在のドキュメントに差し込んだりするものではない。

モーション用モジュールの読み込みに失敗すると、外側の処理がページ表示時の演出用マーカーを解除する。主要な操作処理はすでに初期化されており、通常のリンクもドキュメントに残っている。これはコードに用意されたフォールバックであって、あらゆるネットワーク障害、遷移のキャンセル、ブラウザーの「戻る」の経路を、この確認ですべて操作検証したという意味ではない。

局所的な状態は、実際に影響する範囲に置く

確認した作品ページには、開発、デザイン、AIGC の三つのカテゴリがあり、それぞれの選択項目に固有の hash がある。スクリプトは hash を読み取って表示するセクションを決め、カテゴリがクリックされると URL を更新し、セクションと選択項目の状態を同期する。

この状態をサイト全体の store に引き上げる必要はない。影響するのは作品ページだけであり、選択中のカテゴリは URL でも表せる。画像グリッドの再配置も、そのページのオブザーバー、画像イベント、フレームのスケジューリングで処理する。記事ページやホームページが、画像グリッドの高さを知る必要はない。

すべてを同じ種類のコンポーネントに移すと先に決めるのではなく、次の表のように責任の境界を確認できる。

動作 本サイトでの責任範囲 残す必要がある基本コンテンツ
作品カテゴリ 作品ページのルートノードと URL の hash カテゴリの選択項目と各セクションの内容
画像グリッドの配置 そのページのセル、カード、画像イベント 初期状態でも読めるグリッド
GSAP によるページ遷移 共通のモーション用モジュール 実際のページリンク
保持している Canvas の流線 渡された背景のルートノードと描画リソース 本文を担わない装飾レイヤー

この表は確認したコードの責任分担を整理したもので、すべてのインタラクションを手書きの DOM 操作に置き換える提案ではない。要件が、多数の入力項目の連動、セクションをまたぐリアルタイムの状態共有、複雑なエディターへと変わるなら、状態モデルやフレームワークを改めて検討する必要がある。どの製品でも個別のイベント処理だけを使うべきだと固執するものではない。

リソースの解放では、離脱と一時保存を区別する

初期化関数を切り出すと、「ページを離れたらすべて削除する」とだけ考えがちだ。しかしブラウザーは、ドキュメントを戻る/進むキャッシュに保存し、戻ったときに元の DOM を再利用することがある。

製図風グリッドの外側のコンポーネントは、pagehide でアニメーションのインスタンスを解放し、キャッシュからの復元条件を満たす pageshow で再び初期化する。一方、作品ページは、離脱時に実行待ちのフレームをキャンセルする。オブザーバーを切断するか、操作用のリスナーを削除するかは、イベントの persisted による分岐でも判断している。

これらは同じクリーンアップ方針ではない。アニメーションは再生成できても、通常の操作処理は残す必要があるかもしれない。全体を一括停止する関数では、各機能のライフサイクル上の契約を置き換えられない。具体的な状態と操作の検証については、背景のライフサイクルに関するノートにまとめている。ここで扱うのはアーキテクチャ上の責任の所在であり、旧背景のテスト結果を、今回確認した実装の裏付けには使わない。

コンテンツを先に確認し、その後で追加の動作を確認する

静的ページに局所的なインタラクションをつなぐとき、私はまず初期 HTML を確認する。読者に必要なテキスト、画像、リンク先がすでに存在するか。そのうえで、スクリプトが取り決めたノードだけを操作しているか、生成したリソースを誰が解放するかを確認する。

最後は、実際に操作する必要がある。コードにフレームのキャンセルがあっても、あらゆる復帰の状況をテストしたことにはならない。通常のリンクが存在していても、アニメーションが介入した後のすべての経路に問題がないとは限らない。このノートでは、アニメーションの滑らかさ、パッケージサイズ、性能、検索結果を改めて計測しておらず、すべての端末で操作を検証したわけでもない。

ルーティングと責任分担に関するノートでは、ページを誰が組み立てるかを説明した。このノートで補ったのは、インタラクションが自分の範囲をどう受け取るかだ。コンテンツをアニメーションの起動条件に結び付けなければ、効果を残すためにアプリ全体まで残す必要はない。

Sources

[1] https://docs.astro.build/en/guides/client-side-scripts/