Question

Setting fillStyle works inconsistently for bodies in Matter.JS

I'm working with Matter.JS for an interactive header graphic on a website. I have some basic letter paths in SVG, which get loaded, converted to shapes and bodies and placed in the world. They get initial colors and properties and they animate fine.

The problem is, I wanted the bodies to be interactive, in that when a user clicks one of the bodies, it can change. I used a MouseConstraint for this and it kind of works, but only for certain properties; setting a fillStyle (color) does only work for one (!) of the bodies (the letter "I") and can't figure out why.

I have set up an example and I'm hoping it's a simple thing to figure out for somebody who knows their stuff. I'm only working with MJS for the first time.

Matter.Common._seed = Math.random().toString().slice(2,10); // random 8 digit number
Matter.use(MatterAttractors);

const engine = Matter.Engine.create();
const currentWidth = document.querySelector('#canvas').offsetWidth * 2;

// letter initial coordinates
const offsets = {
  'B':  [currentWidth/100*20, currentWidth/100*5],
  'I':  [currentWidth/100*50, currentWidth/100*5],
  'G':  [currentWidth/100*70, currentWidth/100*5],
  'B2': [currentWidth/100*20, currentWidth/100*50],
  'A':  [currentWidth/100*40, currentWidth/100*50],
  'N':  [currentWidth/100*60, currentWidth/100*50],
  'D':  [currentWidth/100*80, currentWidth/100*50]
};

var render = Matter.Render.create({
  element: document.querySelector('#canvas'),
  engine: engine,
  options: {
    width: currentWidth, 
    height: currentWidth,
    background: '#222',
    wireframes: false,
    showAngleIndicator: false
  }
});

var select = function (root, selector) {
  return Array.prototype.slice.call(root.querySelectorAll(selector));
};

var loadSvg = function (url) {
  return fetch(url)
    .then(function(response) { return response.text(); })
    .then(function(raw) { return (new window.DOMParser()).parseFromString(raw, 'image/svg+xml'); });
};

// add SVG letters
document.querySelectorAll('#letters path').forEach(function (ele, i) {
  const color = ele.getAttribute('fill');
  const id = ele.getAttribute('id').replace('letter-', '');
  const vertexSets = Matter.Svg.pathToVertices(ele, 30);

  const offsetX = offsets[id][0];
  const offsetY = offsets[id][1];

  var letter = Matter.Bodies.fromVertices(offsetX, offsetY, vertexSets, {
    render: {
      fillStyle: color,
      strokeStyle: color,
      lineWidth: 1,
      opacity: 1
    },
    restitution: 0.7,
    label: 'Letter-' + id
  });

  Matter.Body.rotate(letter, Matter.Common.random(-0.3, 0.3));
  Matter.Body.scale(letter, 1.0, 1.0);
  Matter.Body.setMass(letter, 5.01);

  Matter.Composite.add(engine.world, letter, true);
});

// walls
const wallstyle = { fillStyle: '#0f0' };
var ground = Matter.Bodies.rectangle(currentWidth/2, currentWidth, currentWidth, 10, { isStatic: true, render: wallstyle });
var wallLeft = Matter.Bodies.rectangle(0, currentWidth/2, 10, currentWidth, { isStatic: true, render: wallstyle });
var wallRight = Matter.Bodies.rectangle(currentWidth, currentWidth/2, 10, currentWidth, { isStatic: true, render: wallstyle });

Matter.Composite.add(engine.world, [wallLeft, wallRight, ground]); // [boxA, boxB, ground]

Matter.Render.run(render);

var runner = Matter.Runner.create();
Matter.Events.on(runner, "tick", event => {}); // nothing as of right now
Matter.Runner.run(runner, engine);

// mouse events
const mouseConstraint = Matter.MouseConstraint.create(
  engine, 
  Matter.Mouse.create(render.canvas), 
  {}
);

Matter.Events.on(mouseConstraint, "mousedown", event => {
  const target = event.source.body;
  if (target) {
    // why is the fillStyle only working for the letter "I"?
    target.render.fillStyle = Matter.Common.choose(['#f19648', '#f5d259', '#f55a3c']);

    // applying force works for some reason
    const dir = Matter.Common.random(-0.3, 0.3);
    Matter.Body.applyForce(target, { x: target.position.x, y: target.position.y }, { x: dir, y: -0.2 });
  }
});
html {
  background: black;
  color: #fff;
}

