我想在带有离屏画布的 Web Worker 上使用 Matter.js,但问题是它似乎会抛出有关
requestAnimationFrame
的错误。它尝试使用 window.requestAnimationFrame
来获取它,但在网络工作窗口中未定义。另外,如果我尝试设置一个精灵,它会失败,因为 Matter.js 使用 new Image()
但在网络工作人员中没有定义。它还尝试设置画布的样式,但这不起作用,因为它是屏幕外的画布。
有人让它工作吗?
在库的作者提供真正的
Worker
版本之前,您必须对上下文进行猴子修补,以便可以访问他们使用的所有方法。
这里有一些我能想到的常见重写,它们对于其他库也可能有用,但是对于 UI 事件之类的事情,您实际上还需要从所有者的上下文(主线程)中处理它们,而我写的时候前段时间的一个
EventPort
会有帮助,我会将其作为读者的练习,因为每个库的需求可能会有很大差异。
无论如何,在这里你会找到一个覆盖:
Image
构造函数,其底层使用 ImageBitmap
,document
的 createElement("canvas")
,返回 OffscreenCanvas
document
的 createElement("image")
,它返回我们的 Image
OffscreenCanvas
上的一些方法和属性,以便它更好地映射到 <canvas>
HTMLImageElement
的消费者(在 2D 环境中),以便我们的 Image
暴露其 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>