Next.jsでMatter.jsを使う
Next.jsでMatter.jsを使う
導入
趣味の方でNext.jsでMatter.jsを使う機会があったのですがなかなか参考となる記事がなく少し詰まったのでここで自分の備忘録としてまとめておきます。
この方法がベストプラクティスかはわかりませんが、少しでも参考になればと思ってます!!
Matter.jsとは
Matter.jsはJavaScriptで書かれた2Dの物理エンジンです。
JavaScriptなどで物理演算の実装を行うためのライブラリは他にはGSAPやPhysicsJSなどがありますが、Matter.jsはその中でも軽量でかつ、大量のオブジェクトを扱えるライブラリです。(しかも使いやすい!!)
また、Matter.jsはTypeScriptにも対応しているのも大きなポイントです。
Matter.jsは、オブジェクトの衝突判定、重力、摩擦、バネ、連結などを簡単に実装できます。
公式ドキュメントにもたくさんのサンプルがあるので、ぜひ一度見てみてください!!
Matter.js - a 2D rigid body JavaScript physics engine - brm·io
Matter.js is a 2D physics engine for the web github · docs · see demos → · matter-js. Rigid bodies; Compound bodies; Composite bodies; Concave and convex ...
動作環境
今回の動作環境は以下の通りです。
- Next.js 15.3.2
- TypeScript 5.8.3
- Matter.js 0.20.0
セットアップ
アプリのセットアップを行っていきます。
今回の例ではnpmを使用しています。他のパッケージマネージャーを使用しても問題ありません!
公式ドキュメントに他のパッケージマネージャーでのインストール方法も記載されているので、そちらを参考にしてください。
npx create-next-app@latest --typescript matter-next-tutorial
今回はApp Routerを使用しました。(srcディレクトリは使いません。)
次にMatter.jsをインストールします。
npm install matter-js
型の定義ファイルもインストールします。
npm install --save @types/matter-js
これでセットアップは完了です!
Matter.jsの基本
Matter.jsについて簡単に説明します。
Matter.jsは、以下の主要なモジュールで構成されています。
- Engine: 物理演算を行うエンジンです。
- Render: 物理世界を描画するためのモジュールです。
- Runner: エンジンを実行するためのモジュールです。世界の更新を定期的に行います。
- Bodies: 物理エンジンで扱うモジュールです。矩形、円、ポリゴンなどの形状を持つことができます。
- World: 物理エンジンが管理する仮想空間で、オブジェクトを配置し物理演算を行います。World内の結果がcanvasに描画されます。
基本的にはこれらのプロパティなどを組み合わせて、物理シミュレーションを行います。
Next.jsでの使い方
ではここからは実際にNext.jsでMatter.jsを使っていきます。
Matter.js自体の使い方は色々なサンプルがあるので、ここではNext.jsでの使い方に焦点を当てて説明していきます。
おすすめのMatter.jsのサンプルについても後述の参考資料で紹介します。
Matter.jsはブラウザのDOMを直接操作するため、クライアントサイドでのみ動作します。
Next.jsでMatter.jsを使う際に注意が必要なのはレンダリングのタイミングです。
ここがNext.jsとMatter.jsの組み合わせで少し難しいところです。
不要な再レンダリングを避けるために、useEffectを使用してMatter.jsの初期化を適切に行います。
useEffect(() => {
const canvas = canvasRef.current;
const engine = Engine.create()
const runner = Runner.create();
const world = engine.world;
...
また、メモリリークやパフォーマンスの問題を避けるために、useEffectのクリーンアップ関数を使用してエンジンを停止する必要があります。
...
return () => {
console.log("Cleaning up Matter.js resources");
Render.stop(render);
Runner.stop(runnerRef.current as Runner);
Engine.clear(engineRef.current as Engine);
render.canvas.remove();
render.textures = {};
Composite.clear(worldRef.current as World, false);
}
}, []);
各モジュールもuseRefを使用して不要な再レンダリングを避けるようにします。
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const engineRef = useRef<Engine | null>(null);
const runnerRef = useRef<Runner | null>(null);
const worldRef = useRef<World | null>(null);
DOM要素とMatter.jsのcanvasを連結させるためにuseRefでcanvas要素を参照します。
const canvasRef = useRef<HTMLCanvasElement | null>(null);
‥
return (
<div>
<canvas ref={canvasRef} />
</div>
);
これらを踏まえた簡単なMatter.jsのサンプルがこちらになります。
"use client"
import {
Engine,
Render,
Bodies,
Runner,
Composite,
Common,
World
} from 'matter-js'
import { useEffect, useRef } from 'react';
export default function Home() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const engineRef = useRef<Engine | null>(null);
const runnerRef = useRef<Runner | null>(null);
const worldRef = useRef<World | null>(null);
const pixelRatioRef = useRef(1);
useEffect(() => {
const engine = Engine.create()
engineRef.current = engine;
worldRef.current = engine.world;
const canvas = canvasRef.current;
if (!canvas) return;
pixelRatioRef.current = window.devicePixelRatio;
const render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
background: 'transparent',
wireframes: false,
hasBounds: true,
pixelRatio: pixelRatioRef.current,
}
})
Render.run(render);
const box = Bodies.rectangle(400, 200, 80, 80, {
restitution: 0.5, friction: 0.5, angle: Common.random(0, 360)});
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
Composite.add(worldRef.current, [box, ground]);
const runner = Runner.create();
runnerRef.current = runner;
Runner.run(runner, engine);
return () => {
console.log("Cleaning up Matter.js resources");
Render.stop(render);
Runner.stop(runnerRef.current as Runner);
Engine.clear(engineRef.current as Engine);
render.canvas.remove();
render.textures = {};
Composite.clear(worldRef.current as World, false);
}
}, []);
return (
<canvas ref={canvasRef} style={{ display: 'block', width: '100vw', height: '100vh' }} />
);
}
このコードのページを実行するとcanvas内でMatter.jsの物理エンジンが動作します。

