canvas 性能优化之 putImageData 的思考

37 篇文章 0 订阅
9 篇文章 2 订阅

作为一名前端数据可视化工程师来说,canvas 的使用可以说是最最基础的基本功了。

canvas 虽然只是一个 html 的标签,但是可以干的事情,却远远比普通的 html 标签大得多。

canvas 是一个画布,提供给我们绘图的功能,而且更神奇的是,它同时给我们提供了 2d 绘图和 3d 绘图的功能。

这个 2d 和 3d 当然是广义的概念,不针对具体背后使用的技术原理。

如果真的要细分的话,可以分成 CanvasRenderingContext2D 和 WebGLRenderingContext 等两种。

两者都可以用来绘制 2d 和 3d 场景,但是通常情况下,我们一般用前者也就是 CanvasRenderingContext2D 来绘制 2d 场景,而用后者也就是 WebGLRenderingContext 来绘制 3d 场景。

之所以这样做,是因为,前者用的是 cpu 来进行运算的,而后者用 gpu 来进行运算的。

2d 场景较为简单,构建起来难度低,因此我们多用 CanvasRenderingContext2D 相关的 api 来实现,而且由于历史原因,webgl 其实是一个相对比较新的技术,开始的时候,浏览器并不支持 WebGLRenderingContext,而且显卡技术也是近些年才飞速发展的。

虽然早些年,也有用 CanvasRenderingContext2D 相关 api 来实现 3d 场景的构建的,这一点,可以通过研究 three.js 源码的变化就可以看出来了。但是,随着 WebGLRenderingContext 的出现和普及,构建 3d 场景的任务就更多的落在了后者的头上了。

所以,虽然 canvas 也被规划为前端的范畴内,但是我更觉的 canvas 其实也能看作是一门编程语言,一门偏向应用型的编程语言。

canvas 虽然好用,但是如果用起来不得当的话,也是会造成性能问题的。

比如最近我用需要在 3d 场景里面用到 2d 的贴图,来创建一个公告牌(billboard)的效果。

就像下面这样:
image.png

如果要是以往的话,我肯定会选择使用图片来实现这个功能。

无他,因为最简单粗暴。

但是,这样做也是有缺点,最致命的缺点就是,公告牌上的字是固定的,并不能随意定制。

因为在项目实际运行的过程中,我们更希望看到的结果是,公告牌上的内容,是根据加载的数据的不同而动态的变更的。

所以,为了实现定制性,难道我们就需要通过我们传入的数据,动态的去给公告牌加载不同的贴图么?

而且还有一个问题是,既然我们都会用 webgl 了,为什么不给这个广告牌,直接用 canvas 2d 绘制出来呢?

如果这样做了的话,一下子就会把我们之前关心的几大问题:复用性、定制性、资源最小化等,统统都解决掉了。

所以,为了绘制出这么一个 billboard,我们需要绘制一个底图。

首先我们创建一个 canvas 元素,用来绘制我们的内容。

const canvas = document.createElement('canvas');
const w = 128;
const h = 64;
canvas.height = w;
canvas.height = h;

这地方,我们设置 canvas 画布的宽高为 128 * 64,是因为,我们 3d 场景中的贴图,宽高最好是 2 的幂,不然可能会出现贴图闪烁的问题。

接着,拿到我们的 2d 绘图画笔:

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

写一个创建背景的方法:

