How to Build a Matter.js Visual Level Editor
Building an in-game level editor using Matter.js requires combining a physics simulation with a direct-manipulation user interface. This guide outlines how to implement an interactive system that allows designers to spawn, select, move, and visually resize rigid bodies directly on a HTML5 canvas. You will learn how to capture pointer events, manipulate Matter.js body dimensions dynamically, render selection bounds with transform handles, and serialize the resulting layout into reusable JSON level data.
1. Canvas Setup and Interaction States
To edit physics entities visually, decouple editing interactions from
the active physics simulation. Pause or disable the physics update loop
(Runner.stop(runner)) while in edit mode so bodies remain
stationary during manipulation.
Set up an interaction state machine to track user actions:
- IDLE: Awaiting user input.
- PLACING: Spawning a new body at the cursor position.
- DRAGGING: Moving a selected body.
- RESIZING: Dragging a specific scale handle to alter dimensions.
2. Selecting Bodies with Spatial Queries
Matter.js provides built-in query methods to detect which body lies
under the cursor. Use Matter.Query.point() to detect
clicks:
function getBodyAtMouse(engine, mousePosition) {
const allBodies = Matter.Composite.allBodies(engine.world);
const clickedBodies = Matter.Query.point(allBodies, mousePosition);
// Filter out boundary walls or non-editable utility bodies
return clickedBodies.find(body => !body.isStaticLevelBoundary) || null;
}When a body is clicked, store it as the activeBody in
your editor state.
3. Rendering Bounding Boxes and Resize Handles
Once a body is selected, draw an overlay on a dedicated UI canvas layer directly above the Matter.js render canvas.
- Calculate Dimensions: For a box, use the body's
local width and height, or derive the bounding box using
body.bounds. - Draw Transform Gizmo: Render an outline around the body and draw small interactive squares (handles) on the four corners and the midpoints of the edges.
- Handle Hit Detection: Before checking for body
selection on
pointerdown, check if the click intersects any handle's hit radius. If it hits a handle, enter the RESIZING state and record which handle was grabbed (e.g.,'top-left','right').
4. Resizing Matter.js Bodies
Matter.js bodies are defined by geometric vertices. Unlike standard game engine sprites, changing a body's width and height dynamically requires updating its vertex data.
Method A: Using
Matter.Body.scale()
The simplest approach is applying scale factors relative to the current size:
function resizeBodyProportional(body, scaleFactorX, scaleFactorY) {
Matter.Body.scale(body, scaleFactorX, scaleFactorY);
}Note: Continuous scaling can introduce floating-point
inaccuracies over time, as Body.scale modifies existing
vertices incrementally.
Method B: Rebuilding Vertices from Exact Dimensions
For a precise level editor, store the absolute width and
height in a custom property on the body (e.g.,
body.customData.width). When dragging an edge or corner
handle:
- Calculate the new width and height based on pointer displacement.
- Clamp values to a minimum threshold (e.g., 10px) to prevent inverted geometry.
- Generate new vertices or recreate the body definition:
function setRectangleDimensions(body, newWidth, newHeight) {
const newVertices = Matter.Bodies.rectangle(
body.position.x,
body.position.y,
newWidth,
newHeight
).vertices;
Matter.Body.setVertices(body, newVertices);
// Update internal dimensions
body.customData.width = newWidth;
body.customData.height = newHeight;
}Always update the body's position offset if the resize anchor is a corner rather than the center, ensuring the opposite edge remains stationary during the drag.
5. Placing New Bodies
Implement a tool palette for spawning primitive shapes:
- Select a tool (e.g., "Box", "Circle", "Platform").
- On canvas
pointerdown, instantiate a new body usingMatter.Bodies.rectangle(x, y, width, height, options). - Set
isStatic: trueby default for level architecture, or leave it dynamic for interactive props. - Add the body to the world:
Matter.Composite.add(engine.world, newBody). - Immediately set the new body as the
activeBodyto allow fine-tuning position and scale.
6. Level Serialization and Deserialization
To save levels, map the active Matter.js world bodies to a clean, serializable JSON format:
function exportLevel(engine) {
const bodies = Matter.Composite.allBodies(engine.world);
const levelData = bodies.map(body => ({
type: body.customData?.type || 'rectangle',
position: { x: body.position.x, y: body.position.y },
angle: body.angle,
isStatic: body.isStatic,
width: body.customData?.width,
height: body.customData?.height,
radius: body.circleRadius || null
}));
return JSON.stringify(levelData, null, 2);
}To load the level, iterate over the parsed JSON array, instantiate
the corresponding Matter.Bodies factory methods with the
stored transform and dimension data, and populate
engine.world.