Skip to main content

Web Worker 中的离屏画布

如何在 Web Worker 中运行 Konva?

警告!这个演示是非常实验性的!可能在许多浏览器中无法正常工作。 请查看 Offscreen canvas capability tabletv

通过一些额外的工作,我们可以在 Web Worker 中使用 Offscreen Canvas 渲染 Konva 阶段,以提高性能或实现一些疯狂的想法。

您可以使用 Web Worker 使用 Konva 制作一些可视化效果。

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

这个演示改编自 跳跃的兔子 性能压力测试。

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

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

// main.js
const workerCode = `
// 加载 konva 框架
importScripts('https://unpkg.com/konva@9/konva.min.js');

// monkeypatch Konva 以使用离屏画布
Konva.Util.createCanvasElement = () => {
  const canvas = new OffscreenCanvas(1, 1);
  canvas.style = {};
  return canvas;
};

// 现在我们可以创建我们的画布内容
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);

// 计数器将显示兔子的数量
var counter = new Konva.Text({
  x: 5,
  y: 35,
});
topGroup.add(counter);

// “添加更多兔子”按钮
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: '推我以添加兔子',
    fontFamily: 'Calibri',
    fontSize: 18,
    padding: 5,
    fill: 'white',
  })
);

// 可拖动的圆圈以显示交互性
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) {
  // 当画布被传递时,我们可以启动工作线程
  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);
    });
  }
  // 模拟一些拖动和放置事件
  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);
  }

  // 将传入的事件传递到舞台
  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('兔子数量: ' + 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('兔子数量: ' + 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();
`;

// 从工作代码创建一个 Blob
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;

// 将画布的控制权转移给工作线程
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// 代理所有事件
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,
      },
    });
  });
});