跳到主要内容

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 线程。

// 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,
      },
    });
  });
});