一般来说,在H5开发中,使用canvas往往只是为了展示一些简单的图表或者简单短小的动画,很少考虑到有闪烁的问题。
最近,在手机QQ魔法表情的项目中,就遇到了奇葩的闪烁问题。
这里说的闪烁,是指动画刚开始播放,突然出现瞬间空白(大概1帧到2帧的时间)。
闪烁分析
这个魔法表情,实际是html5版本的动画,使用Fanvas(即将腾讯开源),从swf转化为canvas 2d动画。
在iOS体系下,无论哪个机型还是哪个系统版本,都没有出现问题。
但是,在部分Android机器上则出现了很奇葩的闪烁,包括小米note,小米4,三星,魅族。奇怪的是,小米同体系的红米note则完全正常。
翻阅H5 api的资料,我们知道requestAnimationFrame在Android 4.4后才支持,而动画的机制是,如果该接口不可用,则采用setInterval取代。
那么貌似有点眉目了,红米note也是4.4系统,而iOS全系都ok,也许问题就在这。
重温一下FPS和浏览器重绘的知识。浏览器保持一个帧频(一般60fps)刷新画面,这就包括页面中的canvas。而动画的绘制过程,包括几个步骤:
1、擦除整个canvas;
2、计算所有元件/图元的位置颜色;
3、逐个逐个,绘制所有元件到canvas上。
这个过程,由不精准的setInterval驱动,这个时钟无法跟浏览器重绘的频率同步。
那么,就可能出现这样的时序情况:
1、擦除整个canvas;
2、浏览器到达重绘时间点,此时canvas为空白,浏览器绘制空白的canvas;
3、50ms后,这一帧动画所有元件绘制完成(可能会因为动画复杂, 而消耗长时间,超过16ms)
关键点就在这里了。
好招不怕旧
双缓冲,只要对图形图象处理编程有稍稍一些了解,都应该听过这个术语,即使不知道这玩意是什么。这个技术非常非常古老,也非常非常简单,但效果却非常非常好。
来看看百度百科的说明,可能没有wikipedia专业,但我觉得足够解释问题了。
闪烁是图形编程的一个常见问题。需要多重复杂绘制操作的图形操作会导致呈现的图像闪烁或具有其他不可接受的外观。双缓冲的使用解决这些问题。双缓冲使用内存缓冲区来解决由多重绘制操作造成的闪烁问题。当启用双缓冲时,所有绘制操作首先呈现到内存缓冲区,而不是屏幕上的绘图图面。所有绘制操作完成后,内存缓冲区直接复制到与其关联的绘图图面。因为在屏幕上只执行一个图形操作,所以消除了由复杂绘制操作造成的图像闪烁。
回到我们的动画中,发现异曲同工,闪烁、掉帧的问题根源就是因为部分机型下没有自动实现cnavas的双缓冲(一般这些都是底层实现的),而canvas每一帧动画过程又比较漫长,擦除上一帧动画后,要过几十毫秒才能绘制完成下一帧,而这段时间内就会出现明显的空白。
解决办法就是:
创建一个临时canvas,先把下一帧动画绘制到临时canvas上。在每次真正绘制的时候,擦除正式canvas后,马上drawImage把临时canvas的内容copy过去,而这个copy过程是非常非常高效的,所以基本可以杜绝闪烁。
具体代码
p.update = function() { if (!this.cacheCanvas) { this.cacheCanvas = document.createElement("canvas"); this.cacheCanvas.width = this.canvas.width; this.cacheCanvas.height = this.canvas.height; } updateMovieClip(); //图形变换 var ctx = this.cacheCanvas.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.save(); ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); //部分Android机器很奇葩,如果局部刷新会出现空白的情况 drawMovieclip(ctx); //绘制 ctx.restore(); //双缓冲,先画到临时canvas,再转到正式canvas ctx = this.canvas.getContext("2d"); ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); ctx.drawImage(this.cacheCanvas, 0, 0, this.canvas.width, this.canvas.height); };