const createBackground = (ctx, x, y, width, height, lineWidth, radius) => {
  ctx.fillStyle = '#3e76aa';
  ctx.strokeStyle = '#1dceb7';
  ctx.lineWidth = lineWidth;

  let offset = lineWidth + 2;
  width -= offset;
  height -= offset;
  x += offset / 2;
  y += offset / 2;

  const maxRadius = Math.min(width, height) / 2;
  if (radius > maxRadius) {
    radius = maxRadius;
  }

  const interval = 10;
  var rx = x + width;
  var ry = y + height - interval;

  ctx.beginPath();
  ctx.moveTo(x, y + radius);
  ctx.lineTo(x, ry - radius);
  ctx.arcTo(x, ry, x + radius, ry, radius);

  ctx.lineTo((x + rx) / 2 - interval, ry);
  ctx.lineTo((x + rx) / 2, ry + interval - offset / 2);
  ctx.lineTo((x + rx) / 2 + interval, ry);
  ctx.lineTo(rx - radius, ry);

  ctx.arcTo(rx, ry, rx, ry - radius, radius);
  ctx.lineTo(rx, y + radius);
  ctx.arcTo(rx, y, rx - radius, y, radius);
  ctx.lineTo(x + radius, y);
  ctx.arcTo(x, y, x, y + radius, radius);
  ctx.closePath();
  ctx.fill();

  ctx.stroke();
}

然后我们测试一下,我们的代码写的对不对,打开浏览器,运行我们的代码:

image.png

可以看到,运行的很成功,我们成功的绘制出了我们的背景图片。至于内容的绘制呢,就不贴代码了,同样是很容易的操作。

我场景里面有很多这种公告牌,每个公告牌上的文字都是不一样的,所以对于每个公告牌,我都必须要重新绘制一个 canvas 去承载其内容。

现在我想讨论的关键问题是,在这个创建公告牌的过程中,有没有什么途径,去优化这段逻辑呢?

这个回答是肯定的。

我们可以发现,所有公告牌的背景都是一样的。所以我们就可以不需要在每个公告牌创建的时候,都调用 createBackground 方法去创建一遍背景了。

这块逻辑其实不复杂,在我们这个例子中,其实不需要做性能优化。

但是如果以后碰到很复杂的绘制逻辑,同样是需要优化的,不然每次都来绘制一遍,大大的降低了 canvas 的性能。

基于这种优化的思路,我想出了以下几种解决方案:

  1. 用背景图片代替 canvas 背景图绘制代码
  2. 先画出来,然后用 putImageData 方法,将背景图的数据信息填充到每个公告牌的 canvas 中去
  3. 先画出来,然后用 drawImage 的方式将 canvas 背景图画到每个公告牌的 canvas 中去

稍微思索了一番以后,我马上就将第一种方案给排除掉了,因为这种方案可行,但是还是跟我的初衷相违背,我想要的是背景图的可定制性,你一旦给我一张图片,换图片就太麻烦了,非常不灵活。

比如我想换下配色,我还得找设计师重新设计一张新的图片,太过于繁琐了。

既然排除了第一种方案,那么接下来就需要从第二种方案和第三种方案里面选择一种比较好的方案了。

在比较了第二种方案和第三种方案以后,我想当然的认为,第二种方案应该是这个问题里面的最优解了。

毕竟我拿到背景图上每个像素的数据以后,逐个填充到一个新的 canvas 上去的话,难道不必我绘制一张 canvas 图片到新的 canvas 上面去更快么?

事实证明,确实不如我想象中的这般,drawImage 真的就比 putImageData 更快。

我分别用两种方法进行了测试,最后是 drawImage 胜出。

至于为什么 drawImage 比 putImageData 更快,我当然是无法回答的,只得求助于 Google 来解决这个问题了。

经过一番搜素,我找到了在 stackoverflow 上,同样有过我这种困惑的童鞋问的问题:https://stackoverflow.com/questions/3952856/why-is-putimagedata-so-slow

不得不说,stackoverflow 真是程序员必备的网站,很多你想到的或者意想不到的问题,在这个上面统统能找到相似的讨论。

题主也发现了用 putImageData 绘图速度很慢,没有 drawImage 速度快,所以他提出了这个问题。

有兴趣的童鞋可以去原页面去查看一下讨论。

所以结论就是,如果你需要缓存 canvas 上的某一部分内容的时候,用一个 canvas 作为缓存,在使用的时候,通过 drawImage 再还原回去就行了。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值