1

I'm new to Matter JS, so please bear with me. I have the following code I put together from demos and other sources to suit my needs:

function biscuits(width, height, items, gutter) {
  const {
    Engine,
    Render,
    Runner,
    Composites,
    MouseConstraint,
    Mouse,
    World,
    Bodies,
  } = Matter

  const engine = Engine.create()
  const world = engine.world

  const render = Render.create({
    element: document.getElementById('canvas'),
    engine,
    options: {
      width,
      height,
      showAngleIndicator: true,
    },
  })

  Render.run(render)

  const runner = Runner.create()
  Runner.run(runner, engine)

  const columns = media({ bp: 'xs' }) ? 3 : 1
  const stack = Composites.stack(
    getRandom(gutter, gutter * 2),
    gutter,
    columns,
    items.length,
    0,
    0,
    (x, y, a, b, c, i) => {
      const item = items[i]

      if (!item) {
        return null
      }

      const {
        width: itemWidth,
        height: itemHeight,
      } = item.getBoundingClientRect()

      const radiusAmount = media({ bp: 'sm' }) ? 100 : 70
      const radius = item.classList.contains('is-biscuit-4')
        ? radiusAmount
        : 0
      const shape = item.classList.contains('is-biscuit-2')
        ? Bodies.circle(x, y, itemWidth / 2)
        : Bodies.rectangle(x, y, itemWidth, itemHeight, {
            chamfer: { radius },
          })

      return shape
    }
  )

  World.add(world, stack)

  function positionDomElements() {
    Engine.update(engine, 20)

    stack.bodies.forEach((block, index) => {
      const item = items[index]
      const xTrans = block.position.x - item.offsetWidth / 2 - gutter / 2
      const yTrans = block.position.y - item.offsetHeight / 2 - gutter / 2

      item.style.transform = `translate3d(${xTrans}px, ${yTrans}px, 0) rotate(${block.angle}rad)`
    })

    window.requestAnimationFrame(positionDomElements)
  }

  positionDomElements()

  World.add(world, [
    Bodies.rectangle(width / 2, 0, width, gutter, { isStatic: true }),
    Bodies.rectangle(width / 2, height, width, gutter, { isStatic: true }),
    Bodies.rectangle(width, height / 2, gutter, height, { isStatic: true }),
    Bodies.rectangle(0, height / 2, gutter, height, { isStatic: true }),
  ])

  const mouse = Mouse.create(render.canvas)
  const mouseConstraint = MouseConstraint.create(engine, {
    mouse,
    constraint: {
      stiffness: 0.2,
      render: {
        visible: false,
      },
    },
  })

  World.add(world, mouseConstraint)

  render.mouse = mouse

  Render.lookAt(render, {
    min: { x: 0, y: 0 },
    max: { x: width, y: height },
  })
}

I have a HTML list of links that mimics the movements of the items in Matter JS (the positionDomElements function). I'm doing this for SEO purposes and also to make the navigation accessible and clickable.

However, because my canvas sits on top of my HTML (with opacity zero) I need to be able to make the items clickable as well as draggable, so that I can perform some other actions, like navigating to the links (and other events).

I'm not sure how to do this. I've searched around but I'm not having any luck.

Is it possible to have each item draggable (as it already is) AND perform a click event of some kind?

Any help or steer in the right direction would be greatly appreciated.

