build-interactive-particle-trail について
個々の粒子がジェスチャーに反応する、ポインター連動の粒子トレイルを作るデザインスキルです。距離に応じた発生と再利用可能なGPUプールを扱います。
主な機能
- 01ポインター移動距離に応じた粒子発生
- 02ジェスチャー経路に沿った補間配置
- 03固定GPU粒子プールの再利用
- 04頂点シェーダーによる粒子の飛行
- 05ボタン起動バーストでプールを再利用
- 06一時停止と視差軽減への対応
活用シーン
- 01速いポインター操作でも間隔の揃った軌跡を作成
- 02カーソル停止中の粒子の蓄積を防止
- 03ボタンやキーボードで粒子バーストを起動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill build-interactive-particle-trailインストール後、Claude Code で/build-interactive-particle-trailを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamldemo/index.htmlPROMPT.md詳細
インタラクティブなパーティクルトレイルを作成する
ポインターの移動距離に応じて粒子を個別に放出し、セグメント全体に配置して、固定サイズの GPU プールを再利用します。粒子が主に自律的な雰囲気を演出する場合は ambient-section-particles を、連続した流体やシェーダーのリボンを出力する場合は add-shader-cursor-trail を使います。個々の粒子がジェスチャーに反応する必要がある場合は、このスキルを使ってください。
inner-green-3d.html から抽出しました。このページでは、ポインターを使ってプロシージャルに生成された苔の根から花粉を舞い上がらせる必要がありました。素早くなぞっても隙間ができず、カーソルを置いたままにしても明るい粒子の塊が増え続けないようにする必要がありました。
インタラクション平面を決める
ポインターを正規化デバイス座標に変換し、カメラからレイキャストして、ビジュアルサーフェス近くの安定した平面と交差させます。その交点をすべてのインタラクションシステムで再利用します。
ポインターがサーフェスから離れたとき、またはレイがヒットしなかったときは、前回の交点をリセットします。そうしないと、再びポインターが入ったときに、古い退出点から長い対角線が描かれます。
表示されるサーフェスが曲面の場合は、実際のコライダーと交差させるか、そのすぐ手前に反応用の平面を配置します。装飾用の草や粒子を一つずつレイキャストしないでください。
距離に応じて放出する
仕組みの基準は時間ではなく間隔です。
const distance = current.distanceTo(previous);
const count = Math.min(14, Math.floor(distance / 7));
for (let i = 1; i <= count; i++) {
point.lerpVectors(previous, current, i / count);
spawn(point);
}
if (count) {
previous.copy(current);
idle = 0;
} else if ((idle += dt) > 0.055) {
spawn(current); // a resting hand trickles; it does not pump
idle = 0;
}
タイマー方式のエミッターでは、素早くなぞると点の間に隙間ができ、手を止めるとその下に点が積み重なります。セグメント補間を使えば、どちらも解消できます。タブへの復帰やポインターの瞬間移動で一度にプールを使い切らないよう、1 フレームあたりの放出数を 14 個に制限します。
次の実用的なデフォルト値を使います。
| パラメーター | デフォルト | 防止できる問題 |
|---|---|---|
| パス間隔 | 7 px/ワールド単位 | 間隔が広いとサンプル点のように見え、狭いとリボン状になります |
| 1 フレームあたりの上限 | 14 粒子 | ポインターの瞬間移動でプール全体が上書きされます |
| アイドル間隔 | 55 ms | アイドル時に放出しないと動きがなくなり、間隔が短すぎると局所的に密集します |
| プールサイズ | 620 | 割り当てを無制限に増やさずに、1.6 秒のトレイルに十分な重なりを確保できます |
| 寿命 | 1.6 s | 短すぎるとトレイルが途切れ、長すぎるとコンテンツが粒子で覆われます |
| サイズのランダム幅 | 0.50~1.15 | すべての点が同じサイズだと、点描ブラシのように見えます |
| DPR の上限 | 2 | 密度の高いディスプレイで塗りつぶしのコストが急増します |
GPU プールを再利用する
原点、速度、生成時刻、ランダムシード用の型付き配列を一度だけ確保します。生成時にはリングヘッドを進めます。
const index = head;
head = (head + 1) % POOL_SIZE;
origins.set([x, y, z], index * 3);
velocities.set([vx, vy, vz], index * 3);
birth[index] = elapsed;
random.set([size, phase], index * 2);
dirty = true;
再生成後にのみ、動的属性をアップロード対象としてマークします。CPU 上で 620 個の粒子を更新せず、頂点シェーダーで age = uTime - aBirth から飛行をアニメーションさせます。
float age = uTime - aBirth;
float u = age / uLife;
vec3 p = position + aVelocity * age * (1.0 - 0.34 * u)
+ vec3(sin(aRandom.y * 6.283 + age * 2.6) * 22.0 * u,
46.0 * age,
0.0);
vAlpha = smoothstep(0.0, 0.09, u)
* (1.0 - smoothstep(0.40, 1.0, u));
頂点シェーダーでサイズをゼロにし、クリップ空間の外に移動させて、寿命が尽きた点をカリングします。事前に作成した柔らかな放射状テクスチャ、加算ブレンド、depthWrite: false を使い、プール全体を 1 回の THREE.Points 描画呼び出しでレンダリングします。
バーストにもプールを再利用する
ボタンやフォーカス中のコントロールを同じインタラクション平面に対応付け、2.5 倍の広がりで 40~60 回 spawn を呼び出します。プールを一つにまとめることで、ビジュアル表現、予算、クリーンアップを共通化できます。
視認できるフォーカス状態と aria-live による確認メッセージを備えた、実際の 花粉を放つ ボタンを用意します。エフェクトを見たり発動したりする方法を、ポインターの動きだけにしないでください。
モーションとライフサイクルに配慮する
prefers-reduced-motion: reduceの場合は粒子の経過時間を進めないでください。デザインされた曲線状のトレイルを初期状態に配置し、バーストボタンを押すと、より密度の高い静止構図に置き換わるようにします。dtを 1/30 s に制限し、再開後に時刻をリセットします。document.hiddenの間と、セクションが画面外にある間は一時停止します。ResizeObserverでサイズを取得し、ビューポートがゼロの場合を処理して、DPR を 2 に制限します。- サーフェスが対応している場合は、ペンやタッチでも操作できるよう Pointer Events を使います。ホバーだけを前提にした説明は避けてください。
- 終了時には、フレームをキャンセルし、オブザーバーを切断して、リスナーを削除し、点のジオメトリ、マテリアル、スプライトテクスチャ、レンダラーを破棄します。
コストを正直に示す
粒子ごとの飛行計算は主なコストではありません。加算合成によるポイントのオーバードローと属性のアップロードが主なコストです。スプライトを小さく保ち、プールを固定し、リングに変更があった場合にだけアップロードします。プールを増やす前に寿命を短くしてください。シェーダー方式のほうが高速だと主張する前に、密度の高いディスプレイで GPU のフレーム時間とアップロードコストを測定してください。
検証
- 同じ経路でゆっくりカーソルを動かした場合と素早く動かした場合を比較し、間隔がほぼ均等に保たれることを確認します。
- 動きを止め、点が増えて不透明な塊にならず、少しずつ流れ落ちることを確認します。
- 離れてから再び入って、古い点から斜めにつながる線がないことを確認します。
- キーボードでバーストを発生させ、同じプールが再利用されることを確認します。
- バーストを繰り返し、プールの長さが620のままで、メモリ使用量が一定であることを確認します。
- 390×844、1440×900、タッチ/ペン、高速なリサイズをテストします。
?reduced=1をテストします。合成された静止画が表示され、経年変化がなく、バーストボタンが動作することを確認します。- 非表示/表示を切り替え、ビューポートから離れて再び入っても、時間が飛ばないことを確認します。
- コントロールをTabキーで移動し、フォーカスが見えることと、ライブアナウンスが行われることを確認します。
- どちらのサイズでもコンソールにエラーがないことを確認します。
動作を実証するには demo/index.html を使い、再作成またはリミックスするには demo/PROMPT.md を使います。