HTML5/Canvas 支持双缓冲吗?

91
我想做的是在缓冲区上绘制图形,然后将其原样复制到画布上,以便进行动画处理并避免闪烁问题。但是我没有找到这个选项。有人知道我该怎么做吗?

12
我发现浏览器会对画布绘制进行合并,因此动画效果会更加流畅。你能否分享一些代码,让它像你描述的那样闪烁? - eyelidlessness
2
我注意到在使用explorercanvas时,IE在某些情况下会闪烁,但这当然不是HTML5,而只是由VML模拟的canvas元素。我从未见过其他浏览器这样做。 - cryo
3
与https://dev59.com/gWgt5IYBdhLWcg3w2A77相关。 - julien
2
真的很蠢的初学者代码,不会闪烁。http://jsfiddle.net/linuxlizard/ksohjr4f/3/ 按理说,应该会闪烁。浏览器真是令人印象深刻。 - David Poole
1
只有在使用异步绘制函数时才需要双缓冲。只要不让浏览器停下来,即使是同步绘制,也不会出现问题。一旦添加了 Promise 或 setTimeout 等内容,就会将控制权交还给浏览器,并在完成之前绘制当前状态,从而导致闪烁。 - albertjan
13个回答

83

一个非常简单的方法是在同一屏幕位置上有两个canvas元素,并设置所需显示的缓冲的可见性。在隐藏的画布上绘制,完成后翻转即可。

一些代码:

CSS:

A very simple method is to have two canvas-elements at the same screen location and set visibility for the buffer that you need to show. Draw on the hidden and flip when you are done.

Some code:

CSS:

canvas { border: 2px solid #000; position:absolute; top:0;left:0; 
visibility: hidden; }

JS中的翻转操作:

Buffers[1-DrawingBuffer].style.visibility='hidden';
Buffers[DrawingBuffer].style.visibility='visible';

DrawingBuffer=1-DrawingBuffer;
在这段代码中,数组 'Buffers[]' 包含了两个canvas对象。因此当你想要开始绘制时,仍然需要获取上下文(context):
var context = Buffers[DrawingBuffer].getContext('2d');

离题:我个人喜欢使用<noscript>,并在Javascript中创建我的画布元素。您通常会想要在Javascript中检查canvas支持,那么为什么要在HTML中放置canvas回退消息呢? - Shea

43
以下有用的链接除了展示使用双缓冲的例子和优势,还展示了使用html5画布元素的其他性能技巧。它包括jsPerf测试的链接,这些测试结果会聚合到Browserscope数据库中,跨浏览器验证了性能技巧。

https://web.dev/canvas-performance/

为了方便起见,我已经包括了一份最小的示例,展示了文章中描述的有效双缓冲技术。
// canvas element in DOM
var canvas1 = document.getElementById('canvas1');
var context1 = canvas1.getContext('2d');

// buffer canvas
var canvas2 = document.createElement('canvas');
canvas2.width = 150;
canvas2.height = 150;
var context2 = canvas2.getContext('2d');

// create something on the canvas
context2.beginPath();
context2.moveTo(10,10);
context2.lineTo(10,30);
context2.stroke();

//render the buffered canvas onto the original canvas element
context1.drawImage(canvas2, 0, 0);

20

11
我注意到屏幕出现了闪烁或撕裂的情况,不确定如何描述。使用的是最新的Linux Chrome浏览器。 - grom

18
你可以始终执行 var canvas2 = document.createElement("canvas"); 而不将其附加到DOM中。
只是说因为你们似乎对 display:none; 迷恋不已, 我觉得这样做更加干净,并更准确地模仿了双缓冲的思想,而不仅仅是拥有一个尴尬的不可见画布。

11

1
你如何解释使用双缓冲所见证的性能提升?http://www.html5rocks.com/en/tutorials/canvas/performance/ - Rick Suggs
@ricksuggs嗯,html5rocks上提到的“双缓冲”或“离屏渲染”与我想的有些不同。我认为DB是交换屏幕页面(在vram中),这实际上只是一个指针操作,而不是复制内存块。 OP要求使用DB来避免闪烁,这确实可以通过requestAnimationFrame()解决。也许这篇关于离屏渲染的文章可能会有趣。在那里,我回答了OP关于使用Sprite Buffer将缓冲图像数据复制和粘贴到屏幕的问题。 - ohager

9
对于不信者,这里有一些闪烁的代码。请注意,我明确地清除以擦除先前的圆形。

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

function draw_ball(ball) {
    ctx.clearRect(0, 0, 400, 400);
    ctx.fillStyle = "#FF0000";
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, 30, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}

var deltat = 1;
var ball = {};
ball.y = 0;
ball.x = 200;
ball.vy = 0;
ball.vx = 10;
ball.ay = 9.8;
ball.ax = 0.1;