Michael Giovanni Pumo
  • 14,338
  • 18
  • 91
  • 140
  • 1
    I see the MJS code here but it's not clear to me how this works with the not-shown HTML/CSS code. Can you show a [mcve] that illustrates the problem? I'm also not clear how you're able to drag elements correctly through an opaque canvas (presumably unrelated to MJS canvas?) but can't click. I'd think neither would work, intuitively. – ggorlen Oct 19 '20 at 18:00
  • 1
    I will try to get a working example going to illustrate this. – Michael Giovanni Pumo Oct 19 '20 at 20:17
  • Sounds good, thanks. I looked at it again and I was wondering if you thought you need the canvas to run MJS. If you're just going to hide it with opacity 0, I'd run the MJS engine headlessly and use its body positions to set the positions of your DOM list elements. You can still use the mouse with `Matter.Mouse.setOffset`. – ggorlen Oct 19 '20 at 20:24
  • Oh right that sounds like an idea...I didn't know you could do that. Any decent tutorials on it you know of? Thank you! – Michael Giovanni Pumo Oct 19 '20 at 20:28
  • There's probably many out there. I have an example on [codepen](https://codepen.io/ggorlen/pen/LOwrxX) that runs MJS "headlessly" and just pulls out the body positions as I described above, ticking the engine forward manually in the `requestAnimationFrame`, but I ultimately draw on my own canvas instead of positioning HTML elements. If I have a better idea of what you're going for here I can provide an example that uses a composite stack and an HTML list. – ggorlen Oct 19 '20 at 20:35

1 Answers1

1

It seems like your task here is to add physics to a set of DOM navigation list nodes. You may be under the impression that matter.js needs to be provided a canvas to function and that hiding the canvas or setting its opacity to 0 is necessary if you want to ignore it.

Actually, you can just run MJS headlessly using your own update loop without injecting an element into the engine. Effectively, anything related to Matter.Render or Matter.Runner will not be needed and you can use a call to Matter.Engine.update(engine); to step the engine forward one tick in the requestAnimationFrame loop. You can then position the DOM elements using values pulled from the MJS bodies. You're already doing both of these things, so it's mostly a matter of cutting out the canvas and rendering calls.

Here's a runnable example that you can reference and adapt to your use case.

Positioning is the hard part; it takes some fussing to ensure the MJS coordinates match your mouse and element coordinates. MJS treats x/y coordinates as center of the body, so I used body.vertices[0] for the top-left corner which matches the DOM better. I imagine a lot of these rendering decisions are applicaton-specific, so consider this a proof-of-concept.

const listEls = document.querySelectorAll("#mjs-wrapper li");
const engine = Matter.Engine.create();

const stack = Matter.Composites.stack(
  // xx, yy, columns, rows, columnGap, rowGap, cb
  0, 0, listEls.length, 1, 0, 0,
  (xx, yy, i) => {
    const {x, y, width, height} = listEls[i].getBoundingClientRect();
    return Matter.Bodies.rectangle(x, y, width, height, {
      isStatic: i === 0 || i + 1 === listEls.length
    });
  }
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
  stiffness: 0.5,
  length: 20
});
const mouseConstraint = Matter.MouseConstraint.create(
  engine, {element: document.querySelector("#mjs-wrapper")}
);
Matter.Composite.add(engine.world, [stack, mouseConstraint]);

listEls.forEach(e => {
  e.style.position = "absolute";
  e.addEventListener("click", e =>
    console.log(e.target.textContent)
  );
});

(function update() {
  requestAnimationFrame(update);
  stack.bodies.forEach((block, i) => {
    const li = listEls[i];
    const {x, y} = block.vertices[0];
    li.style.top = `${y}px`;
    li.style.left = `${x}px`;
    li.style.transform = `translate(-50%, -50%) 
                          rotate(${block.angle}rad) 
                          translate(50%, 50%)`;
  });
  Matter.Engine.update(engine);
})();
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
}

body {
  min-width: 600px;
}

#mjs-wrapper {
  /* position this element */
  margin: 1em; 
  height: 100%;
}
#mjs-wrapper ul {
  font-size: 14pt;
  list-style: none;
  user-select: none;
  position: relative;
}
#mjs-wrapper li {
  background: #fff;
  border: 1px solid #555;
  display: inline-block;
  padding: 1em;
  cursor: move;
}
#mjs-wrapper li:hover {
  background: #f2f2f2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

<div id="mjs-wrapper">
  <ul>
    <li>Foo</li>
    <li>Bar</li>
    <li>Baz</li>
    <li>Quux</li>
    <li>Garply</li>
    <li>Corge</li>
  </ul>
</div>
ggorlen
  • 44,755
  • 7
  • 76
  • 106