在HTML5画布中处理大型图片

4

我有一个15000x15000像素的矢量图像,我想将其用作画布项目的背景。我需要能够快速而经常地(在requestAnimationFrame内)裁剪图像的一部分并将其绘制为背景。

为了绘制所需的图像扇区,我正在使用...

const xOffset = (pos.x - (canvas.width / 2)) + config.bgOffset + config.arenaRadius;
const yOffset = (pos.y - (canvas.height / 2)) + config.bgOffset + config.arenaRadius;
c.drawImage(image, xOffset, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);

计算所需的背景面积并绘制图像。这一切都很好,但重绘速度很慢,第一次绘制甚至更慢。
加载如此庞大的图片似乎荒谬,并且性能缓慢。我该如何减小文件大小或以其他方式提高性能?
编辑:在其完整尺寸下绘制这样大的图像没有合理的解决方案。因为背景是一个重复的模式,我的当前解决方案是取一个“单元格”并多次绘制它。

你的画布有多大?还有,矢量图像是指SVG吗? - Máté Safranka
@MátéSafranka 画布会根据屏幕大小进行缩放。我设计的本机尺寸为1920x1080(实际窗口为1920x974)。我在Affinity Designer中制作了背景,以便导出为SVG。 - jolaleye
我有点超出了我的自信范围,但这是我的两分钱。绘制位图的一部分不应该是特别消耗CPU的操作,因为它基本上只是从一个缓冲区复制一堆字节到另一个缓冲区。我能想象到唯一的减速源是要复制的字节数量,即目标画布的大小。然而 - 这就是我的信心动摇的地方 - 从SVG中绘制可能是完全不同的事情,因为在这种情况下,源不是位图,意味着有很多额外的计算。 - Máté Safranka
所以,除了使用较小的画布外,我的唯一猜测是使用位图而不是SVG作为源。然而,在15k x 15k像素的情况下,这可能会消耗过多的内存。 - Máté Safranka
@MátéSafranka 我会在项目进展过程中再试验一下,看看它的速度有多慢。图像文件大小为9.23MB...我想这引起了一些注意。至于你最后提到的那一点,该图像是使用矢量程序设计的,但可以导出为PNG格式(这就是我使用的格式)。事实上,当我第一次尝试绘制SVG时,它甚至都没有起作用。 - jolaleye
2个回答

2
我强烈推荐使用EaselJS将多个/大型对象呈现到HTML画布上。这个库支持向量缓存、WebGL等功能。
以下是将多个向量对象缓存为位图的示例(单击复选框进行比较):https://www.createjs.com/demos/easeljs/cache 您应该能够以同样的方式处理背景向量。只需缓存对象,让软件处理所有繁重的工作即可。

那个演示很令人印象深刻。我会调查一下,谢谢! - jolaleye
1
这些演示都没有展示它如何改善绘制大图像的效果。OP的情况是一个已经光栅化的15000 * 15000像素图像。EaselJS缓存策略是通过生成光栅化版本来避免多次调用相同的路径方法。但这对OP没有帮助。 - Kaiido
@kaiido,这是一个很好的例子,展示了在将矢量转换为位图后使用“cacheCanvas”的方法:https://stackoverflow.com/a/35991855/2510368。理论上,您可以将大型矢量缓存到大型位图中,然后从该画布对象缓存小位图。 - doppler
@Kaiido,我明白了。假设这个15000x15000的图像(矢量或位图)是静态的,你可以将图像缓存到类似800x600的尺寸,并以更小的尺寸重新绘制它。 - doppler
@doppler 但是图片需要以其真实比例显示。无论如何,我会停止争论并坚持我的第一个评论,即此答案没有针对所提出的问题(“在HTML5画布中处理大型图像”)。 - Kaiido
1
EaselJS并没有解决原始图像问题,但我最终还是因为它的其他好处而使用了它,所以感谢您的推荐。 - jolaleye

2

15000像素 x 15000像素确实很大。

GPU必须将其存储为原始RGB数据在其内存中(我不记得数学准确的数字,但我认为它是类似于宽度x高度x3字节,即675MB在您的情况下,这超出了大多数常见GPU的处理能力)。 加上您可能拥有的所有其他图形,您的GPU将被迫在每帧中丢弃您的大图像并重新获取它。

为了避免这种情况,您最好将大图像分成多个较小的图像,并每帧调用多次drawImage。这样,在最坏的情况下,GPU只需要获取所需的部分,在最好的情况下,它已经在其内存中。

这是一个粗略的概念证明,它将把一个5000*5000像素的svg图像分成250*250像素的瓷砖。当然,您需要根据自己的需求进行调整,但这可能会给您一个想法。

