0

I want to use matter.js on a web worker with offscreen canvas, but the problem is it seems to throw errors about requestAnimationFrame. It tries to get it by using window.requestAnimationFrame but in a web worker window is not defined. Also if I try to set a sprite, it fails because matter.js uses new Image() but that isn't defined in a web worker. Also it tries to style the canvas, but that doesn't work since its an offscreen canvas.

Has anyone gotten this to work?

2
  • 1
    Please share a complete, runnable minimal reproducible example of your current code. As with your last question, it feels like you're fishing for someone to hand you a full working example, but that's unlikely because there's not really any other active MJS answerers besides myself so you're probably not going to get an answer anytime soon. Even if someone did provide a full working example, there's a good chance it won't align well with whatever your actual use case is, so providing context helps you too. Thanks. Commented May 2, 2024 at 22:50
  • If you're cross-posting to GH, you might as well link it so that future visitors with the same problem as you can find answers that may have been posted only there: github.com/liabru/matter-js/issues/1288. Please try to be a steward of the site and respond to comments asking for clarification. Otherwise there's not much to be done here except close the questions. Thanks. Commented May 2, 2024 at 22:58

1 Answer 1

1

Until the library's authors provide a true Worker version, you'll have to monkey-patch the context so that all the methods they use can be accessed.

Here are a few common overrides I could think of, which could also be useful for other libraries, but for things like UI events, you'd need to actually also handle them from the owner's context (main thread), and while I wrote an EventPort some times ago that would help, I'll let that as an exercise for the readers, since the needs could vary a lot for each lib.

Anyway, here you'll find an override for:

  • the Image constructor, which under the hood uses ImageBitmap,
  • document's createElement("canvas"), which returns an OffscreenCanvas
  • document's createElement("image"), which returns our Image
  • some methods and properties on the OffscreenCanvas so that it maps better to a <canvas>
  • some consumers of HTMLImageElement (in the 2D context) so that our Image exposes its ImageBitmap.

const scriptContent = document.querySelector("[type=worker-script]").textContent;
const scriptURL = URL.createObjectURL(new Blob([scriptContent]));
const worker = new Worker(scriptURL);
worker.onerror = console.log
const placeholder = document.querySelector("canvas");
const offCanvas = placeholder.transferControlToOffscreen();
worker.postMessage(offCanvas, [offCanvas]);
<canvas></canvas>
<script type="worker-script">
self.window = self;
self.document = { // Not really needed for this example
  createElement(val) {
    if (val === "img") {
      return new Image();
    }
    if (val === "canvas") {
      return new OffscreenCanvas(300, 150);
    }
  }
};
// They try to set a background
// You could catch it and let the placeholder know, if wanted
Object.defineProperty(OffscreenCanvas.prototype, "style", { value: {} });
// Make it act more like an element
OffscreenCanvas.prototype.getAttribute = function (attr) {
  return this._attributes?.[attr];
};
OffscreenCanvas.prototype.setAttribute = function (attr, value) {
  if (attr === "width" || attr === "height") {
    this[attr] = parseInt(value);
  }
  return (this._attributes ??= {})[attr] = value.toString();
};

