How to make items draggable and clickable?

1.4k views Asked by At

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.

1

There are 1 answers

0
ggorlen On

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>