Using Matter.js to render to the DOM or React
Asked Answered
I

1

4

I want to render custom HTML elements as Bodies in Matter.js. I am using it in React which adds a bit of complexity but it's irrelevant to my issue.

I've searched a lot and the only example I found was this one here, which seems to use querySelector to select the elements that live in the HTML code then somehow use them inside the rectangle shapes.

The part that seems to be doing the job is the following:

var bodiesDom = document.querySelectorAll('.block');
var bodies = [];
for (var i = 0, l = bodiesDom.length; i < l; i++) {
    var body = Bodies.rectangle(
        VIEW.centerX,
        20, 
        VIEW.width*bodiesDom[i].offsetWidth/window.innerWidth, 
        VIEW.height*bodiesDom[i].offsetHeight/window.innerHeight
    );
    bodiesDom[i].id = body.id;
    bodies.push(body);
}
World.add(engine.world, bodies);

(the VIEW variables there could be just random numbers as they define the shape)

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

Ideally, I want to have complex HTML elements interacting with the physics world (like a small box with buttons, etc).

Any ideas on how this could be achieved? Or, can you explain the method used in the example that seems to have managed it?

Infinite answered 15/9, 2020 at 16:31 Comment(0)
E
10

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.20.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 engineRef = useRef();

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

    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);
      Matter.Engine.clear(engineRef.current);
      // see https://github.com/liabru/matter-js/issues/564
      // for additional cleanup if using MJS renderer/runner
    };
  }, []);

  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.20.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.

Erythroblastosis answered 18/12, 2020 at 9:4 Comment(2)
great examples, thanks, is it possible to somehow display the debug flags of the canvas version, or in other words, to render the canvas version as a transparent overlay version in parallel?Emmie
@Emmie Sure, there's nothing stopping you from creating any number of custom renderers and using them either alongside, on top of, or in any combination with Matter.js' internal renderer. Think of Matter.js as exposing an array of body positions which you can do whatever you want with--render to 100 canvases, overlayed with DOM objects, with the internal MJS renderer running as well, all stacked.Erythroblastosis

© 2022 - 2024 — McMap. All rights reserved.