// Our new Image() class
const bitmapSymbol = Symbol("bitmap");
const imageMap = new Map();
class Image extends EventTarget {
  [bitmapSymbol] = null;
  get width() {
    return this[bitmapSymbol]?.width || 0;
  }
  get height() {
    return this[bitmapSymbol]?.height || 0;
  }
  get naturalWidth() {
    return this.width;
  }
  get naturalHeight() {
    return this.height;
  }
  #src = null;
  get src() { return this.#src; }
  set src(value) {
    this.#src = value.toString();
    (async () => {
      // The request has already been performed before
      // We try to make it behave synchrnously like the actual
      // Image does when the resource has already been loaded
      if (imageMap.has(this.#src)) {
        // Still ongoing, await
        if (imageMap.get(this.#src) instanceof Promise) {
          await imageMap.get(this.#src);
        }
        // Set it sync if possible
        this[bitmapSymbol] = imageMap.get(this.#src);
        this.dispatchEvent(new Event("load"));
      }
      else {
        const { promise, resolve } = Promise.withResolvers();
        imageMap.set(this.#src, promise);
        const resp = await fetch(this.#src);
        const blob = resp.ok && await resp.blob();
        const bmp = this[bitmapSymbol] = await createImageBitmap(blob);
        resolve(bmp);
        imageMap.set(this.#src, bmp);
        this.dispatchEvent(new Event("load"));
      }
    })().catch((err) => {
      this.dispatchEvent(new Event("error"));
    });
  }
  async decode() {
    if (!imageMap.has(this.src)) {
      throw new DOMException("Invalid image request.");
    }
    await imageMap.get(this.src);
  }
  #onload = null;
  set onload(handler) {
    this.removeEventListener("load", this.#onload);
    this.#onload = handler
    this.addEventListener("load", this.#onload);    
  }
  #onerror = null;
  set onerror(handler) {
    this.removeEventListener("error", this.#onerror);
    this.#onerror = handler;
    this.addEventListener("error", this.#onerror);
  }
}
// Image() consumers need to be overridden
const overrideProto = (proto, funcName) => {
  const orig = proto[funcName];
  proto[funcName] = function(source, ...args) {
    const fixedSource = source[bitmapSymbol] || source;
    return orig.call(this, fixedSource, ...args);
  };
};
overrideProto(OffscreenCanvasRenderingContext2D.prototype, "drawImage");
overrideProto(OffscreenCanvasRenderingContext2D.prototype, "createPattern");
overrideProto(globalThis, "createImageBitmap");
// WebGL has another signature, if needed, shouldn't be too hard to write it yourself.
// END OVERRIDES
/////////////////////////////

importScripts("https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js");

onmessage = async ({ data: canvas }) => {
  // matter.js doesn't wait for the texture have loaded...
  // So we must preload them, also tru for when in the main thread
  const preloadImage = (url) => {
    const img = new Image();
    img.src = url;
    return img.decode();
  };
  await preloadImage("https://cdn.jsdelivr.net/gh/liabru/matter-js/demo/img/box.png");
  await preloadImage("https://cdn.jsdelivr.net/gh/liabru/matter-js/demo/img/ball.png");
  // https://github.com/liabru/matter-js/blob/master/examples/sprites.js
  // Edited to point to set the Render's canvas to our OffscreenCanvas
  // (and the URLs absolute)
  var Engine = Matter.Engine,
        Render = Matter.Render,
        Runner = Matter.Runner,
        Composites = Matter.Composites,
        Common = Matter.Common,
        MouseConstraint = Matter.MouseConstraint,
        Mouse = Matter.Mouse,
        Composite = Matter.Composite,
        Bodies = Matter.Bodies;

    // create engine
    var engine = Engine.create(),
        world = engine.world;

    // create renderer
    var render = Render.create({
        canvas, // [EDITED]
        engine: engine,
        options: {
            width: 800,
            height: 600,
            showAngleIndicator: false,
            wireframes: false
        }
    });

    Render.run(render);

    // create runner
    var runner = Runner.create();
    Runner.run(runner, engine);

    // add bodies
    var offset = 10,
        options = { 
            isStatic: true
        };

    world.bodies = [];

    // these static walls will not be rendered in this sprites example, see options
    Composite.add(world, [
        Bodies.rectangle(400, -offset, 800.5 + 2 * offset, 50.5, options),
        Bodies.rectangle(400, 600 + offset, 800.5 + 2 * offset, 50.5, options),
        Bodies.rectangle(800 + offset, 300, 50.5, 600.5 + 2 * offset, options),
        Bodies.rectangle(-offset, 300, 50.5, 600.5 + 2 * offset, options)
    ]);

    var stack = Composites.stack(20, 20, 10, 4, 0, 0, function(x, y) {
        if (Common.random() > 0.35) {
            return Bodies.rectangle(x, y, 64, 64, {
                render: {
                    strokeStyle: '#ffffff',
                    sprite: {
                        texture: 'https://cdn.jsdelivr.net/gh/liabru/matter-js/demo/img/box.png'
                    }
                }
            });
        } else {
            return Bodies.circle(x, y, 46, {
                density: 0.0005,
                frictionAir: 0.06,
                restitution: 0.3,
                friction: 0.01,
                render: {
                    sprite: {
                        texture: 'https://cdn.jsdelivr.net/gh/liabru/matter-js/demo/img/ball.png'
                    }
                }
            });
        }
    });

    Composite.add(world, stack);

    // add mouse control
    var mouse = Mouse.create(render.canvas),
        mouseConstraint = MouseConstraint.create(engine, {
            mouse: mouse,
            constraint: {
                stiffness: 0.2,
                render: {
                    visible: false
                }
            }
        });

    Composite.add(world, mouseConstraint);

    // keep the mouse in sync with rendering
    render.mouse = mouse;

    // fit the render viewport to the scene
    Render.lookAt(render, {
        min: { x: 0, y: 0 },
        max: { x: 800, y: 600 }
    });
}
</script>

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.