声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢!javascript
canvas玩多了后,就会自动的要开始考虑性能问题了。怎么优化canvas的动画呢?html
【使用缓存】前端
使用缓存也就是用离屏canvas进行预渲染了,原理很简单,就是先绘制到一个离屏canvas中,而后再经过drawImage把离屏canvas画到主canvas中。可能看到这不少人就会误解,这不是写游戏里面用的不少的双缓冲机制么?java
其实否则,双缓冲机制是游戏编程中为了防止画面闪烁,所以会有一个显示在用户面前的画布以及一个后台画布,进行绘制时会先将画面内容绘制到后台画布中,再将后台画布里的数据绘制到前台画布中。这就是双缓冲,可是canvas中是没有双缓冲的,由于现代浏览器基本上都是内置了双缓冲机制。因此,使用离屏canvas并非双缓冲,而是把离屏canvas当成一个缓存区。把须要重复绘制的画面数据进行缓存起来,减小调用canvas的API的消耗。git
众所周知,调用canvas的API很消耗性能,因此,当咱们要绘制一些重复的画面数据时,妥善利用离屏canvas对性能方面有很大的提高,能够看下下面的DEMOgithub
1 、 没使用缓存 web
2 、 使用了缓存
编程
能够看到上面的DEMO的性能不同,下面分析一下缘由:为了实现每一个圈的样式,因此绘制圈圈时我用了循环绘制,若是没用启用缓存,当页面的圈圈数量达到必定时,动画每一帧就要大量调用canvas的API,要进行大量的计算,这样再好的浏览器也会被拖垮啦。canvas
ctx.save(); var j=0; ctx.lineWidth = borderWidth; for(var i=1;i<this.r;i+=borderWidth){ ctx.beginPath(); ctx.strokeStyle = this.color[j]; ctx.arc(this.x , this.y , i , 0 , 2*Math.PI); ctx.stroke(); j++; } ctx.restore();
因此,个人方法很简单,每一个圈圈对象里面给他一个离屏canvas做缓存区。数组
除了建立离屏canvas做为缓存以外,下面的代码中有一点很关键,就是要设置离屏canvas的宽度和高度,canvas生成后的默认大小是300X150;对于个人代码中每一个缓存起来圈圈对象半径最大也就不超过80,因此300X150的大小明显会形成不少空白区域,会形成资源浪费,因此就要设置一下离屏canvas的宽度和高度,让它跟缓存起来的元素大小一致,这样也有利于提升动画性能。上面的四个demo很明显的显示出了性能差距,若是没有设置宽高,当页面超过400个圈圈对象时就会卡的不行了,而设置了宽高1000个圈圈对象也不以为卡。
var ball = function(x , y , vx , vy , useCache){ this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.r = getZ(getRandom(20,40)); this.color = []; this.cacheCanvas = document.createElement("canvas"); this.cacheCtx = this.cacheCanvas.getContext("2d"); this.cacheCanvas.width = 2*this.r; this.cacheCanvas.height = 2*this.r; var num = getZ(this.r/borderWidth); for(var j=0;j<num;j++){ this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)"); } this.useCache = useCache; if(useCache){ this.cache(); } }
当我实例化圈圈对象时,直接调用缓存方法,把复杂的圈圈直接画到圈圈对象的离屏canvas中保存起来。
cache:function(){ this.cacheCtx.save(); var j=0; this.cacheCtx.lineWidth = borderWidth; for(var i=1;i<this.r;i+=borderWidth){ this.cacheCtx.beginPath(); this.cacheCtx.strokeStyle = this.color[j]; this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI); this.cacheCtx.stroke(); j++; } this.cacheCtx.restore(); }
而后在接下来的动画中,我只须要把圈圈对象的离屏canvas画到主canvas中,这样,每一帧调用的canvasAPI就只有这么一句话:
ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);
跟以前的for循环绘制比起来,实在是快太多了。因此当须要重复绘制矢量图的时候或者绘制多个图片的时候,咱们均可以合理利用离屏canvas来预先把画面数据缓存起来,在接下来的每一帧中就能减小不少不必的消耗性能的操做。
下面贴出1000个圈圈对象流畅版代码:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <style> body{ padding:0; margin:0; overflow: hidden; } #cas{ display: block; background-color:rgba(0,0,0,0); margin:auto; border:1px solid; } </style> <title>测试</title> </head> <body> <div > <canvas id='cas' width="800" height="600">浏览器不支持canvas</canvas> <div style="text-align:center">1000个圈圈对象也不卡</div> </div> <script> var testBox = function(){ var canvas = document.getElementById("cas"), ctx = canvas.getContext('2d'), borderWidth = 2, Balls = []; var ball = function(x , y , vx , vy , useCache){ this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.r = getZ(getRandom(20,40)); this.color = []; this.cacheCanvas = document.createElement("canvas"); this.cacheCtx = this.cacheCanvas.getContext("2d"); this.cacheCanvas.width = 2*this.r; this.cacheCanvas.height = 2*this.r; var num = getZ(this.r/borderWidth); for(var j=0;j<num;j++){ this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)"); } this.useCache = useCache; if(useCache){ this.cache(); } } function getZ(num){ var rounded; rounded = (0.5 + num) | 0; // A double bitwise not. rounded = ~~ (0.5 + num); // Finally, a left bitwise shift. rounded = (0.5 + num) << 0; return rounded; } ball.prototype = { paint:function(ctx){ if(!this.useCache){ ctx.save(); var j=0; ctx.lineWidth = borderWidth; for(var i=1;i<this.r;i+=borderWidth){ ctx.beginPath(); ctx.strokeStyle = this.color[j]; ctx.arc(this.x , this.y , i , 0 , 2*Math.PI); ctx.stroke(); j++; } ctx.restore(); } else{ ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r); } }, cache:function(){ this.cacheCtx.save(); var j=0; this.cacheCtx.lineWidth = borderWidth; for(var i=1;i<this.r;i+=borderWidth){ this.cacheCtx.beginPath(); this.cacheCtx.strokeStyle = this.color[j]; this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI); this.cacheCtx.stroke(); j++; } this.cacheCtx.restore(); }, move:function(){ this.x += this.vx; this.y += this.vy; if(this.x>(canvas.width-this.r)||this.x<this.r){ this.x=this.x<this.r?this.r:(canvas.width-this.r); this.vx = -this.vx; } if(this.y>(canvas.height-this.r)||this.y<this.r){ this.y=this.y<this.r?this.r:(canvas.height-this.r); this.vy = -this.vy; } this.paint(ctx); } } var Game = { init:function(){ for(var i=0;i<1000;i++){ var b = new ball(getRandom(0,canvas.width) , getRandom(0,canvas.height) , getRandom(-10 , 10) , getRandom(-10 , 10) , true) Balls.push(b); } }, update:function(){ ctx.clearRect(0,0,canvas.width,canvas.height); for(var i=0;i<Balls.length;i++){ Balls[i].move(); } }, loop:function(){ var _this = this; this.update(); RAF(function(){ _this.loop(); }) }, start:function(){ this.init(); this.loop(); } } window.RAF = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); }; })(); return Game; }(); function getRandom(a , b){ return Math.random()*(b-a)+a; } window.onload = function(){ testBox.start(); } </script> </body> </html>
离屏canvas还有一个注意事项,若是你作的效果是会将对象不停地建立和销毁,请慎重使用离屏canvas,至少不要像我上面写的那样给每一个对象的属性绑定离屏canvas。
由于若是这样绑定,当对象被销毁时,离屏canvas也会被销毁,而大量的离屏canvas不停地被建立和销毁,会致使canvas buffer耗费大量GPU资源,容易形成浏览器崩溃或者严重卡帧现象。解决办法就是弄一个离屏canvas数组,预先装进足够数量的离屏canvas,仅将仍然存活的对象缓存起来,当对象被销毁时,再解除缓存。这样就不会致使离屏canvas被销毁了。
【使用requestAnimationFrame】
这个就不具体解释了,估计不少人都知道,这个才是作动画的最佳循环,而不是setTimeout或者setInterval。直接贴出兼容性写法:
window.RAF = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); }; })();
【避免浮点运算】
虽然javascript提供了很方便的一些取整方法,像Math.floor,Math.ceil,parseInt,可是,国外友人作过测试,parseInt这个方法作了一些额外的工做(好比检测数据是否是有效的数值,parseInt 甚至先将参数转换成了字符串!),因此,直接用parseInt的话相对来讲比较消耗性能,那怎样取整呢,能够直接用老外写的很巧妙的方法了:
rounded = (0.5 + somenum) | 0; rounded = ~~ (0.5 + somenum); rounded = (0.5 + somenum) << 0;
运算符不懂的能够直接戳:http://www.w3school.com.cn/js/pro_js_operators_bitwise.asp 里面有详细解释
【尽可能减小canvasAPI的调用】
做粒子效果时,尽可能少使用圆,最好使用方形,由于粒子过小,因此方形看上去也跟圆差很少。至于缘由,很容易理解,咱们画一个圆须要三个步骤:先beginPath,而后用arc画弧,再用fill进行填充才能产生一个圆。可是画方形,只须要一个fillRect就能够了。虽然只是差了两个调用,当粒子对象数量达到必定时,这性能差距就会显示出来了。
还有一些其余注意事项,我就不一一列举了,由于谷歌上一搜也挺多的。我这也算是一个给本身作下记录,主要是记录缓存的用法。想要提高canvas的性能最主要的仍是得注意代码的结构,减小没必要要的API调用,在每一帧中减小复杂的运算或者把复杂运算由每一帧算一次改为数帧算一次。同时,上面所述的缓存用法,我由于贪图方便,因此是每一个对象一个离屏canvas,其实离屏canvas也不能用的太泛滥,若是用太多离屏canvas也会有性能问题,请尽可能合理利用离屏canvas。
源码地址:https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Other-demo/cache
本人前端小菜,写的很差请见谅。