コードの解説
サンプルコードについて解説していきます。 まずは必要なモジュールをインポートします。
import {
Engine,
Render,
Bodies,
Runner,
Composite,
Common,
World
} from 'matter-js'
import { useEffect, useRef } from 'react';
useRefを用いて各モジュールを参照するための変数を定義しています。 このあとでuseEffect内で作成した各モジュールと紐づけます。
最後にCSSピクセル解像度に対するディスプレイの物理的なピクセルの比率を取得するための変数を定義します。 これにより、高DPIディスプレイでも適切に描画されるようになります。
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const engineRef = useRef<Engine | null>(null);
const runnerRef = useRef<Runner | null>(null);
const worldRef = useRef<World | null>(null);
const pixelRatioRef = useRef(1);
useEffectを用いてMatter.jsの初期化をしています。
ここでuseRefとの紐づけも行います。
renderのoptionsで色々と設定をすることができます。
ここで説明すると長くなってしまうので、公式ドキュメントを参照してください!!
最後にrenderを起動させています。
useEffect(() => {
const engine = Engine.create()
engineRef.current = engine;
worldRef.current = engine.world;
const canvas = canvasRef.current;
if (!canvas) return;
const render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
background: 'transparent',
wireframes: false,
hasBounds: true,
pixelRatio: pixelRatioRef.current,
}
})
Render.run(render);
正方形と地面を作成し、Composite.addでworldに追加しています。
const box = Bodies.rectangle(400, 200, 80, 80, {
restitution: 0.5, friction: 0.5, angle: Common.random(0, 360)});
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
Composite.add(worldRef.current, [box, ground]);
Runnerの作成とRunner.runでエンジンを実行しています。
const runner = Runner.create();
runnerRef.current = runner;
Runner.run(runner, engine);
クリーンアップ関数の定義です。
モジュールを正常に停止させています。
[]なので、コンポーネントがアンマウントされるときに一度だけ実行されます。
return () => {
console.log("Cleaning up Matter.js resources");
Render.stop(render);
Runner.stop(runnerRef.current as Runner);
Engine.clear(engineRef.current as Engine);
render.canvas.remove();
render.textures = {};
Composite.clear(worldRef.current as World, false);
}
}, []);
最後にcanvasをレンダリングしています。
return (
<canvas ref={canvasRef} style={{ display: 'block', width: '100vw', height: '100vh' }} />
);
まとめ
今回はNext.jsでMatter.jsを使う方法について紹介しました。
Reactや生のJavascriptでの記事はある程度あったのですが、Next.jsでの使用例が少なかったので割と苦労しました。
useEffectやuseRefなどのhooks周りの復習にもなったので、個人的にはかなりいい勉強になりました!
だいぶ多くの先人さんたちのコードに助けられました、、!(技術記事ってありがたい)
今回のコードのサンプルはGitHubに公開していますので、ぜひ参考にしてみてください。
R1N-K0/next-matter-js-tutorial
Next.js上で Matter.js を動かすシンプルなチュートリアル。基本的な2D物理シミュレーションのサンプルです。
📚 参考資料
Matter.js - a 2D rigid body JavaScript physics engine - brm·io
Matter.js is a 2D physics engine for the web github · docs · see demos → · matter-js. Rigid bodies; Compound bodies; Composite bodies; Concave and convex ...
【Matter.js】物理演算で遊べるサイトを作る【エイプリルフール】
matter.jsでつくるタワーバトルゲーム
Matter.jsをかじる本
物理エンジンライブラリ「matter.js」を使って要素が動くリッチなアニメーションを作ってみた
Create Interactive Pill Animation Using Matter.js
Recently, our team at createxp undertook a fascinating challenge: developing a portfolio website for...
saitodisse/matterjs-nextjs
Contribute to saitodisse/matterjs-nextjs development by creating an account on GitHub.