ブログ |

PROMPT → WEB
#004
PHYSICS PLAYGROUND
CLAUDE CODE × CANVAS 2D / PHYSICS

そのWeb演出、Claude Codeで作れます。クリエイティブWeb表現のプロンプト集 #04 Physics Playground

株式会社グルーヴサインの米沢です。

AIコーディングでクリエイティブなWeb表現を実際に作り、そのプロンプトごと公開していく「PROMPT → WEB」の第4回です。

PROMPT → WEB #004

Physics Playground

INTRODUCTION

ここまでの3回は「どう描くか」の技術で差をつけてきました。WebGLで粒子(#01)、Canvas 2Dで文字(#02)、SVGで線(#03)。ブラウザの主要な描画技術はひと巡りしました。

そこで今回からは、描画技術の目新しさではなく、**「仕掛け」**で驚きを作ります。第4回の仕掛けは リアルタイム物理です。

これまでのアニメーションは、いわば「あらかじめ決めた動き」を再生するものでした。粒子が球になり波になる軌道も、等高線が描き出される順序も、こちらが指定しています。物理シミュレーションはそこが決定的に違います。重力・衝突・摩擦というルールだけを与えて、動きそのものは計算に委ねる。 結果は毎回変わり、誰も正確には予測できません。触ると本当に反応が返ってくる、あの手応えの正体です。

題材は、社名やキーワードを刻んだ文字コインにしました。上からバラバラと落ちてきて、床に積もり、崩れる。カーソルで押しのけられ、クリックで増える。見た目は、ビビッドな色の光沢のある金属球。ガチャガチャのカプセルやおはじきのように、思わず触りたくなる質感を狙いました。理屈より先に、指が動いてしまう「おもちゃ」です。

RESULT

まず現物です。下のボタンを押してみてください。

クリックすると、この画面の上で Physics Playground が再生されます(何度でも再生可能)。ESC または CLOSE で終了します。

記事の上に明るい画面が広がり、PROMPT・WEB・GROOVESIGN の文字コインが上から降ってきて、床に積もっていきます。操作は2つです。

同じ積まれ方は二度と起きません。どこに何色が来るか、どう崩れるかは、その場の計算次第です。ESC または CLOSE で終了します。

これは動画でも物理エンジンライブラリでもなく、300行ほどの自作2D物理をその場で回しています。この演出をClaude Codeに作らせるためのプロンプトは、記事の後半でそのまま公開しています。

WHAT WE MADE

技術的に分解すると、次のようになります。

HOW IT WORKS

物理エンジンと聞くと身構えますが、この規模なら中身は素朴です。毎フレーム、次の3つを順番にやっているだけです。

1. 動かす(重力と積分)

各コインは位置 (x, y) と速度 (vx, vy) を持ちます。毎フレーム、速度に重力を足し、位置に速度を足す。これだけで落下します。

c.vy += GRAV * dt;   // 速度に重力を加える
c.x  += c.vx * dt;   // 位置を速度分だけ進める
c.y  += c.vy * dt;

dt は前フレームからの経過秒。これを掛けることで、端末のフレームレートが変わっても落下速度が一定になります。地味ですが、これを忘れると速い端末だけコインが吹っ飛びます。

2. 壁と床で跳ね返す

画面の外に出ようとしたら、位置を内側へ戻し、その軸の速度を反転させます。反転する際に反発係数(0〜1)を掛けると、跳ね返りの強さを調整できます。床では横方向の速度に摩擦を掛け、やがて滑りが止まって静止します。

if (c.y > H - c.r) {           // 床にめり込んだ
  c.y = H - c.r;               // 床の上に戻す
  c.vy = -c.vy * REST;         // 上向きに、弱めて跳ね返す
  c.vx *= FRICT;               // 横滑りを摩擦で減衰
}

3. コイン同士をぶつける

ここが「積もる」の心臓です。2つのコインの中心距離が半径の和より近ければ、衝突しています。まず重なった分を押し戻して離し、次に近づく向きの速度だけを反発させます。

const d = Math.hypot(dx, dy);      // 中心間の距離
if (d < a.r + b.r) {               // 重なっている
  const overlap = (a.r + b.r - d) * 0.5;
  // 互いに半分ずつ、法線方向へ押し戻す(めり込み解消)
  a.x -= nx * overlap; b.x += nx * overlap;
  a.y -= ny * overlap; b.y += ny * overlap;
  // 近づく速度成分だけを反発させる(離れつつあるなら触らない)
  if (vn < 0) { /* 法線方向の運動量を交換 */ }
}

コインは数十枚なので、総当たり(O(n²))で十分間に合います。数千個を超えるなら空間分割(グリッドや四分木)が必要ですが、この規模で持ち込むのは早すぎる最適化です。まず素朴に書いて、実測してから考える。順序を守るのが結局いちばん速く仕上がります。

なめらかさの鍵は「サブステップ」

物理で最も厄介なのが、コインが速すぎて1フレームで相手をすり抜ける「トンネリング」と、積み重なりが震える「ジッター」です。対策はシンプルで、1フレームの dt を数分割し、物理計算を複数回まわすこと。

const sub = 3;
for (let s = 0; s < sub; s++) step(dt / sub);   // 3回に分けて解く

計算量は増えますが、積み重なりが安定し、貫通も減ります。数十枚なら60fpsに余裕で収まります。「1回で大きく動かす」より「小さく何回も動かす」ほうが、物理は安定する。 これは覚えておくと応用が利きます。

平らな円を、金属球に見せる

物理とは別に、見た目でも一つだけ手をかけています。Canvasの arc() はどこまでいっても平らな円です。これを立体的な玉に見せるのに、3Dも画像も要りません。光のふるまいを4枚重ねるだけです。

ポイントは、光は画面に対して固定することです。コインは回転していますが、光源まで一緒に回すと、玉がグルグル光って安っぽくなる。回すのは表面に刻まれた文字だけ。ハイライトは常に左上 ── そう分けると、回っているのに艶の位置は動かない、本物の球の見え方になります。

// 光源を画面左上に置いた放射グラデーション(本体の塗り)
const g = ctx.createRadialGradient(-r*0.4, -r*0.44, r*0.05,  0, 0, r*1.15);
g.addColorStop(0,    lighten(color, 0.6));  // 光の当たる面
g.addColorStop(0.55, color);                // 固有色
g.addColorStop(1,    darken(color, 0.5));   // 陰・縁

全コインは同じ半径なので、このグラデーションはリサイズ時に1度だけ作って使い回します。毎フレーム120個ぶんを作り直す必要はありません(後述)。

THE PROMPT

ここが本題です。上のデモを作るためにClaude Codeへ渡せる、汎用プロンプト全文を掲載します。右上の COPY ボタンでそのままコピーできます。語・色・ラベルを置き換えれば、自分用になります。

あなたはCreative Codingと物理シミュレーションに精通したフロントエンドエンジニアです。
「Physics Playground」=文字コインが重力で落ちて床に積もり、ぶつかって崩れ、
カーソルで押しのけられ、クリックで増える、リアルタイム物理のWeb表現を、
Canvas 2D と自作の軽量2D物理で実装してください。
(物理エンジンライブラリは使わない。200行程度の自作で足りる規模にする)

# 置き換える変数
- WORDS       = ["PROMPT", "WEB", "GROOVESIGN"]   // 各文字が1枚のコインになる
- BG_COLOR    = "#efece4"     // 明るい生成り。宇宙や黒背景にしない
- PALETTE     = ビビッドな6色(各色を金属球の固有色として使う):
                朱 "#ff4d2e" / 青緑 "#1fae9c" / 藍 "#3b5bdb" /
                琥珀 "#f6a723" / 墨 "#17130e" / 生成り "#e9e4d8"
                ※語ごとに1色を割り当てる(PROMPT=朱, WEB=青緑, GROOVESIGN=藍 …)
- LABEL       = "PHYSICS PLAYGROUND"

# ビジュアル / クリエイティブディレクション
- コインは平らな円ではなく「光沢のある3Dの金属球」に見せる(ガチャのカプセルのような艶)
- 各球は次の4枚を重ねて立体に見せる:
    ①本体 = 光源(画面左上)中心の放射グラデ(明→固有色→暗い縁)
    ②リムライト = 球の下端に環境光の照り返しを薄く
    ③鏡面ハイライト = 左上に小さく鋭い白(濡れたような艶)
    ④接地影 = 真下に楕円の影を1枚
- 【重要】光源は画面に固定する。球が回転しても光は左上のまま。回すのは表面の文字だけ
  (光源ごと回すと安っぽくなる)
- 球の中央に極太サンセリフの1文字(Helvetica/Arial系, 800)を白抜き or 墨で刻む
- 隅に等幅フォントで小さく "PROMPT → WEB" / "#004" / LABEL を置く
- ネオン・rainbow・紫グラデ・glassmorphism・過剰なブルームは使わない(前作と差別化。艶は上品に)

# 物理(自作・ライブラリ禁止)
- 各コイン: 位置(x,y) 速度(vx,vy) 半径r 回転a 角速度av 文字 色index
- 重力: vy += GRAV * dt  (GRAV ≈ 2200 px/s^2)。dtを必ず掛ける(フレームレート非依存)
- 壁: 画面端で位置を内側へ戻し、その軸の速度を反発係数REST(≈0.28)で反転
- 床: 位置を戻して跳ね返し、横速度に摩擦FRICT(≈0.86)を掛けて静止させる
- コイン同士: 総当たり(O(n^2))で
    ①中心距離 < 半径和 なら重なり分を互いに半分ずつ法線方向へ押し戻す
    ②近づく速度成分(vn<0)のときだけ法線方向の運動量を反発係数で交換する
  (数十枚なので総当たりで十分。空間分割は入れない=早すぎる最適化)
- 【安定化】1フレームのdtを3分割し、物理stepを3回まわす(サブステップ)。
  積み重なりのジッターと高速時のすり抜け(トンネリング)を抑える
- dtは上限でクランプ(タブ復帰などの暴れ防止, 例: 0.05秒)

# アニメーション / インタラクション
- 起動時: WORDSの各文字を上から時間差(≈130ms間隔)で1枚ずつ落とす。
  x位置はランダム、初速に軽い横ブレを与える
- ポインタ(マウス/タッチ)を「見えない反発体」にする:
  半径 R*2.6 以内のコインを、近いほど強く外向きに押す力を毎フレーム加える
- クリック/タップ: その座標からコインを3枚、上向きの初速でspawnして増やす
- コイン数の上限を決め(デスクトップ120/モバイル60)、超えたら古いものから消す

# 描画(Canvas 2D)
- devicePixelRatio対応(setTransform, Math.min(dpr,2))。毎フレーム背景で塗り潰す
- 各コイン: 接地影 → 本体(放射グラデ) → リムライト → 文字(回転aを反映) → 鏡面ハイライト
- 【性能】全球は同じ半径なので、放射グラデーションはリサイズ時に1度だけ生成して使い回す
  (毎フレーム全コインぶん作り直さない)
- 隅のラベルは等幅フォントで薄く

# 実装要件
- position: fixed の全画面オーバーレイとして被せ、通常ページに影響させない
- リサイズ・画面回転で幅高さと半径を再計算する
- マウスとタッチの両方に対応。モバイルは半径を大きめ・コイン数を少なめに

# 堅牢性 / 品質
- CLOSE ボタンと ESC で必ず終了できる
- prefers-reduced-motion では、落下アニメを行わず、文字コインを下部に整列して静止表示する
- Canvasが取得できない等の失敗時はエラーを出さず自動でスキップ
- JavaScriptが失敗しても本体ページは必ず表示される Progressive Enhancement
- 終了時に requestAnimationFrame を停止し、イベントリスナ(mouse/touch/resize/key)と
  落下用インターバルをすべて解除、Canvasを DOM から除去(何度実行してもリークしないこと)
- 多重起動を running フラグで防ぐ
- production 品質。共通のオーバーレイ/CLOSE/後始末は再利用できる形に分離する

# アクセシビリティ
- CLOSE は button 要素。ESC で閉じられる。オーバーレイに aria-label
- 装飾なので Canvas には aria-hidden を付ける
- 動きが苦手な人向けに prefers-reduced-motion を尊重(静止表示)

まず重力と壁・床の反発で1枚を落として静止させ、次にコイン同士の衝突、
サブステップ、時間差の落下、ポインタ反発、クリック生成、の順に積み上げ、
モバイル・reduced motion・後始末・本番ビルド確認まで進めてください。

弊社がこのデモを作った際も、この粒度から始めました。落下と衝突の土台は、ほぼ一発で動きます。時間をかけたのは、触ったときの手応えの調整でした。反発が強すぎるとコインが跳ね回って落ち着かず、弱すぎると濡れた雑巾のように積もる。摩擦とサブステップの数を行き来しながら、「積もるけれど、押すと気持ちよく崩れる」一点を探しました。

CUSTOMIZE

PERFORMANCE

物理は「触った感触」が命なので、コマ落ちは致命傷です。実運用では次の点に気をつけました。

WHAT AI DID / WHAT HUMAN DIRECTION MATTERS

重力・衝突・反発・摩擦。物理の基本式そのものは、教科書にもAIの頭の中にも入っています。「Canvasで文字コインが落ちて積もる自作2D物理を」と伝えれば、動く土台はすぐ立ち上がります。 めり込みの押し戻しも、法線方向の運動量交換も、AIは淀みなく書きます。ここは完全にAIの領分です。

一方で、任せきりにできない部分もはっきりしています。

最初に出てきた実装は、反発係数が高すぎてコインがトランポリンのように跳ね回り、いつまでも落ち着きませんでした。数値を一つずつ下げ、床の摩擦を効かせ、サブステップを足す。派手にするのではなく、落ち着かせる方向の調整でした。物理は放っておくと暴れます。それを「気持ちよく暴れて、気持ちよく静まる」範囲に収めるのが、人間の仕事でした。

物理シミュレーションは、AIにとって書きやすい題材です。だからこそ差が出るのは、どの数値で手が止まるかという感覚のほうです。手が速くなった分、この「触って決める」時間にこそ、人が向き合う価値が残っています。4回続けて、同じ結論に辿り着いています。

NEXT

第1回は粒子(WebGL)、第2回は文字(Canvas 2D)、第3回は線(SVG)、そして第4回は物理(自作エンジン)。描画技術で一巡したあと、「仕掛け」で驚きを作る新しい章に入りました。

次回はまた別の仕掛け ── 画像処理(ハーフトーン)、音(Web Audio)、スクロール連動といった方向を考えています。一覧で見たときに「毎回違う驚きがある」状態を、引き続き目指します。

今回のプロンプトも丸ごとコピーして使えます。自社の言葉と色に置き換えて、ぜひ一度Claude Codeに渡してみてください。降ってくる自社名は、想像より少しだけ楽しいはずです。

レガシーPHPシステムの保守・引き継ぎのご相談を受け付けています。
初回のご相談・ヒアリングは無料です。

無料相談・お問い合わせはこちら

株式会社グルーヴサイン 代表取締役 米沢忠司。PHP開発歴約20年。他社開発システムの保守・引き継ぎを主力に、2007年の設立から19期目。