Web Worker 中的离屏画布

如何在 Web Worker 中运行 Konva?

警告!这个演示是非常实验性的!可能在许多浏览器中无法工作。 请查看 离屏画布能力表

通过一些额外的工作,我们可以使用 离屏画布Web Worker 中渲染 Konva 舞台,以提高性能或实现一些疯狂的想法。

你可以使用 Web Worker 来进行一些与 Konva 相关的可视化。

但是 Konva 的一个主要特点是其交互性(对画布形状完全支持事件)。而在 Web Worker 中没有 DOM 事件。因此,我们必须写一些“代理”代码来传递所有的 DOM 事件到 Konva 引擎。这样我们也可以在 Web Worker 中拥有交互式对象。

这个演示基于 跳跃的兔子 性能压力测试进行了调整。

你可能需要编写更多的代码来覆盖更多的功能和不同的边缘情况(例如 HDPI 屏幕支持)。

说明:舞台上有两个交互对象。“添加按钮”和一个可拖动的红色圆圈。尝试添加更多的兔子或拖动圆圈。

你在屏幕上看到的所有内容都是 在另一个 JavaScript 线程中渲染的!因此,它不应该阻塞当前页面的主 JS 线程。

Web_Worker.htmlview raw
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Konva Offscreen Canvas Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
</head>

<body>
<canvas id="canvas"></canvas>
<script>
var htmlCanvas = document.getElementById('canvas');
htmlCanvas.width = window.innerWidth;
htmlCanvas.height = window.innerHeight;

var hasOffscreenSupport = !!htmlCanvas.transferControlToOffscreen;
if (hasOffscreenSupport) {
var offscreen = htmlCanvas.transferControlToOffscreen();

var w = new Worker('./Web_Worker.js');
// pass canvas into webworker, so we can do all rendering inside it
w.postMessage({ canvas: offscreen }, [offscreen]);

// "proxy" all DOM events from canvas into Konva engine
var EVENTS = [
'mouseenter',
'mousedown',
'mousemove',
'mouseup',
'mouseout',
'wheel',
'contextmenu',
'pointerdown',
'pointermove',
'pointerup',
'pointercancel',
'lostpointercapture',
];
EVENTS.forEach((eventName) => {
htmlCanvas.addEventListener(eventName, (e) => {
w.postMessage({
eventName,
event: {
clientX: e.clientX,
clientY: e.clientY,
type: e.type,
},
});
});
});
} else {
htmlCanvas
.getContext('2d')
.fillText(
'🛑 Sorry, your browser does not support Offscreen rendering...',
20,
20
);
}
</script>
</body>
</html>

而工作线程的代码是:

Worker Codeview raw
// load konva framework
importScripts('https://unpkg.com/konva@9/konva.min.js');

// monkeypatch Konva for offscreen canvas usage
Konva.Util.createCanvasElement = () => {
const canvas = new OffscreenCanvas(1, 1);
canvas.style = {};
return canvas;
};

// now we can create our canvas content
var stage = new Konva.Stage({
width: 200,
height: 200,
});

var layer = new Konva.Layer();
stage.add(layer);

var topGroup = new Konva.Group();
layer.add(topGroup);

// counter will show number of bunnies
var counter = new Konva.Text({
x: 5,
y: 35,
});
topGroup.add(counter);

// "add more bunnies" button
var button = new Konva.Label({
x: 5,
y: 5,
opacity: 0.75,
});
topGroup.add(button);

button.add(
new Konva.Tag({
fill: 'black',
})
);

button.add(
new Konva.Text({
text: 'Push me to add bunnies',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);

// draggable circle to show interactivity
var circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 20,
fill: 'red',
draggable: true,
});
topGroup.add(circle);

self.onmessage = function (evt) {
// when canvas is passes we can start our worker
// we can try to use that canvas for the layer with some manual replacement (and probably better performance)
// but for simplicity we will just copy layer content into passed canvas
if (evt.data.canvas) {
var canvas = evt.data.canvas;
// adapt stage size
// we may need to add extra event to resize stage on a fly
stage.setSize({
width: canvas.width,
height: canvas.height,
});

const ctx = canvas.getContext('2d');

// Konva.Layer has support for "draw" event
// so every time the layer is re-rendered we need to update the canvas
layer.on('draw', () => {
// clear content
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw layer content
ctx.drawImage(layer.getCanvas()._canvas, 0, 0);
});
}
// emulate some drag&drop events
if (evt.data.eventName === 'mouseup') {
Konva.DD._endDragBefore(evt.data.event);
}
if (evt.data.eventName === 'touchend') {
Konva.DD._endDragBefore(evt.data.event);
}
if (evt.data.eventName === 'mousemove') {
Konva.DD._drag(evt.data.event);
}
if (evt.data.eventName === 'touchmove') {
Konva.DD._drag(evt.data.event);
}
if (evt.data.eventName === 'mouseup') {
Konva.DD._endDragAfter(evt.data.event);
}
if (evt.data.eventName === 'touchend') {
Konva.DD._endDragAfter(evt.data.event);
}

// pass incoming events into the stage
if (evt.data.eventName) {
const event = evt.data.eventName.replace('mouse', 'pointer');
stage['_' + event](evt.data.event);
}
};

function requestAnimationFrame(cb) {
setTimeout(cb, 16);
}

// that function is large and adapted from bunnies demo
// the only interesting part here is how to load images to use for Konva.Image
async function runBunnies() {
const imgBlob = await fetch('https://konvajs.org/assets/bunny.png').then(
(r) => r.blob()
);
// use "createImageBitmap" instead of "new window.Image()"
const img = await createImageBitmap(imgBlob);

var bunnys = [];
var gravity = 0.75;

var startBunnyCount = 100;
var isAdding = false;
var count = 0;
var amount = 10;

button.on('mousedown', function () {
isAdding = true;
});

button.on('mouseup', function () {
isAdding = false;
});

for (var i = 0; i < startBunnyCount; i++) {
var bunny = new Konva.Image({
image: img,
transformsEnabled: 'position',
x: 10,
y: 10,
listening: false,
});

bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;

bunnys.push(bunny);
counter.text('Bunnies number: ' + bunnys.length);
layer.add(bunny);
}
topGroup.moveToTop();

function update() {
var maxX = stage.width() - 10;
var minX = 0;
var maxY = stage.height() - 10;
var minY = 0;
if (isAdding) {
for (var i = 0; i < amount; i++) {
var bunny = new Konva.Image({
image: img,
transformsEnabled: 'position',
x: 0,
y: 0,
listening: false,
});
bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;
bunnys.push(bunny);
layer.add(bunny);
counter.text('Bunnies number: ' + bunnys.length);
count++;
}
topGroup.moveToTop();
}

for (var i = 0; i < bunnys.length; i++) {
var bunny = bunnys[i];
bunny.setX(bunny.getX() + bunny.speedX);
bunny.setY(bunny.getY() + bunny.speedY);
bunny.speedY += gravity;
if (bunny.getX() > maxX - img.width) {
bunny.speedX *= -1;
bunny.setX(maxX - img.width);
} else if (bunny.getX() < minX) {
bunny.speedX *= -1;
bunny.setX(minX);
}

if (bunny.getY() > maxY - img.height) {
bunny.speedY *= -0.85;
bunny.setY(maxY - img.height);
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (bunny.getY() < minY) {
bunny.speedY = 0;
bunny.setY(minY);
}
}
layer.drawScene();
requestAnimationFrame(update);
}
update();
}

runBunnies();