如何一次性在画布上绘制多个矩形?

4

我正在使用drawRect在HTML5画布上绘制大量矩形(约20000个),每个矩形都位于不同的预定位置。 我正在从循环中执行此操作:

for (var i = 0; i < 100; ++i) {
    for (var j = 0; j < 200; ++j) {
        context.fillStyle = '#000000';
        context.fillRect(i * 8, j * 2, 6, 1);
    }
}

JSFiddle上的演示

这段代码在800像素宽的画布上绘制了100个条形图,每个条形图宽度为6像素,由许多小的(1x6)矩形堆叠而成:在提供的示例中有200个静态计数,但在我的应用程序中会动态改变数量(因此需要重新渲染)。

这个过程本身只需要几十毫秒,没有什么严重的问题。但是这个整个过程会被反复调用,这会显著影响性能,并且不是好的方式。

是否有解决方案或变通方法,可以使这样的条形图在一次画布指令中绘制,希望更好地利用硬件加速?

在我的应用程序中,每个矩形都会发光,因此水平切割它们不是一个好的方法。我已经尝试使用离屏画布,在那里绘制,并将其图像输出呈现到主画布上,但性能没有明显提高。


你试过让屏幕外的画布在下一次迭代中保留吗?如果内容是“静态”的,为什么还要不断重绘它呢? - Amit
@Amit 根据输入数据,条形图的高度会发生变化,因此需要重新渲染。我想我会将这个编辑到问题中。 - John Weisz
1
相反的方法:在静态图像/离屏画布中拥有完整的条形图,将其复制到目标画布,然后“ctx.clearRect”不需要的区域。这样可以吗? - Amit
请参见http://mdn.github.io/voice-change-o-matic/。 - Alnitak
@Amit 是的,事实上,这正是我在考虑的。尽管这将需要单独进行一次操作来创建发光效果,因为渐变也会被切断。 - John Weisz
显示剩余4条评论
3个回答

2

制作一个单独的(离屏)画布图像,其中包含一个完整高度的条形图,然后对于每个动态高度的条形图,只需从该图像中复制所需数量的垂直像素到屏幕画布上。


@John White。Alnitak在这里给出了你的答案。复制图像比绘制新矩形要快得多。您可以使用drawImage的剪辑版本,仅从源栏中复制所需数量的垂直矩形到屏幕上的画布中。顺便说一句,我知道您的示例代码只是用于演示,但将context.fillStyle='#000000'移出循环--它只需要在循环开始之前设置一次。并且用while循环替换for循环也会使您的演示更快。 - markE
@markE 是的,这正是我想要做的,尽管我认为可能有一些微不足道的东西我忽略了。 - John Weisz
1
@JohnWhite。我忍不住要在加法和减法方法之间进行时间测试:https://jsfiddle.net/m1erickson/0vjz3dqh/。使用任何一种方法,在我的相对较快的计算机上,每屏幕条形图的性能约为1毫秒。 - markE
1
@MarkE 在我的笔记本电脑上,一个条形方法的时间是约0.15毫秒,另一个方法的时间是0.45毫秒。 - Alnitak
@Alnitak,听起来是一台不错的笔记本电脑...我好嫉妒! - markE
1
@MarkE 早在2015年初,13英寸rMBP配备了双核3.1 GHz i7处理器和(相对较差的)集成显卡。 - Alnitak

2
您可以这样初始化有用的变量:

var context = canvas.getContext("2d");
var map = context.getImageData(startX, startY, width, height);

canvas是类似于document.getElementById("myCanvas")的返回值。

startX是起始x坐标,startY是起始y坐标。您可以将它们的值设为0,但我更喜欢不假设其起始点在画布的左上角。width是宽度,height是高度。现在,您可以使用循环并设置值,如下所示:

map.data[4 * index] = r; //red
map.data[4 * index + 1] = g; //green
map.data[4 * index + 2] = b; // blue

完成循环后,保存内容的方式如下所示:
context.putImageData(map, startX, startY);

速度应该得到提升,因为你只读取一次并且只绘制一次。至于循环,你只是在设置值,这与在画布中绘制相比是一个廉价的操作。所以,优化的思路是:一次性读取所有内容,在循环中设置值,只绘制一次而不是每次获取输入时都进行绘制。你还可以省略读取相关部分并自己生成数据,但我决定向你展示如何在需要了解已绘制内容的情况下读取数据,保留HTML标签。

这意味着一切都必须“手工完成”。Context2D对象中有很多功能,包括描边、填充、效果等。 - Amit
@Amit,不是真的。我的答案不是关于需要手动完成它。它是关于避免重复绘制的。我的解决方案建议是“手工完成”,因为我还没有足够的经验来熟知画布的功能。你可以像我的建议那样做,也可以从其他地方使用一些函数。虽然我没有隐藏你用来解决问题的方法的重要性,但我强调这里最重要的是想法:即应该减少绘图数量以进行优化。 - Lajos Arpad
@LajosArpad。我怀疑这种方法会慢得多,因为它改变的是单个像素而不是像素组,并且getImageDataputImageData没有硬件优化。 - markE
@markE,这个方法从画布中加载图像数据,解析它,然后存储它。如果该方法是单独存储每个像素,那么你关于单个像素更改的说法是正确的。相反,它准备所有像素,然后一次性存储准备好的值。因此,我不同意你的第一个说法。 - Lajos Arpad
@markE,关于您提到的硬件优化的第二个声明,我相信问题至少有点更加复杂,因为有许多浏览器跨越许多操作系统。除非您的声明对于每个浏览器都是正确的(即这些功能没有进行硬件优化),那么您就有了一个很好的观点。然而,是否有一些替代方案,可以执行相同任务并在所有成功的浏览器中进行硬件优化的某些功能呢? - Lajos Arpad
显示剩余3条评论

0

看起来这是一个背景,你打算在上面绘制其他东西,你考虑过将其转换为静态图像,并将画布背景设置为此图像吗?你可以关闭/打开背景,或者在填充/绘制发生的区域中,你所绘制的任何内容都会重叠在背景上。

如果你要动态调整它,那么在另一个完全相同位置的单独画布元素下方也是值得考虑的。


非常感谢您的帮助,但是根据输入数据,条形图的高度会发生变化,这就是为什么需要重新渲染的原因。我忘记在我的问题中包含这一点了。 - John Weisz
啊,好的!我误以为它是一个背景。对于一组输入数据,所有条形图的高度/宽度都相同吗?顺便说一下,如果不会改变的话,你可以将fillStyle移出循环以获得一些“微小”的性能提升。 - potatopeelings
是的,没错。实际上,它很快就会成为一个音频可视化组件,因此对于频率上相同的值,将绘制相同数量的这些小矩形。 - John Weisz

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