#canvas {
  margin: 2rem auto;
  width: 40vw;
  max-width: 40rem;
  aspect-ratio: 1 / 1;
  outline: 1px solid #0ff;
}

#canvas canvas {
  display: block;
  width: 100%;
  height: auto;
}

#letters { display: none; }
<div id="canvas"></div>

<!-- SVG letter shapes, to be loaded in JS -->
<div id="letters">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" viewBox="0 0 200 200" xml:space="preserve">
    <path id="letter-B" fill="#00A15D" d="M187.5,101c3.3-5.6,4.9-12.1,4.9-19.3v-81h-62.7h-30H7.6v198h92.1v-36.4h-1.8v-30.6h1.8V97.3 h-1.8V36.9h1.9h2.4v56.8c0,2.5-0.8,3.7-2.4,3.7h0v34.3h2.4v26.1c0,1.2-0.2,2.3-0.5,3.2c-0.4,0.9-1,1.3-1.8,1.3h0v36.4h53.1  c12.7,0,22.4-3.6,29.3-10.7c6.9-7.1,10.3-16.5,10.3-28.1v-48.8h-13.5C182.4,108.3,185.3,104.9,187.5,101z" />
    <path id="letter-I" fill="#00A15D" d="M54.9 1h90.1v198H54.9Z" />
    <path id="letter-G" fill="#00A15D" d="M7.5,199V39.8c0-11.6,3.4-21,10.3-28.1C24.7,4.6,34.4,1,46.9,1h145.6v38.5H102   c-1.4,0-2.5,0.5-3.2,1.5c-0.7,1-1.1,2.1-1.1,3.3v117.2h4.2V81.3V43.8h90.4V199H7.5z" />
    <path id="letter-B2" fill="#F6B9AA" d="M152,102.6c3.3-5.6,4.9-12.1,4.9-19.3V1H43.1v69.5h54.6V35.8h4.2v58.9c0,2.5-0.8,3.7-2.4,3.7 h-1.8V70.3H43.1v83.5h54.6v-21.1h4.2v26.7c0,1.2-0.2,2.3-0.5,3.2c-0.4,0.9-1,1.3-1.8,1.3h-1.8v-10.8H43.1V199h74.2 c12.7,0,22.4-3.6,29.3-10.7c6.9-7.1,10.3-16.5,10.3-28.1v-48.6h-12.2C147.6,109,150,106.1,152,102.6z" />
    <path id="letter-A" fill="#F6B9AA" d="M43.1,1v91.1l54.7,1v-57h4.2V124h-4.2V87.1H43.1V199h54.7v-39.6h4.2V199h54.9V1H43.1z"/>
    <path id="letter-N" fill="#F6B9AA" d="M43.1 199L43.1 1L156.9 1L156.9 199L102 199L102 36.1L97.8 36.1L97.8 199z" />
    <path id="letter-D" fill="#F6B9AA" d="M43,1v114.5h54.6V36.1h4.2v123.8c0,0.9-0.2,1.7-0.5,2.5c-0.4,0.8-0.8,1.2-1.3,1.2h-2.4v-52.1 H43V199h75.2c7.6,0,14.3-1.7,20.1-5c5.8-3.3,10.4-7.9,13.7-13.7c3.3-5.8,5-12.2,5-19.3V1H43z" />
  </svg>
</div>

<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.2.1/build/decomp.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pathseg@1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-js@0.20.0/build/matter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-attractors@0.1.6/build/matter-attractors.min.js"></script>

 3  119  3
1 Jan 1970

Solution

 4

Looks like this issue occurs if the label has more than 1 part. The I label has only 1 part so it works properly but if it's more than 1, then when we update the fillStyle property, it only updates the first part's fillStyle attribute. You can review the parts array in the target object. It updates only the parts array's first element's fillStyle attribute. Therefore, to fix this problem, need to map parts and set the fillStyle property the same as the color we want to fill:

// *** Added this mapper to map parts and set the color ***
const partsMapper = (parts, color) => { 
  parts.forEach(part => {
      part.render.fillStyle = color;
      part.render.strokeStyle = color;
  });
}

