Web Worker 中的离屏 Canvas
如何在 Web Worker 中运行 Konva?
警告!此示例仍处于高度实验阶段!它可能无法在许多浏览器中运行。 请查看离屏 Canvas 支持情况表。
通过一些额外工作,可以在 Web Worker 中使用离屏 Canvas 渲染 Konva 舞台,以提高性能或实现特殊用途。
可以使用 Web Worker 和 Konva 创建可视化内容。
但是,Konva 的主要功能之一是交互性(完整支持 Canvas 图形事件),而 Web Worker 中没有 DOM 事件。因此,必须编写某种“代理”,将所有 DOM 事件传入 Konva 引擎。这样,Web Worker 中也可以包含交互式对象。
此示例改编自跳跃的兔子性能压力测试。
你可能需要编写更多代码,以支持更多功能和不同的边缘情况(例如 HDPI 屏幕支持)。
操作说明:舞台上有两个交互式对象:“Add buttons”和一个可拖动的红色圆形。尝试添加更多兔子或拖动圆形。
屏幕上看到的所有内容都在另一个 JavaScript 线程中渲染!因此,它不会阻塞当前页面的主 JS 线程。
- Vanilla
// main.js const workerCode = ` // 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 if (evt.data.canvas) { var canvas = evt.data.canvas; stage.setSize({ width: canvas.width, height: canvas.height, }); const ctx = canvas.getContext('2d'); layer.on('draw', () => { ctx.clearRect(0, 0, canvas.width, canvas.height); 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); } async function runBunnies() { const imgBlob = await fetch('https://konvajs.org/assets/bunny.png').then( (r) => r.blob() ); 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(); `; // Create a blob from the worker code const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); const canvas = document.createElement('canvas'); document.body.appendChild(canvas); canvas.style.border = '1px solid black'; canvas.width = window.innerWidth; canvas.height = window.innerHeight; // transfer control of the canvas to the worker const offscreen = canvas.transferControlToOffscreen(); worker.postMessage({ canvas: offscreen }, [offscreen]); // proxy all events const events = [ 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', // 'click', // 'dblclick', 'touchstart', 'touchend', 'touchmove', ]; events.forEach((eventName) => { canvas.addEventListener(eventName, (event) => { worker.postMessage({ eventName, event: { clientX: event.clientX, clientY: event.clientY, type: event.type, button: event.button, }, }); }); });