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.