function compute_position() {
    if (ball.y > 370 && ball.vy > 0) {
        ball.vy = -ball.vy * 84 / 86;
    }
    if (ball.x < 30) {
        ball.vx = -ball.vx;
        ball.ax = -ball.ax;
    } else if (ball.x > 370) {
        ball.vx = -ball.vx;
        ball.ax = -ball.ax;
    }
    ball.ax = ball.ax / 2;
    ball.vx = ball.vx * 185 / 186;
    ball.y = ball.y + ball.vy * deltat + ball.ay * deltat * deltat / 2
    ball.x = ball.x + ball.vx * deltat + ball.ax * deltat * deltat / 2
    ball.vy = ball.vy + ball.ay * deltat
    ball.vx = ball.vx + ball.ax * deltat
    draw_ball(ball);
}

setInterval(compute_position, 40);
<!DOCTYPE html>
<html>
<head><title>Basketball</title></head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
</body></html>


4
对我来说似乎没有闪烁,至少当球每帧移动的距离不超过其半径时没有闪烁。Chrome/Windows。 - Jules
10
我已经在http://jsfiddle.net/GzSWJ/28/的示例中添加了一个请求requestAnimFrame的调用 - 在这里查看这里 - 现在它不再闪烁。 - rhu

7
Josh之前询问了浏览器如何知道“绘制过程何时结束”以避免闪烁。我会直接评论他的帖子,但我的声望还不够高,所以我给出自己的看法。我没有事实来支持它,但我对此感到非常有信心,这可能对未来阅读此内容的其他人有所帮助。
我猜测浏览器并不知道你何时完成绘制。但是,就像大多数JavaScript一样,只要你的代码在不放弃控制权的情况下运行,浏览器就基本上被锁定了,并且不会/无法更新/响应其UI。我猜如果你清除画布并在没有将控制权交还给浏览器的情况下绘制整个帧,它实际上不会绘制你的画布,直到你完成为止。
如果你设置了一个渲染跨越多个setTimeout/setInterval/requestAnimationFrame调用的情况,在其中一个调用中清除画布并在接下来的几个调用中绘制画布元素,重复循环(例如)每5个调用一次,我愿意打赌,你会看到闪烁,因为画布会在每个调用后更新。
话虽如此,我不确定我是否会相信。我们已经到了JavaScript编译为本机机器代码执行的点(至少据我所知,Chrome的V8引擎是这样做的)。我不会感到惊讶,因为不久之后,浏览器开始在UI以外的单独线程中运行它们的JavaScript,并同步任何访问UI元素以允许UI在未访问UI的JavaScript执行期间更新/响应。如果发生这种情况(我知道还有很多障碍需要克服,比如事件处理程序在你仍在运行其他代码时启动),我们可能会看到没有使用某种双缓冲技术的画布动画闪烁。
就个人而言,我喜欢两个画布元素相互重叠并在每个帧上交替显示/绘制的想法。相当不显眼,可能可以通过几行代码轻松添加到现有应用程序中。

没错。请参考这里的JSFiddle示例:https://dev59.com/gWgt5IYBdhLWcg3w2A77 - Eric Rini

6

在Web浏览器中不存在闪烁问题!它们已经使用双缓冲技术进行渲染。JavaScript引擎会在显示之前完成所有渲染。此外,上下文保存和恢复仅堆栈转换矩阵数据等内容,而不是画布内容本身。 因此,您不需要或不想使用双缓冲技术!


8
你能提供一些支持你说法的证据吗? - Rick Suggs
@ricksuggs 我发现不使用数据库会导致动画出现不流畅的情况,我还没有尝试过使用数据库。 - FutuToad

4

你需要两个画布:(注意CSS的z-index和position:absolute)

<canvas id="layer1" width="760" height="600" style=" position:absolute; top:0;left:0; 
visibility: visible;  z-index: 0; solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
<canvas id="layer2" width="760" height="600" style="position:absolute; top:0;left:0; 
visibility: visible;  z-index: 1; solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>

您可以注意到第一个画布是可见的,而第二个则被隐藏了。这样做的目的是在隐藏的画布上绘制,随后我们将隐藏可见的画布并使隐藏的画布变为可见。当它被隐藏时 '清除隐藏画布'

<script type="text/javascript">
var buff=new Array(2);
buff[0]=document.getElementById("layer1");
buff[1]=document.getElementById("layer2");

ctx[0]=buff[0].getContext("2d");
ctx[1]=buff[1].getContext("2d");
var current=0;
// draw the canvas (ctx[ current ]);
buff[1- current ].style.visibility='hidden';
buff[ current ].style.visibility='visible';
ctx[1-current].clearRect(0,0,760,600);
current =1-current;

不错的实现,如果有人需要手动应用双缓冲技术。只需在上面的空行中添加以下行:var ctx = new Array(2); - dimmat

4

与其自己动手写,使用现有的库来创建干净、无闪烁的JavaScript动画可能会更好:

这里有一个受欢迎的库:http://processingjs.org


100
没错!为什么要费劲写10行自己的代码,当你可以毫不知情地使用一个275KB的整个库呢?是的,我是在讽刺。 - Tom

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