使用canvas绘图的前端工程师可能都会遇到过canvas绘图黑屏、白屏的问题。以前写过一篇文章描述过hybrid下canvas黑屏的一种可能性:
http://blog.csdn.net/yuhk231/article/details/53560782
那么如果我们面对的场景就是移动端各大浏览器呢?当出现黑屏、白屏的时候就只能认栽了吗?不是的,还有很多优化工作是我们可以做的。在这里本文就将详细分析绘图策略中最伟大的方法:双缓冲绘图机制。
百度百科搜索双缓冲技术绘图,解释如下:双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。拿html5-canvas场景来举例:就是我们创建两个相同的画布,一张画来打草稿(在内存中的canvas虚拟节点上绘制),一张真正画给观众看(真正的页面dom元素canvas绘制)。
介绍完了背景知识,那这种技术到底能做什么?说了那么多和本文的主题解决黑屏白屏问题又有什么关联?
先来讨论黑屏的情况。我们知道,移动设备上canvas呈现出黑屏很大一部分原因就是绘图的计算量太大,导致设备显示出现问题。那么最直接的解决方案就是减少计算量(或者称为优化)。
正常情况下,我们会为页面添加一个canvas元素,并且一边算各种图形坐标,一边一点点的绘制上去。在这个过程当中我们就损耗了相当大的性能,每执行一次context.fill()或者context.stroke(),图形都会被渲染一次。而利用双缓冲机制,我们这样处理:为页面添加一个canvas元素,同时创建另一个一个一模一样的canvas元素,但是不添加到页面dom树(也就是存在内存中),这时我们在内存中的canvas元素上一边算图形坐标一边绘制,当绘制完成时直接调用canvas的复制方法(context.drawImage)一次性将“草稿画布”上的图形绘制到真正的页面canvas元素上,通过这样的技巧,我们将会节省大量页面绘制的性能损耗,不但能解决黑屏的问题,还能缩短图形绘制的时间。这里有一篇可以说明虚拟画布和普通画布差异的文章:
http://blog.csdn.net/yuhk231/article/details/54376525
下面我们来描述一下javascript如何实现上文所述的双缓冲机制。
先来看一段很普通的绘制一个正方形的代码:
//获取页面中的canvas画布容器,通常为一个div
var container=document.getElementById("container");
//创建一个真实的画布,他将呈现给用户
var realCanvas=document.createElement("canvas");
//设置realCanvas的width/height属性,乘以二以防止像素点模糊问题
realCanvas.width=container.clientWidth*2;
realCanvas.height=container.clientHeight*2;
//设置realCanvas的样式width/height属性,填充满父元素
realCanvas.style.width=container.clientWidth+"px";
realCanvas.style.height=container.clientHeight+"px";
container.appendChild(realCanvas);
//开始绘制
var realContext=realCanvas.getContext("2d");
realContext.fillRect(0,0,100,100);
下面一段代码与前面的代码功能完全相同,但是实现时使用了双缓冲绘图机制:
//获取页面中的canvas画布容器,通常为一个div
var container=document.getElementById("container");
//创建一个真实的画布,他将呈现给用户
var realCanvas=document.createElement("canvas");
//设置realCanvas的width/height属性,乘以二以防止像素点模糊问题
realCanvas.width=container.clientWidth*2;
realCanvas.height=container.clientHeight*2;
//设置realCanvas的样式width/height属性,填充满父元素
realCanvas.style.width=container.clientWidth+"px";
realCanvas.style.height=container.clientHeight+"px";
container.appendChild(realCanvas);
//创建一个虚拟节点cacheCanvas,我们不会将他添加到页面上
var cacheCanvas=document.createElement("canvas");
//设置cacheCanvas的width/height属性,和realCanvas的完全相同
cacheCanvas.width=container.clientWidth*2;
cacheCanvas.height=container.clientHeight*2;
//开始绘制,获取真实节点和虚拟节点的上下文context
var realContext=realCanvas.getContext("2d");
var cacheContext=cacheCanvas.getContext("2d");
/*
* 这次试用双缓冲技术,我们在cacheCanvas中绘制,
* 但此时什么都看不到,因为cacheCanvas没有添加到页面上
*/
cacheContext.fillRect(0,0,100,100);
/*
* 在缓冲区完成绘制之后,我们将缓冲区的内容一次性绘制到页面上
*/
realContext.drawImage(cacheCanvas,0,0);
经过了这样的优化之后,一般的图形都不会在出现HTML5-canvas黑屏的现象了。
说完了黑屏,接下来说说白屏。
先简单阐述一下为什么会产生白屏。首先可以肯定的是:白屏一定是程序有问题!最常见的情况:
(1)绘图代码报错,导致绘图不执行,因此出现白屏
(2)计算图形坐标的时候算错,比如计算出无穷大或者undefined或者超出画布边界的坐标值,导致你画的图看不见(HTML5-canvasAPI在执行绘图方法时传入的数据异常时不会抛出任何错误),也是白屏。
(3)没有给画布canvas设置width或height或值为0,比如画布初始化的时候页面还没有撑开,导致画布canvas元素的高度或者宽度为零,也会出现白屏。
(4)如果看到这里仍然没有解决你的白屏问题,那么恭喜你,本文将帮助你解决“canvas动画白屏闪烁”问题。首先简单说下什么是“canvas动画白屏闪烁”。
做过H5-canvas动画的读者肯定知道动画制作的基本原理:
1.将画布清空
2.计算下一帧图形并绘制
3.空出一帧的时间(通常为16ms)
4.循环第一步
我们用一个简单的线条变长的动画代码为示例,进一步说明这四步动画过程:
//获取页面中的canvas画布容器,通常为一个div
var container=document.getElementById("container");
//创建一个真实的画布,他将呈现给用户
var realCanvas=document.createElement("canvas");
//设置realCanvas的width/height属性,乘以二以防止像素点模糊问题
realCanvas.width=container.clientWidth*2;
realCanvas.height=container.clientHeight*2;
//设置realCanvas的样式width/height属性,填充满父元素
realCanvas.style.width=container.clientWidth+"px";
realCanvas.style.height=container.clientHeight+"px";
container.appendChild(realCanvas);
/*-------------------初始化--------------------*/
//开始绘制
var realContext=realCanvas.getContext("2d");
//初始化线条长度,这个长度将会随着动画演进而变长
var lineHeight=0,
//设置动画帧间隔为16毫秒
tick=16;
//使用setTimeout定时器保证动画帧间隔相同
setTimeout(function animate(){
//步骤一
realContext.clearRect(0,0,realCanvas.width,realCanvas.height);
//步骤二
lineHeight++;
//步骤三
realContext.moveTo(10,10);
realContext.lineTo(10,lineHeight);
//步骤四,在下一个定时器中调用内联函数,从而创造出流畅的动画
setTimeout(animate,tick);
},tick);
用一个图形来形象的表示这段代码的过程:
这幅图表示了我们用普通动画代码实现方案时程序执行的过程,首先设置了帧间隔16毫秒,我们在javascript中使用定时器”setTimeout“方法来每个16毫秒执行一次动画绘图;然后当方法开始执行时,我们擦除上一帧的图像(清空画布);接着计算下一帧图像的坐标点;最终绘制新的图像,然后下一帧循环往复,形成了流畅的动画。很多细心的读者应该已经发现了,当我们要绘制时,先执行了擦除,再来计算图形以及绘制,那么计算图形这段时间画布不就会显示“空白”。在前文提供的例子里大家永远不会看到画布空白,因为计算线条图形的时间太短了(小于10纳秒),但是当我们绘制的动画图形足够复杂时,在某些计算能力不足的设备的浏览器上就会看到“canvas动画白屏闪烁”。双缓冲绘图技术同样可以巧妙的解决这个问题。
---------------------------------------------------------------------
可能会有人想说把“计算图形”和“擦除”两个步骤对换位置不就解决问题了?看下面绘制五条线条的动画代码你就会知道,实际场景中我们编写的代码往往会把“计算图形”和“绘制”混杂在一起,使得“擦除”必须在之前执行。
//五条线的初始化长度
var lineHeight=[0,0,0,0,0],
tick=16;
setTimeout(function animate(){
//“擦除”操作还能放在哪里呢?
realContext.clearRect(0,0,realCanvas.width,realCanvas.height);
for(var i=0;i<5;i++){
realContext.moveTo(10*i,10);
//使用lineHeight++的方式,使得计算图形和绘制混杂在了一起
realContext.lineTo(10*i,lineHeight[i]++);
}
setTimeout(animate,tick);
},tick);
---------------------------------------------------------------------
对于如何使用双缓冲绘图机制解决“白屏闪烁”的问题,我们同样先展示代码,然后通过一张流程图形来详细讲解实现过程:
//初始化元素,会创建一个真实画布和虚拟画布
var container=document.getElementById("container");
var realCanvas=document.createElement("canvas");
var cacheCanvas=document.createElement("canvas");
realCanvas.width=container.clientWidth*2;
realCanvas.height=container.clientHeight*2;
cacheCanvas.width=container.clientWidth*2;
cacheCanvas.height=container.clientHeight*2;
realCanvas.style.width=container.clientWidth+"px";
realCanvas.style.height=container.clientHeight+"px";
container.appendChild(realCanvas);
var realContext=realCanvas.getContext("2d");
var cacheContext=cacheCanvas.getContext("2d");
/*-------------------初始化完毕--------------------*/
var lineHeight=0,tick=16;
//第一遍绘制
cacheContext.clearRect(0,0,realCanvas.width,realCanvas.height);
lineHeight++;
cacheContext.moveTo(10,10);
cacheContext.lineTo(10,lineHeight);
//将cacheCanvas一次性整张画到realCanvas上
realContext.drawImage(cacheCanvas,0,0);
setTimeout(function animate(){
//先在虚拟画布上绘制,此时不影响页面
cacheContext.clearRect(0,0,realCanvas.width,realCanvas.height);
cacheContext++;
cacheContext.moveTo(10,10);
cacheContext.lineTo(10,lineHeight);
//此时重绘页面
realContext.clearRect(0,0,realCanvas.width,realCanvas.height);
realContext.drawImage(cacheCanvas,0,0);
setTimeout(animate,tick);
},tick);
流程图如下:
这一次由于我们将“重新计算坐标点”以及“重新绘制”两个耗时操作移到了虚拟画布上(也就是双缓冲机制),而在最后才通过realContext.drawImage()的方式一次性将虚拟画布上的图形呈现到页面上(这个操作通常小于1ms)。通过这种优化手段,即使再复杂的图形绘制,页面上也不会再看到动画帧之间的白屏问题,耗时操作都在虚拟节点上执行,真正的画布只会在虚拟画布准备好图形之后才擦除+重绘。
熬夜不易,请作者喝酒!