Using Matter.js to render to the DOM or React

However, I cannot understand how to pass an HTML element inside the Bodies rectangle as in the example above.

This isn’t quite what the example does. When running headlessly, Matter.js handles the physics without having any idea how it’s rendered. Per animation frame, you can use the current positions of the MJS bodies and reposition your elements (or draw on canvas, etc) to reflect MJS’s view of the world.

Providing a root element to MJS does seem to break the one-way data flow, but this is only for informing MJS about events like mouse position and clicks–not to be confused with rendering.

Here’s a minimal example to hopefully make this clearer:

const engine = Matter.Engine.create();  
const box = {
  body: Matter.Bodies.rectangle(150, 0, 40, 40),
  elem: document.querySelector("#box"),
  render() {
    const {x, y} = this.body.position;
    this.elem.style.top = `${y - 20}px`;
    this.elem.style.left = `${x - 20}px`;
    this.elem.style.transform = `rotate(${this.body.angle}rad)`;
  },
};
const ground = Matter.Bodies.rectangle(
  200, 200, 400, 120, {isStatic: true}
);
const mouseConstraint = Matter.MouseConstraint.create(
  engine, {element: document.body}
);
Matter.Composite.add(
  engine.world, [box.body, ground, mouseConstraint]
);
(function rerender() {
  box.render();
  Matter.Engine.update(engine);
  requestAnimationFrame(rerender);
})();
#box {
  position: absolute;
  background: #111;
  height: 40px;
  width: 40px;
  cursor: move;
}

#ground {
  position: absolute;
  background: #666;
  top: 140px;
  height: 120px;
  width: 400px;
}

html, body {
  position: relative;
  height: 100%;
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div id="box"></div>
<div id="ground"></div>

React

React changes the workflow but the fundamental concept is the same–MJS body data flows one-directionally from the MJS back-end to the rendering front-end, so from MJS’ perspective, everything is identical as the vanilla example above. Most of the work is setting up refs and useEffect properly for use with requestAnimationFrame.

const {Fragment, useEffect, useRef} = React;

const Scene = () => {
  const requestRef = useRef();
  const boxRef = useRef();
  const groundRef = useRef();

  const animate = () => {
    const engine = Matter.Engine.create();

    const box = {
      body: Matter.Bodies.rectangle(150, 0, 40, 40),
      elem: boxRef.current,
      render() {
        const {x, y} = this.body.position;
        this.elem.style.top = `${y - 20}px`;
        this.elem.style.left = `${x - 20}px`;
        this.elem.style.transform = `rotate(${this.body.angle}rad)`;
      },
    };
    const ground = Matter.Bodies.rectangle(
      200, // x
      200, // y
      400, // w
      120, // h
      {isStatic: true}
    );
    const mouseConstraint = Matter.MouseConstraint.create(
      engine,
      {element: document.body}
    );
    Matter.Composite.add(engine.world, [
      box.body,
      ground,
      mouseConstraint,
    ]);

    (function rerender() {
      box.render();
      Matter.Engine.update(engine);
      requestRef.current = requestAnimationFrame(rerender);
    })();
  };

  useEffect(() => {
    animate();
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  return (
    <Fragment>
      <div id="box" ref={boxRef}></div>
      <div id="ground" ref={groundRef}></div>
    </Fragment>
  );
};

ReactDOM.createRoot(document.querySelector("#app")).render(
  <Scene />
);
#box {
  position: absolute;
  background: #111;
  height: 40px;
  width: 40px;
  cursor: move;
}

#ground {
  position: absolute;
  top: 140px;
  height: 120px;
  width: 400px;
  background: #666;
}

html, body {
  position: relative;
  height: 100%;
  margin: 0;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div id="app"></div>

Note that these are only proofs-of-concept. More work setting up abstractions will likely be necessary before they can support more involved use cases.

Leave a Comment