console.log('generating image...');
var bigImg = new Image();
bigImg.src = URL.createObjectURL(generateBigImage(5000, 5000));
bigImg.onload = init;

function splitBigImage(img, maxSize) {
  if (!maxSize || typeof maxSize !== 'number') maxSize = 500;

  var iw = img.naturalWidth,
    ih = img.naturalHeight,
    tw = Math.min(maxSize, iw),
    th = Math.min(maxSize, ih),
    tileCols = Math.ceil(iw / tw), // how many columns we'll have
    tileRows = Math.ceil(ih / th), // how many rows we'll have
    tiles = [],
    r, c, canvas;

  // draw every part of our image once on different canvases
  for (r = 0; r < tileRows; r++) {
    for (c = 0; c < tileCols; c++) {
      canvas = document.createElement('canvas');
      // add a 1px margin all around for antialiasing when drawing at non integer
      canvas.width = tw + 2;
      canvas.height = th + 2;
      canvas.getContext('2d')
        .drawImage(img,
          (c * tw | 0) - 1, // compensate the 1px margin
          (r * tw | 0) - 1,
          iw, ih, 0, 0, iw, ih);
      tiles.push(canvas);
    }
  }

  return {
    width: iw,
    height: ih,
    // the drawing function, takes the output context and x,y positions
    draw: function drawBackground(ctx, x, y) {
      var cw = ctx.canvas.width,
        ch = ctx.canvas.height;
      // get our visible rectangle as rows and columns indexes
      var firstRowIndex = Math.max(Math.floor((y - th) / th), 0),
        lastRowIndex = Math.min(Math.ceil((ch + y) / th), tileRows),
        firstColIndex = Math.max(Math.floor((x - tw) / tw), 0),
        lastColIndex = Math.min(Math.ceil((cw + x) / tw), tileCols);

      var col, row;
      // loop through visible tiles and draw them
      for (row = firstRowIndex; row < lastRowIndex; row++) {
        for (col = firstColIndex; col < lastColIndex; col++) {
          ctx.drawImage(
            tiles[row * tileCols + col], // which part
            col * tw - x - 1, // x position
            row * th - y - 1 // y position
          );
        }
      }
    }
  };
}

function init() {
  console.log('image loaded');

  var bg = splitBigImage(bigImg, 250); // image_source, maxSize
  var ctx = document.getElementById('canvas').getContext('2d');
  var dx = 1,
    dy = 1,
    x = 150,
    y = 150;
  anim();
  setInterval(changeDirection, 2000);

  function anim() {
    // just to make the background position move...
    x += dx;
    y += dy;
    if (x < 0) {
      dx *= -1;
      x = 1;
    }
    if (x > bg.width - ctx.canvas.width) {
      dx *= -1;
      x = bg.width - ctx.canvas.width - 1;
    }
    if (y < 0) {
      dy *= -1;
      y = 1;
    }
    if (y > bg.height - ctx.canvas.height) {
      dy *= -1;
      y = bg.height - ctx.canvas.height - 1;
    }
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    if(chck.checked) {
      // that's how you call it
      bg.draw(ctx, x, y);
    }
    else {
      ctx.drawImage(bigImg, -x, -y);
    }
    requestAnimationFrame(anim);
  }

  function changeDirection() {
    dx = (Math.random()) * 5 * Math.sign(dx);
    dy = (Math.random()) * 5 * Math.sign(dy);
  }

  setTimeout(function() { console.clear(); }, 1000);
}
// produces a width * height pseudo-random svg image
function generateBigImage(width, height) {
  var str = '<svg width="' + width + '" height="' + height + '" xmlns="http://www.w3.org/2000/svg">',
    x, y;
  for (y = 0; y < height / 20; y++)
    for (x = 0; x < width / 20; x++)
      str += '<circle ' +
      'cx="' + ((x * 20) + 10) + '" ' +
      'cy="' + ((y * 20) + 10) + '" ' +
      'r="15" ' +
      'fill="hsl(' + (width * height / ((y * x) + width)) + 'deg, ' + (((width + height) / (x + y)) + 35) + '%, 50%)" ' +
      '/>';
  str += '</svg>';
  return new Blob([str], {
    type: 'image/svg+xml'
  });
}
<label>draw split <input type="checkbox" id="chck" checked></label>
<canvas id="canvas" width="800" height="800"></canvas>

实际上,在您的特定情况下,我甚至会将分割图像存储在服务器上(无论是在svg中,因为它应该需要更少的带宽),并从不同来源生成瓷砖。但我会让读者自己练习。


我最终使用了一种与此方法有些相似的方法...由于我的背景是一个重复的图案,我取了一个图案“单元”并多次绘制它。感谢您的解决方案! - jolaleye

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接