add-mouse-driven-orbit について
3Dシーンに、ポインターに応じた控えめなカメラとオブジェクトの動きを加えるデザインスキルです。オブジェクトを直接操作せず、映画的な奥行きを演出したい制作者向けです。
主な機能
- 01フレームレートに応じたポインター補間
- 02カメラとオブジェクトの回転を分離
- 03奥行きに応じたCSSパララックス
- 04画面サイズをまたいだ構図維持
- 05視差軽減とキーボード操作に対応
活用シーン
- 013Dシーンに控えめなポインター反応を追加
- 02カメラ移動中もテキストやカードを固定
- 03キーボードでオービット状態を操作
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill add-mouse-driven-orbitインストール後、Claude Code で/add-mouse-driven-orbitを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamldemo/index.htmlPROMPT.md詳細
マウス操作によるオービットを追加する
減衰するポインターのターゲットを、浅いカメラの弧と小さなオブジェクトの回転に変換します。ユーザーが製品を直接確認する必要がある場合は threejs と OrbitControls を使用し、ドラッグ、ズーム、オクルージョン、ゲームプレイ用のカメラには build-game-camera-controls を使用します。このスキルが実現するのは受動的なシネマティックな奥行きであり、直接操作ではありません。
inner-green-3d.html から抽出しました。ここでは、プロシージャルな苔の根をポインターの方向へ向けつつ、見出し、カード、固定されたシルエットの目印からずれないようにする必要がありました。
レイアウトではなく意図を記録する
ポインターハンドラーでは、正規化した座標のみを書き込みます。
function recordPointer(event) {
if (event.pointerType === "touch") return;
target.x = (event.clientX / viewport.width) * 2 - 1;
target.y = (event.clientY / viewport.height) * 2 - 1;
}
インタラクティブ領域は ResizeObserver でキャッシュします。pointermove のたびに各レイヤーで getBoundingClientRect() を呼び出さないでください。同期的なレイアウト計算により、軽いエフェクトでもフレームの急増が発生します。
pointerleave では、ターゲットを 0, 0 に戻します。粗いポインターでは、デザインされた中央のポーズを維持します。コンテンツを表示するためにタッチドラッグが必要になるような受動的なオービットにしてはいけません。
フレームループで減衰させる
ソースでは、60 Hz のフレームあたり 0.055 を使用しています。リフレッシュレートが変わっても、この感覚を維持します。
const alpha = 1 - Math.pow(1 - 0.055, dt * 60);
smooth.x += (target.x - smooth.x) * alpha;
smooth.y += (target.y - smooth.y) * alpha;
dt を 1/30 秒に制限し、復帰後は時間基準をリセットします。フレームごとに固定の lerp を使うと、30 Hz では重く、120 Hz ではぎくしゃくします。デルタを制限しないと、バックグラウンドから復帰したときに値が跳ねます。
丸めた値が安定したら、スタイルや uniform の書き込みを停止します。小数第3位までの精度は、着地点までの移動量の1ピクセルより細かい値です。
const x = Math.round(smooth.x * 1000) / 1000;
const y = Math.round(smooth.y * 1000) / 1000;
if (x !== lastX || y !== lastY) publish(x, y);
モーションを分ける
互いに逆向きで、量の異なるレイヤーを使い、シーン全体が1枚の板のように平行移動するのではなく、ピボットするようにします。
camera.position.x = -smooth.x * 26;
camera.position.y = smooth.y * 16;
camera.lookAt(camera.position.x * 0.42, camera.position.y * 0.42, 0);
nearGroup.rotation.y = smooth.x * 0.055;
nearGroup.rotation.x = smooth.y * 0.026;
farGroup.rotation.y = smooth.x * 0.030;
以下は、ステージのピクセルをワールド単位として構成したシーンでの着地点の値です。
| レイヤー | 水平方向 | 垂直方向 | 理由 |
|---|---|---|---|
| カメラの平行移動 | -26 | +16 | 浅い弧を作る |
| カメラの注視点の追従 | 42% | 42% | 被写体を固定された構図の近くに保つ |
| 手前のオブジェクトのヨー | 0.055 rad | — | 側面を見せずに表面の奥行きを出す |
| 手前のオブジェクトのピッチ | — | 0.026 rad | 上面が平坦に見えるのを防ぐ |
| 奥のオブジェクトのヨー | 0.030 rad | — | 前景と同じ動きにせず、面を分離する |
すべてを同じ量だけ回転させないでください。動きが等しいと、カーソルに追従する平面ポスターのように見えます。カメラを平行移動させるときに、固定された原点へ向けないでください。被写体がレイアウトから目に見えてずれてしまいます。
同じ smooth のペアから CSS パララックスを適用します。ただし、テキストやコントロールの奥行き係数は、3D フォームより小さくします。パララックス用に transform を空けておきます。関係のない表示アニメーションには、clip、opacity、子ラッパーを使い、アニメーション同士が上書きし合わないようにします。
サイズが変わっても構図を維持する
各ブレークポイントで、まず中央のポーズを構成します。ResizeObserver からカメラのアスペクト比とステージからワールドへのスケールを再計算し、サイズがゼロのルートをガードしてから、オービットのオフセットを適用します。ポインターを両端の極端な位置に動かして確認し、コピーとの衝突、カードのクリッピング、空の端の露出がないことを確かめます。
デモやコンフィギュレーターでは、キーボードで操作できる X 軸と Y 軸の範囲コントロールを使用します。これによりオービットがパラメーター化されていることを示せます。また、ポインターを使わないユーザーも、同じようにデザインされた状態にアクセスできます。フォーカスを可視化し、モーションモードの変更を通知します。
モーションとライフサイクルに配慮する
prefers-reduced-motion: reduceの場合は、x=.28, y=-.12付近を基準に、デザインされた斜め前方からの静止画を表示します。範囲コントロールは有効なままにし、選択された静止画を補間せずに再描画します。- 非表示または画面外にある間は一時停止し、再開時に時間基準をリセットします。
- DPR を 2 に制限し、
dtを 1/30 秒に制限します。 - 3D オブジェクトが情報を持つ場合を除き、canvas は装飾として扱います。すべてのラベルとコントロールはセマンティックな HTML で維持します。
- 終了時は、フレームをキャンセルし、オブザーバーを切断し、ポインターやメディアのリスナーを削除し、Three.js のリソースを破棄します。
コストを正直に伝える
軌道計算の負荷はごくわずかです。コストがかかるのは、もともとシーンを再描画する処理と、その上に重なる DOM スタイルの無効化です。フレームループは共有し、値が落ち着いたら丸め、ポインターハンドラー内でレイアウトを読み取らないようにし、インタラクションを取り除く前に、基盤となる描画処理のプロファイルを取得してください。カメラを動かしても、密度の高いシーンが軽くなるわけではありません。
確認
- 1440×900 で、中央と四隅のポーズをソースと比較します。
- カメラとオブジェクトが異なる量で、逆方向に動くことを確認します。
- ビューポート内を素早く移動します。シーンは瞬時に切り替わらず、経路に沿って滑らかに追従する必要があります。
- ビューポートから離れ、中央へ滑らかに戻ることを確認します。
- 390×844 および粗いポインター/タッチ入力でテストし、中央の構図が完全に保たれることを確認します。
- X/Y コントロールを Tab キーで移動し、矢印キーで値を変更して、フォーカスが視覚的に確認できることを確認します。
?reduced=1をテストします。デザインされた静止画が安定して表示され、自律的な追従はなく、コントロールは操作可能な状態にします。- 非表示/表示を切り替え、スクロールで画面外に出してから戻します。ジャンプがなく、フレームループが 1 つだけであることを確認します。
- 両方のサイズで、コンソールにエラーがないことを確認します。
作業用の実証例として demo/index.html を使用し、再作成またはリミックスするには demo/PROMPT.md を使用してください。