Matter.Events.on(mouseConstraint, "mousedown", event => {
  const target = event.source.body;
  if (target) {
    // *** Filtered colors to not set the same color again ***
    const chosenColor = Matter.Common.choose(['#f19648', '#f5d259', '#f55a3c'].filter(color => color !== target.render.fillStyle));
    target.render.fillStyle = chosenColor;
    
    // *** Map parts ***
    partsMapper(target.parts, chosenColor);
    
    // applying force works for some reason
    const dir = Matter.Common.random(-0.3, 0.3);
    Matter.Body.applyForce(target, { x: target.position.x, y: target.position.y }, { x: dir, y: -0.2 });
  }
});

DEMO:

Matter.Common._seed = Math.random().toString().slice(2,10); // random 8 digit number
Matter.use(MatterAttractors);

const engine = Matter.Engine.create();
const currentWidth = document.querySelector('#canvas').offsetWidth * 2;

// letter initial coordinates
const offsets = {
  'B':  [currentWidth/100*20, currentWidth/100*5],
  'I':  [currentWidth/100*50, currentWidth/100*5],
  'G':  [currentWidth/100*70, currentWidth/100*5],
  'B2': [currentWidth/100*20, currentWidth/100*50],
  'A':  [currentWidth/100*40, currentWidth/100*50],
  'N':  [currentWidth/100*60, currentWidth/100*50],
  'D':  [currentWidth/100*80, currentWidth/100*50]
};

var render = Matter.Render.create({
  element: document.querySelector('#canvas'),
  engine: engine,
  options: {
    width: currentWidth, 
    height: currentWidth,
    background: '#222',
    wireframes: false,
    showAngleIndicator: false
  }
});

var select = function (root, selector) {
  return Array.prototype.slice.call(root.querySelectorAll(selector));
};

var loadSvg = function (url) {
  return fetch(url)
    .then(function(response) { return response.text(); })
    .then(function(raw) { return (new window.DOMParser()).parseFromString(raw, 'image/svg+xml'); });
};

// add SVG letters
document.querySelectorAll('#letters path').forEach(function (ele, i) {
  const color = ele.getAttribute('fill');
  const id = ele.getAttribute('id').replace('letter-', '');
  const vertexSets = Matter.Svg.pathToVertices(ele, 30);

  const offsetX = offsets[id][0];
  const offsetY = offsets[id][1];

  var letter = Matter.Bodies.fromVertices(offsetX, offsetY, vertexSets, {
    render: {
      fillStyle: color,
      strokeStyle: color,
      lineWidth: 1,
      opacity: 1
    },
    restitution: 0.7,
    label: 'Letter-' + id
  });

  Matter.Body.rotate(letter, Matter.Common.random(-0.3, 0.3));
  Matter.Body.scale(letter, 1.0, 1.0);
  Matter.Body.setMass(letter, 5.01);

  Matter.Composite.add(engine.world, letter, true);
});

// walls
const wallstyle = { fillStyle: '#0f0' };
var ground = Matter.Bodies.rectangle(currentWidth/2, currentWidth, currentWidth, 10, { isStatic: true, render: wallstyle });
var wallLeft = Matter.Bodies.rectangle(0, currentWidth/2, 10, currentWidth, { isStatic: true, render: wallstyle });
var wallRight = Matter.Bodies.rectangle(currentWidth, currentWidth/2, 10, currentWidth, { isStatic: true, render: wallstyle });

Matter.Composite.add(engine.world, [wallLeft, wallRight, ground]); // [boxA, boxB, ground]

Matter.Render.run(render);

var runner = Matter.Runner.create();
Matter.Events.on(runner, "tick", event => {}); // nothing as of right now
Matter.Runner.run(runner, engine);

// mouse events
const mouseConstraint = Matter.MouseConstraint.create(
  engine, 
  Matter.Mouse.create(render.canvas), 
  {}
);

// *** Added this mapper to map parts and set the color ***
const partsMapper = (parts, color) => { 
  parts.forEach(part => {
      part.render.fillStyle = color;
      part.render.strokeStyle = color;
  });
}

