← Blog
TypeScriptPhysicsCanvas APIPerformance

ブラウザで動く物理エンジンを自前で書いた話

4 分で読める

このトピックを深掘りする

Canvas vs DOM・インタラクティブUI

CanvasとDOMの使い分け・CSSインタラクション・カスタム物理エンジン・ポートフォリオ設計

全5本中 5 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

なぜ自前で書いたのか

ポートフォリオサイトに物理演算を使ったインタラクションを入れたかった。最初はMatter.jsを検討したが、バンドルサイズが約300KBと重い。使いたい機能は「重力」「衝突判定」「バウンド」だけ。

必要な機能だけを180行で実装することにした。

設計の概要

Body型と8つの関数で構成される、プレーンオブジェクトベースの物理エンジンだ。クラスを使わずReact stateとの相性を最優先にした。

// src/utils/physics.ts の構造
type Body = {
  x: number
  y: number
  vx: number
  vy: number
  radius: number
  mass: number
  restitution: number
}

function applyGravity(body: Body, dt: number) {
  body.vy += 980 * dt // 9.8m/s² をピクセルスケールに
}

function detectCollision(a: Body, b: Body): boolean {
  const dx = a.x - b.x
  const dy = a.y - b.y
  const dist = Math.sqrt(dx * dx + dy * dy)
  return dist < a.radius + b.radius
}

パフォーマンスの工夫

Grid空間分割・固定タイムステップ・Canvas/DOM使い分けの3つで、200パーティクルでも60fpsを維持できる。

1. 空間分割(Grid-based)

全ペアの衝突判定は O(n²) になるので、画面をグリッドに分割して近傍のみチェック:

function buildGrid(bodies: Body[], cellSize: number) {
  const grid = new Map<string, Body[]>()
  for (const body of bodies) {
    const key = `\${Math.floor(body.x / cellSize)},\${Math.floor(body.y / cellSize)}`
    let bucket = grid.get(key)
    if (!bucket) {
      bucket = []
      grid.set(key, bucket)
    }
    bucket.push(body)
  }
  return grid
}

2. requestAnimationFrame + 固定タイムステップ

可変フレームレートでも物理演算が安定するよう、固定タイムステップで積分:

const FIXED_DT = 1 / 60
let accumulator = 0

function gameLoop(timestamp: number) {
  const dt = (timestamp - lastTime) / 1000
  accumulator += dt

  while (accumulator >= FIXED_DT) {
    updatePhysics(FIXED_DT)
    accumulator -= FIXED_DT
  }

  render()
  requestAnimationFrame(gameLoop)
}

3. Canvas vs DOM の使い分け

要素 描画方法 理由
パーティクル(100+個) Canvas DOM操作のオーバーヘッド回避
バッジ(〜20個) DOM + CSS Transform アクセシビリティ・テキスト選択
気泡シート Canvas タッチイベントの応答性

結果

  • バンドルサイズ: Matter.js 300KB → 自前実装 4KB(gzip後)
  • 60fps維持: 200個のパーティクルでも安定
  • Lighthouse Performance: 98点

小さなスコープなら、ライブラリを使わない選択肢も十分にある。

使った道具たち

記事末尾の「道具棚」セクションに、この実装で参考にした書籍を置いた。物理シミュレーションの基礎を学ぶなら、まずはこの2冊がおすすめ。

よくある質問

Q. ポートフォリオサイトに物理エンジンを自作するのは現実的ですか?
使う機能が「重力・衝突判定・バウンド」に限られるなら現実的。180行で実装でき、Matter.jsの300KBを4KBに削減できた。
Q. 物理演算で60fpsを維持するにはどうすればよいですか?
Grid空間分割でO(n²)を軽減し、固定タイムステップで物理演算を安定させ、Canvas/DOMをエンティティ数で使い分けることで200パーティクルでも60fpsを維持できる。
Q. Canvas APIとDOMの使い分け基準は何ですか?
パーティクル100個以上はCanvas、バッジ20個程度はDOM + CSS Transformを使う。アクセシビリティとテキスト選択が必要なものはDOM。