infinite-webgl-grid について
infinite-webgl-gridは、ドラッグやスクロールで全方向に閲覧できる画像グリッドを作るThree.jsスキルです。メッシュを生成・破棄し続ける代わりに、座標ラッピングで固定数のメッシュとテクスチャを再利用します。
主な機能
- 01ドラッグやスクロールで閲覧
- 02座標ラッピングによる無限グリッド
- 03固定メッシュとテクスチャの再利用
- 04グリッドとタイルの設定
- 05オフライン用プレースホルダー表示
活用シーン
- 01ポートフォリオ用のドラッグ可能な画像壁
- 02全方向に閲覧できるポスター壁
- 03連続して巡れる画像ギャラリー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rheinmir/ setup --skill infinite-webgl-gridインストール後、Claude Code で/infinite-webgl-gridを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/demo.cssdemo.htmldemo.js詳細
スキル:infinite-webgl-grid
純粋な Three.js コンポーネント — 固定された画像タイルの N×N グリッドを、座標ラッピング(オフセットの剰余演算)を使って、あらゆる方向に無限に広がるようにドラッグ(ポインター)またはスクロール(ホイール)で閲覧できます。メッシュを継続的に生成・破棄することはありません。
WHAT
目的と背景
- 目的: 座標ラッピングを使って、あらゆる方向に無限に閲覧できる WebGL 画像グリッド N×N を構築します。
Rheinmir/uiux-assetのオリジナルをそのままコピーすることを優先します。 - 使用するタイミング:
- あらゆる方向に「無限に閲覧」できる感覚が必要なポートフォリオ/ギャラリー/ポスターウォール(一般的な一方向スクロールではありません)。
- ユーザーが「無限画像グリッド」「infinite scroll grid webgl」「draggable image wall」「無限スクロールのポスターウォール」と言った場合。
- 対象外:
- スクロールの進行に合わせてパーティクルを特定の1枚の画像に集約する必要がある場合は使用しないでください(これは
threejs-particle-morphです。「到達済み」の状態があるのに対し、こちらには目的地がなく、連続的に閲覧するだけです)。 - 1つのフレーム上で2枚の画像を切り替えるエフェクトには使用しないでください(これは
blurです)。
- スクロールの進行に合わせてパーティクルを特定の1枚の画像に集約する必要がある場合は使用しないでください(これは
メンタルモデル
仮想オフセット(ドラッグ/スクロール/慣性)→ 各固定メッシュ:wrap(localX - offset.x) → 表示位置 → 仮想タイルのインデックスが変化 → 固定テクスチャプールからmaterial.mapを変更。カメラは固定され、実行中にメッシュを追加・削除しません(詳細は「Reference — 仕組み」を参照してください)。
入出力の仕様
| 項目 | 必須? | 意味 | |
|---|---|---|---|
| 入力 | 画像/コンテンツ+色 | いいえ | 不足時はデモがプレースホルダーの Canvas を自動描画し、オフラインで動作します |
| 入力 | プロジェクトの制約:外部ライブラリ/ファイルを取り込めるか | はい | 可能ならオリジナル版、不可能なら assets/ の vanilla 版 |
| 入力 | gridSize/tileSize/gap/poolSize | いいえ | InfiniteGrid の初期化パラメーター |
| 出力 | .html ファイル(+_assets/)またはプロジェクト内で適応済みの demo.html/.css/.js | はい | オリジナル版の場合は、先頭行にクレジットコメントを残します |
| 出力 | Playwright による検証の証拠 | はい | 4方向にドラッグする前/途中/後の画像+エラーのないコンソール |
ルールと機能
- RULE-01 (MUST): 同時に使用するタイル/テクスチャ数の制限:モバイル GPU では、同時にアクティブにできるテクスチャユニット数に制限があります。実際に表示されるグリッド(ラップ用のバッファを除く)は、同時に 5×5 タイル以下を目安にしてください。デモの既定値は
gridSize=7(表示タイル5枚+各辺にバッファ1枚で、タイルが滑らかに飛び移り、ポップインを防ぎます)です。表示タイル数より多くの異なる画像が必要な場合は、poolSizeを無制限に増やすのではなく、テクスチャアトラス(複数の画像を1つのテクスチャにまとめ、個別のテクスチャごとにmaterial.mapを変更する代わりにUVオフセットを変更する方法)を使用してください。個別のテクスチャは、同時に表示されなくても、コードが参照を保持している限りそれぞれ1つのテクスチャユニットを消費します。 - RULE-02 (MUST): Passive: false は必須:
wheelとpointermoveのリスナーは{ passive: false }を指定して登録し、適切な場所(ドラッグ中、またはキャンバス内にいる場合のみ)でpreventDefault()を呼び出す必要があります。このフラグがないと、最新のブラウザーでは既定で passive になり、preventDefault()がひそかに無視されます(エラーも警告もありません)。その結果、ページ背景もグリッドと同時にスクロール/ズームし、モバイルでカクつきや競合が発生します。コンテナに対する CSStouch-action: noneは必要な条件ですが、preventDefaultの代わりにはなりません。 - RULE-03 (SHOULD): デモには直交カメラを使用してください(グリッドが平面で、遠近法による歪みがありません)。奥行きのエフェクト(遠くのタイルがぼやける/小さくなる)が必要なら
PerspectiveCameraに変更してください。その場合も wrap の式は変わらず、world-unit をピクセルに変換する方法だけが変わります。 - RULE-04 (MUST):
playwright-verifyで検証してください — 強くドラッグしている最中の前/途中/後を撮影し(4方向すべてをテスト)、コンソールを確認します(WebGL のコンテキストエラーはキャンバス上では気づきにくく、明確な JS エラーを投げないことがあります)。 - 機能:外部アセットリポジトリ(またはスキルの
assets/)からファイルを読み取り/コピーする、プロジェクトにファイルを書き込む、ローカル HTTP 経由で配信する、ヘッドレスブラウザーを操作して撮影しコンソールを読み取る。
失敗時の境界
- プロジェクトで外部ライブラリを取り込めない場合 →
partialは許容されます。オリジナル版の代わりに、簡易版のassets/を使用します。 file://経由で開いた場合 → テクスチャ/アセットが失敗します → HTTP 経由で実行するまでblockedです。- コンソールに WebGL エラーがある場合、またはページ背景がグリッドと同時にスクロールする場合 → 検証は
failedです。修正(passive:false +touch-action: none)してから再検証してください。 - 実際の要件がパーティクルの集約/2枚の画像の切り替えである場合 →
clarifyし、threejs-particle-morph/blurに切り替えます。
方法
主なワークフロー
| ステップ | 種類 | 入力 | アクション | 出力/終了 | 失敗時/次のステップ |
|---|---|---|---|---|---|
| W01 | 判断 | 要件 | スキルが適切か(無限に閲覧でき、終点がないグリッド)を確認し、オリジナル版かassets/を選択します | 選択したソース | 外部ライブラリを取り込めない場合 → B01 |
| W02 | 作業 | オリジナルのPen | 実行リンクを開いてエフェクトを確認 → .htmlファイル全体(+_assets/)をプロジェクトにコピーし、クレジットコメントを保持します | プロジェクト内のファイル | — |
| W03 | 判断 | コピーしたファイル | コンテンツ/画像/色のみを変更します | 適用済みファイル | — |
| W04 | 作業 | ファイル | file://ではなく、HTTP経由でローカル実行します | 実行中のページ | アセットのエラー → パスを再確認します |
| W05 | 決定論的 | ページ | playwright-verifyで検証します。4方向に強くドラッグする前/途中/後のスクリーンショットを撮り、コンソールを確認します | 画像+エラーのないコンソール | エラー → 修正し、W04を繰り返します |
各ステップの詳細(W01~W05の信頼できる情報源):
オリジナル版 — 先に使用(ファイル全体をコピーし、書き直さないでください)
作者によるオリジナルのソースコード(公開されているCodePen=MIT)は、外部リポジトリ Rheinmir/uiux-asset にあります — 実際に動作するギャラリー:https://rheinmir.github.io/uiux-asset/scroll-effects/ 。
手順:実行リンクを開いて正確なエフェクトを確認 → .htmlファイル全体(ファイルが参照している場合は_assets/フォルダーも)をプロジェクトにコピー → 先頭行のクレジットコメントを保持 → コンテンツ/画像/色のみを変更します。file://ではなく、HTTP経由でローカル実行します。
オフラインで一式が必要な場合は、git clone --depth 1 https://github.com/Rheinmir/uiux-assetを実行します。
| オリジナルのPen — 作者 | リンク |
|---|---|
| Infinite Scrollable and Draggable WebGL Grid — ReGGae | 実行 · コード |
| Infinite 3D Poster Scroll Wall — photodow | 実行 · コード |
| Infinite Scrolling with Image Cards — jkantner | 実行 · コード |
このスキルのassets/は、独自に作成したバニラ版の簡易実装です — プロジェクトで外部ライブラリを取り込めない場合に使用します。通常は上記のオリジナル版を使用します。
分岐
| ID | 種類 | 条件 | 動作 | スキップ/失敗 | 再合流先 |
|---|---|---|---|---|---|
| B01 | 条件付き必須 | プロジェクトで外部ライブラリを取り込めない | assets/demo.html+demo.css+demo.jsを使用し、パラメーターを適用してプレースホルダーを実際の画像に置き換えます(「アセット」セクション) | — | W04 |
| B02 | ユーザー選択 | 奥行きのあるエフェクトが必要 | OrthoをPerspectiveCameraに変更し、wrapの式は維持して、world-unitからpixelへのマッピングのみを変更します | — | W05 |
| B03 | 条件付き必須 | 表示されるタイル数より多くの異なる画像が必要 | poolSizeを増やすのではなく、テクスチャアトラスを使用します(UVオフセットを変更) | — | W05 |
検証と完了条件
Playwrightで4方向へのドラッグ前/途中/後のスクリーンショットをすべて撮影し、グリッドにポップインがなく、背景ページがスクロールせず、コンソールにエラーがない状態で完了です(WebGLエラーは表示されないことが多いため、画像を見るだけでなく、必ずコンソールを確認してください)。
例
- 成功例:「ポートフォリオ用に無限スクロールするポスターウォールを作って」→
Rheinmir/uiux-assetからinfinite-3d-poster-scroll-wall.htmlをコピーし、画像を置き換え、HTTP経由でローカル実行して、Playwrightで4方向にドラッグします → タイルが滑らかに飛び移り、コンソールにエラーがありません。 - 境界/失敗例:
assets/demo.jsを使っているが、wheelを{ passive: false }なしで登録している →preventDefault()が黙って無視され、背景ページもグリッドと一緒にスクロールします → 検証に失敗するため、フラグとtouch-action: noneを追加して再実行します。
参照 — アセット
assets/demo.html+assets/demo.css+assets/demo.js — 読んでから適用します(InfiniteGridの初期化時にgridSize/tileSize/gap/poolSizeを変更し、_drawPlaceholder()を、canvasのdata URLではなくURLをTHREE.TextureLoaderで読み込む実際の画像に置き換えます)。デモはプレースホルダー(canvas上の数字+ハッシュカラー)を自動描画するため、画像やネットワークがなくてもすぐに実行できます。
リファレンス — 仕組み
- メッシュは固定で、追加生成しません:
gridSize × gridSize個のメッシュ(PlaneGeometryは 1 インスタンスを共有し、各メッシュには個別のマテリアルを使用)を初期化時に一度だけ構築します。実行中にメッシュを追加・削除することは決してありません。「未表示の領域までドラッグしたら新しいメッシュを生成する」という素朴な方法とはまったく異なります。 - 仮想オフセットを使い、カメラを無限遠まで移動させません:ドラッグ/スクロールでは
offset.x/offset.y(理論上はいくらでも大きくでき、実際の上限には達しない浮動小数点数)に加算するだけです。カメラは動きません。 - 剰余によるラップ:各メッシュには固定のローカル座標
localX/localY(元の N×N グリッド内の位置)があります。各フレームでの実際の表示位置はwrap(localX - offset.x)です。ここでwrap(raw) = ((raw + half) % full + full) % full - half(full = gridSize*pitch、half = full/2)とし、無限に広がる座標を中心付近の表示範囲に折り返します。この手法の標準的な剰余の式です。 - タイルが飛び越える際のテクスチャ再利用:各メッシュの
material.mapを変更するのは、「仮想タイルのインデックス」が変わったときだけです(wrapped_position + offsetから求め、pitch単位に丸めます)。つまり、タイルが片側の表示範囲から外れ、反対側に再び現れるタイミングです。テクスチャは固定プール(THREE.TextureLoaderで一度だけ読み込むpoolSize個のテクスチャ)から取得し、飛び越えるたびに新しいテクスチャを作成することはありません。 - ネットワークに依存しないプレースホルダー:プール内のテクスチャは、数字と色を描画する
<canvas>から生成します(色相を golden-angle で分散させ、見た目で区別しやすくします)。その後canvas.toDataURL()を使い、引き続きTHREE.TextureLoader.load(dataUrl)で読み込みます。これによりデモはオフラインで動作しつつ、実際の画像を読み込む場合と同じ API 経由の読み込みフローを維持します(実際の画像に置き換える際は URL を変更するだけで、コードの流れは変わりません)。 - 慣性:ポインタのドラッグ中は
velocity(最後のpointermoveにおける移動量)を記録します。手を離すと、閾値を下回るまで各フレームでoffset += velocity; velocity *= damping (0.92)とします。物理を忠実にシミュレートするのではなく、0 に向けて徐々に lerp します。 - クリーンアップ:
destroy()では pointerdown/pointermove/pointerup/pointercancel/wheel/resize リスナーを解除し、各マテリアルを dispose し、共有ジオメトリを一度だけ dispose し、プール内の各テクスチャを一度だけ dispose します(テクスチャは複数のメッシュで共有されるため、メッシュごとに dispose してはいけません)。さらに、レンダラーを dispose し、canvas を DOM から取り除きます。