Matter.Events.on(mouseConstraint, "mousedown", event => {
  const target = event.source.body;
  if (target) {
    // *** Filtered colors to not set the same color again ***
    const chosenColor = Matter.Common.choose(['#f19648', '#f5d259', '#f55a3c'].filter(color => color !== target.render.fillStyle));
    target.render.fillStyle = chosenColor;
    
    // *** Map parts ***
    partsMapper(target.parts, chosenColor);

    // applying force works for some reason
    const dir = Matter.Common.random(-0.3, 0.3);
    Matter.Body.applyForce(target, { x: target.position.x, y: target.position.y }, { x: dir, y: -0.2 });
  }
});
html {
  background: black;
  color: #fff;
}

#canvas {
  margin: 2rem auto;
  width: 40vw;
  max-width: 40rem;
  aspect-ratio: 1 / 1;
  outline: 1px solid #0ff;
}

#canvas canvas {
  display: block;
  width: 100%;
  height: auto;
}

#letters { display: none; }

.as-console-wrapper {
  width: 0;
}
<div id="canvas"></div>

<!-- SVG letter shapes, to be loaded in JS -->
<div id="letters">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" viewBox="0 0 200 200" xml:space="preserve">
    <path id="letter-B" fill="#00A15D" d="M187.5,101c3.3-5.6,4.9-12.1,4.9-19.3v-81h-62.7h-30H7.6v198h92.1v-36.4h-1.8v-30.6h1.8V97.3 h-1.8V36.9h1.9h2.4v56.8c0,2.5-0.8,3.7-2.4,3.7h0v34.3h2.4v26.1c0,1.2-0.2,2.3-0.5,3.2c-0.4,0.9-1,1.3-1.8,1.3h0v36.4h53.1  c12.7,0,22.4-3.6,29.3-10.7c6.9-7.1,10.3-16.5,10.3-28.1v-48.8h-13.5C182.4,108.3,185.3,104.9,187.5,101z" />
    <path id="letter-I" fill="#00A15D" d="M54.9 1h90.1v198H54.9Z" />
    <path id="letter-G" fill="#00A15D" d="M7.5,199V39.8c0-11.6,3.4-21,10.3-28.1C24.7,4.6,34.4,1,46.9,1h145.6v38.5H102   c-1.4,0-2.5,0.5-3.2,1.5c-0.7,1-1.1,2.1-1.1,3.3v117.2h4.2V81.3V43.8h90.4V199H7.5z" />
    <path id="letter-B2" fill="#F6B9AA" d="M152,102.6c3.3-5.6,4.9-12.1,4.9-19.3V1H43.1v69.5h54.6V35.8h4.2v58.9c0,2.5-0.8,3.7-2.4,3.7 h-1.8V70.3H43.1v83.5h54.6v-21.1h4.2v26.7c0,1.2-0.2,2.3-0.5,3.2c-0.4,0.9-1,1.3-1.8,1.3h-1.8v-10.8H43.1V199h74.2 c12.7,0,22.4-3.6,29.3-10.7c6.9-7.1,10.3-16.5,10.3-28.1v-48.6h-12.2C147.6,109,150,106.1,152,102.6z" />
    <path id="letter-A" fill="#F6B9AA" d="M43.1,1v91.1l54.7,1v-57h4.2V124h-4.2V87.1H43.1V199h54.7v-39.6h4.2V199h54.9V1H43.1z"/>
    <path id="letter-N" fill="#F6B9AA" d="M43.1 199L43.1 1L156.9 1L156.9 199L102 199L102 36.1L97.8 36.1L97.8 199z" />
    <path id="letter-D" fill="#F6B9AA" d="M43,1v114.5h54.6V36.1h4.2v123.8c0,0.9-0.2,1.7-0.5,2.5c-0.4,0.8-0.8,1.2-1.3,1.2h-2.4v-52.1 H43V199h75.2c7.6,0,14.3-1.7,20.1-5c5.8-3.3,10.4-7.9,13.7-13.7c3.3-5.8,5-12.2,5-19.3V1H43z" />
  </svg>
</div>

<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.2.1/build/decomp.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pathseg@1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-js@0.20.0/build/matter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-attractors@0.1.6/build/matter-attractors.min.js"></script>

2024-07-06
Onur Doğan