Generate Matter.js Vertices from Image Alpha
Converting a raster image's transparent silhouette into physics
collision boundaries in Matter.js requires extracting pixel alpha
values, tracing the boundary contour, simplifying the resulting vertex
path, and decomposing non-convex shapes into convex polygons. By
rendering a sprite to an offscreen HTML5 canvas, you can sample the
alpha channel using getImageData, identify the outer
perimeter using a contour tracing algorithm such as Marching Squares,
reduce vertex density with the Ramer-Douglas-Peucker algorithm, and
generate a composite body using
Matter.Bodies.fromVertices.
1. Extract Alpha Data via Offscreen Canvas
Matter.js operates purely on geometric vector coordinates, not raster
data. To read the alpha channel of an image, draw it to an offscreen
<canvas> element and access its raw pixel array:
function getAlphaMatrix(image, alphaThreshold = 128) {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
const grid = [];
for (let y = 0; y < canvas.height; y++) {
const row = [];
for (let x = 0; x < canvas.width; x++) {
// The 4th element in every 4-byte chunk represents the Alpha channel
const alphaIndex = (y * canvas.width + x) * 4 + 3;
row.push(data[alphaIndex] >= alphaThreshold ? 1 : 0);
}
grid.push(row);
}
return { grid, width: canvas.width, height: canvas.height };
}2. Trace the Silhouette Boundary
Once pixel states are mapped into a binary grid (solid vs. empty), run a 2D contour tracing algorithm to find the ordered sequence of outer perimeter coordinates.
The Marching Squares algorithm evaluates 2x2 cells of adjacent pixels to produce continuous line segments along the boundary. Alternatively, a radial sweep or Moore-Neighbor tracing can follow the boundary clockwise or counter-clockwise.
A lightweight Moore-Neighbor boundary tracer processes the grid as follows:
function traceContour(grid, width, height) {
const points = [];
let start = null;
// Locate the first solid boundary pixel
findStart: for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (grid[y][x] === 1) {
start = { x, y };
break findStart;
}
}
}
if (!start) return points;
const directions = [
{ x: 0, y: -1 }, { x: 1, y: -1 }, { x: 1, y: 0 }, { x: 1, y: 1 },
{ x: 0, y: 1 }, { x: -1, y: 1 }, { x: -1, y: 0 }, { x: -1, y: -1 }
];
let current = { ...start };
let backtrackDir = 0;
do {
points.push({ x: current.x, y: current.y });
let found = false;
for (let i = 0; i < 8; i++) {
const dirIdx = (backtrackDir + i) % 8;
const nextX = current.x + directions[dirIdx].x;
const nextY = current.y + directions[dirIdx].y;
if (nextX >= 0 && nextX < width && nextY >= 0 && nextY < height && grid[nextY][nextX] === 1) {
current = { x: nextX, y: nextY };
backtrackDir = (dirIdx + 5) % 8;
found = true;
break;
}
}
if (!found) break;
} while (current.x !== start.x || current.y !== start.y);
return points;
}3. Simplify Vertices
Pixel-by-pixel tracing creates redundant, dense vertices along flat and shallow curves, resulting in severe performance penalties during physics calculations.
Use the Ramer-Douglas-Peucker (RDP) algorithm to prune points within an allowable epsilon threshold:
function simplifyPoints(points, epsilon = 1.5) {
if (points.length <= 2) return points;
function perpendicularDistance(pt, lineStart, lineEnd) {
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const mag = Math.hypot(dx, dy);
if (mag === 0) return Math.hypot(pt.x - lineStart.x, pt.y - lineStart.y);
return Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x) / mag;
}
function rdp(pts) {
let maxDist = 0;
let index = 0;
const end = pts.length - 1;
for (let i = 1; i < end; i++) {
const dist = perpendicularDistance(pts[i], pts[0], pts[end]);
if (dist > maxDist) {
maxDist = dist;
index = i;
}
}
if (maxDist > epsilon) {
const left = rdp(pts.slice(0, index + 1));
const right = rdp(pts.slice(index));
return left.slice(0, -1).concat(right);
}
return [pts[0], pts[end]];
}
return rdp(points);
}4. Enable Convex Decomposition
Physics engines rely on the Separating Axis Theorem (SAT), which only functions correctly with convex shapes. Arbitrary silhouettes are typically concave.
To resolve this, install and enable the poly-decomp
library so Matter.js can split concave polygons into an assembly of
convex parts:
// Provide poly-decomp to Matter.js Common utility
Matter.Common.setDecomp(window.decomp);5. Construct the Matter.js Rigid Body
Format the processed coordinates into Matter.js vector objects and
instantiate the body via Matter.Bodies.fromVertices:
function createBodyFromImage(image, x, y, options = {}) {
// 1. Get binary grid
const { grid, width, height } = getAlphaMatrix(image);
// 2. Trace boundary
const rawVertices = traceContour(grid, width, height);
// 3. Simplify point set
const simplifiedVertices = simplifyPoints(rawVertices, 2.0);
// 4. Generate Body (poly-decomp handles internal splitting)
const body = Matter.Bodies.fromVertices(x, y, [simplifiedVertices], {
...options,
render: {
sprite: {
texture: image.src,
xOffset: 0,
yOffset: 0
}
}
});
return body;
}Ensure that vertex ordering remains clockwise to prevent inverted collision normals, and tune the simplification epsilon to strike the necessary balance between boundary precision and collision computation overhead.