Matter.js cursor – how to change cursor when hovering a body?

51 Views Asked by At

On my website, I create a matter.js world where I have a bunch of draggable bodies. Any simple ideas on how to create a grab cursor when hovering over the bodies I had, failed. Without it, there is no indication that the elements are interactive. Do you know how to change the cursor in matter.js?

I've added html elements as bodies to matter.js, so I tried to force css cursor settings on them. Then I tried to change the cursor on mouseenter, then I tried to do it with matter.js mouse constraint, all with no effect

mConstraint = MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: {
      visible: true,
    },
  },
});

 World.add(world, mConstraint);

Events.on(mConstraint, "mousemove", function (event) {
  var foundBodies = Query.point(bodies, event.mouse.position);
  if (foundBodies.length > 0) {
    console.log("foundBodies", foundBodies);
    foundBodies.forEach(function (body) {
      body.render.cursor = "grab"; 
    });
  }
});
1

There are 1 best solutions below

1
ggorlen On

MJS's renderer is mainly intended for prototyping, so your use case may be pushing the limits of the internal renderer. Depending on your broader application context, it may make sense to use a custom renderer. If you use the DOM as your renderer, CSS styling such as the cursor becomes natural. See this example.

For using the prototyping renderer, your move code should work more or less, but add:

render.canvas.style.cursor = "default";

to use the default cursor, and

render.canvas.style.cursor = "grab";

to use the grabbing cursor. Since there's only one cursor, you don't need to set this on each body.

Here's a more complex example that uses "grabbing" and handles drags:

const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
  element: document.body,
  engine,
  options: {...map, wireframes: false},
});
const player = Matter.Bodies.rectangle(
  // x y w h
  map.width / 2, map.height / 2, 35, 35,
  {frictionAir: 1, render: {fillStyle: "#0f0"}}
);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  element: document.body
});
const boxes = [...Array(20)].map(() =>
  Matter.Bodies.rectangle(
    Math.random() * map.width,
    Math.random() * map.height,
    20, 20, {frictionAir: 1}
  )
);
Matter.Composite.add(engine.world, [
  mouseConstraint,
  player,
  ...boxes
]);

const touchingMouse = () =>
  Matter.Query.point(
    engine.world.bodies, // or just [player]
    mouseConstraint.mouse.position
  ).length > 0;

Matter.Events.on(engine, "beforeUpdate", event => {
  if (!mouseDown && !touchingMouse()) {
    render.canvas.style.cursor = "default";
  }
  else if (touchingMouse()) {
    render.canvas.style.cursor = mouseDown ? "grabbing" : "grab";
  }
});
let mouseDown = false;
render.canvas.addEventListener("mousedown", event => {
  mouseDown = true;

  if (touchingMouse()) {
    render.canvas.style.cursor = "grabbing";
  } else {
    render.canvas.style.cursor = "default";
  }
});
render.canvas.addEventListener("mouseup", event => {
  mouseDown = false;

  if (touchingMouse()) {
    render.canvas.style.cursor = "grab";
  } else {
    render.canvas.style.cursor = "default";
  }
});
Matter.Render.run(render);
Matter.Runner.run(Matter.Runner.create(), engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

Another approach that involves a bit more lifting but might come in handy for certain use cases is to have an invisible sensor body positioned under the mouse on each frame which is used to detect interactions on movement:

const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
  element: document.body,
  engine,
  options: {...map, wireframes: false},
});
const player = Matter.Bodies.rectangle(
  // x y w h
  map.width / 2, map.height / 2, 35, 35,
  {frictionAir: 1, render: {fillStyle: "#0f0"}}
);
const boxes = [...Array(20)].map(() =>
  Matter.Bodies.rectangle(
    Math.random() * map.width,
    Math.random() * map.height,
    20, 20, {frictionAir: 1}
  )
);
const mouseBody = Matter.Bodies.rectangle(
  // x y w h
  map.width / 2, map.height / 2, 1, 1, {
    isStatic: true,
    collisionFilter: {mask: 0b01},
    isSensor: true,
    render: {fillStyle: "transparent"},
  }
);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  element: document.body,
  collisionFilter: {category: 0b10},
});
Matter.Composite.add(engine.world, [
  mouseConstraint,
  player,
  mouseBody,
  ...boxes,
]);
Matter.Events.on(engine, "beforeUpdate", event => {
  Matter.Body.setPosition(
    mouseBody,
    mouseConstraint.mouse.position
  );
});
let mouseDown = false;
Matter.Events.on(engine, "collisionEnd", event => {
  if (
    !mouseDown &&
    !Matter.Collision.collides(mouseBody, player)
  ) {
    render.canvas.style.cursor = "default";
  }
});
Matter.Events.on(engine, "collisionStart", event => {
  if (
    Matter.Collision.collides(mouseBody, player)
  ) {
    render.canvas.style.cursor = mouseDown ? "grabbing" : "grab";
  }
});
render.canvas.addEventListener("mousedown", event => {
  mouseDown = true;

  if (Matter.Collision.collides(mouseBody, player)) {
    render.canvas.style.cursor = "grabbing";
  } else {
    render.canvas.style.cursor = "default";
  }
});
render.canvas.addEventListener("mouseup", event => {
  mouseDown = false;

  if (Matter.Collision.collides(mouseBody, player)) {
    render.canvas.style.cursor = "grab";
  } else {
    render.canvas.style.cursor = "default";
  }
});
Matter.Render.run(render);
Matter.Runner.run(Matter.Runner.create(), engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

This is set up such that the cursor only changes when placed on the green "player" box Using [player] in the above touchingMouse function is another way to achieve this. Either way, you might want to disable mouse interactions with other bodies so it's more consistent with the cursor.