javascriptimagematter.js

Matter.js — How to get the dimension of an image to set Bodies sizes?


I am trying to programmatically set the width and heights of the chained bodies in matter.js. Unfortunately, I am only getting 0 as values and I am unsure why. My guess is that the images are not being loaded fast enough to provide those values. How can I load those dimensions before the images are loaded?

Pseudo-code

Code

var playA = Composites.stack(
  percentX(25) - assetSize / 2,
  percentY(25),
  1,
  6,
  5,
  5,
  function (x, y) {
    iA++;

    var imgWidth;
    var imgHeight;

    var img = new Image();
    img.src = String(design[iA]);

    var imgWidth = 0;
    var imgHeight = 0;

    img.onload = function a() {
      imgWidth = img.naturalWidth;
      imgHeight = img.naturalHeight;

      console.log(String(design[iA]), imgWidth, imgHeight);
    };
    console.log(String(design[iA]), imgHeight, imgWidth); // I can't access the values here.

    return Bodies.rectangle(x, y, imgWidth, imgHeight, {
      // collisionFilter: { group: group },
      friction: 1,
      render: {
        sprite: {
          texture: design[iA],
          xScale: (assetSize / 100) * 0.46,
          yScale: (assetSize / 100) * 0.46
        }
      }
    });
  }
);

Composites.chain(playA, 0.3, 0, -0.5, 0, {
  stiffness: 1,
  length: 10,
  render: { type: "line", visible: false }
});

Solution

  • If you know the dimensions and can populate an array beforehand, the solution is potentially straightforward since Matter.js loads images given a URL string, with the caveat that the engine doesn't wait for the loads before running.

    Here's a minimal example of iterating over width/height pairs in an array and passing these properties into the rectangle calls which I'll use as a stepping stone to the example that matches your use case.

    const engine = Matter.Engine.create();
    const render = Matter.Render.create({
      element: document.body,
      engine: engine,
      options: {
        width: 450,
        height: 250,
        wireframes: false, // required for images
      }
    });
    Matter.Render.run(render);
    
    const runner = Matter.Runner.create();
    Matter.Runner.run(runner, engine);
    
    const imgSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
    const stack = Matter.Composites.stack(
      // xx, yy, columns, rows, columnGap, rowGap, cb
      150, 50, 4, 1, 0, 0,
      (x, y, i) => {
        const [w, h] = imgSizes[i];
        return Matter.Bodies.rectangle(x, y, w, h, {
          render: {
            sprite: {
              texture: `https://picsum.photos/${w}/${h}`
            }
          }
        });
      }
    );
    Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
      stiffness: 0.75,
      length: 10,
      render: {type: "line", visible: true}
    });
    
    Matter.Composite.add(engine.world, [
      stack,
      Matter.Bodies.rectangle(225, 0, 450, 25, {
        isStatic: true
      }),
      Matter.Bodies.rectangle(450, 150, 25, 300, {
        isStatic: true
      }),
      Matter.Bodies.rectangle(0, 150, 25, 300, {
        isStatic: true
      }),
      Matter.Bodies.rectangle(225, 250, 450, 25, {
        isStatic: true
      })
    ]);
    
    const mouse = Matter.Mouse.create(render.canvas);
    const mouseConstraint = Matter.MouseConstraint.create(engine, {
      mouse: mouse,
      constraint: {
        stiffness: 0.2,
        render: {visible: true}
      }
    });
    Matter.Composite.add(engine.world, mouseConstraint);
    render.mouse = mouse;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>

    Now, if you need to load images using onload and use their dimensions, you'll need to use promises or put all code dependent on these images into the sequence of onload callback(s) as described in the canonical How do I return the response from an asynchronous call?.

    The failing pattern is:

    const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
    
    let data = null;
    getSomethingAsync(result => {
      data = result;
      console.log("this runs last");
    });
    console.log(data); // guaranteed to be null, not "something"
    // more logic that is supposed to depend on data

    The fix is:

    const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
    
    getSomethingAsync(data => {
      console.log(data);
      // logic that depends on the data from `getSomethingAsync`
    });
    
    console.log("this will run first");
    // logic that doesn't depend on data from `getSomethingAsync`

    Since you're juggling multiple onloads, you can promisify the onloads to make them easier to work with. I have a couple examples of doing this here and here agnostic of matter.js.

    Here's an example of using promises to load images applied to your general problem. Again, I'll use my own code so that it's runnable and reproducible, but the pattern should be easy to extrapolate to your project.

    The idea is to first load the images using a series of promises which are resolved when onload handlers fire, then use Promise.all to chain a then which runs the MJS initializer callback only when all images are loaded. The widths and heights are then accessible to your matter.js code within the callback.

    As a side benefit, this ensures images are loaded by the time MJS runs.

    const initializeMJS = images => {
      const engine = Matter.Engine.create();
      const render = Matter.Render.create({
        element: document.body,
        engine: engine,
        options: {
          width: 450,
          height: 250,
          wireframes: false, // required for images
        }
      });
      Matter.Render.run(render);
    
      const runner = Matter.Runner.create();
      Matter.Runner.run(runner, engine);
    
      const stack = Matter.Composites.stack(
        // xx, yy, columns, rows, columnGap, rowGap, cb
        150, 50, 4, 1, 0, 0,
        (x, y, i) => {
          const {width: w, height: h} = images[i];
          return Matter.Bodies.rectangle(x, y, w, h, {
            render: {
              sprite: {
                texture: images[i].src
              }
            }
          });
        }
      );
      Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
        stiffness: 0.75,
        length: 10,
        render: {type: "line", visible: true}
      });
    
      Matter.Composite.add(engine.world, [
        stack,
        Matter.Bodies.rectangle(225, 0, 450, 25, {
          isStatic: true
        }),
        Matter.Bodies.rectangle(450, 150, 25, 300, {
          isStatic: true
        }),
        Matter.Bodies.rectangle(0, 150, 25, 300, {
          isStatic: true
        }),
        Matter.Bodies.rectangle(225, 250, 450, 25, {
          isStatic: true
        })
      ]);
    
      const mouse = Matter.Mouse.create(render.canvas);
      const mouseConstraint = Matter.MouseConstraint.create(engine, {
        mouse: mouse,
        constraint: {
          stiffness: 0.2,
          render: {visible: true}
        }
      });
      Matter.Composite.add(engine.world, mouseConstraint);
      render.mouse = mouse;
    };
    
    const imageSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
    const imageURLs = imageSizes.map(([w, h]) =>
      `https://picsum.photos/${w}/${h}`
    );
    
    Promise.all(imageURLs.map(async e => {
      const img = new Image();
      img.src = e;
      await img.decode();
      return img;
    })).then(initializeMJS);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>