去年圣诞节产品提了一个活动需求,其中有一个下雪的背景动画。在作这个动画的过程当中加深了对 canvas 动画的一些了解,在这里我仅是抛砖引玉的分享一下,欢迎各位大佬批评。javascript
代码已上传至 github 【https://github.com/wanqihua/koa-canvas】,感兴趣的能够 clone 代码到本地运行。望给个 star 支持一下。前端
需求给出的 UI 样式以下:java
UI 的需求是雪花下落的方向有点倾斜角度,每片雪花的下落速度不同但要保持在一个范围内。git
需求了解的差很少就开始实现这个效果(在看这篇文章以前你须要对 canvas 的一些基本 API 了解)。github
drawImage 可传入 9 个参数,上图中的 5 个参数是比较经常使用的,另外几个参数是拿来剪切图片的。web
直接使用 drawImage 来剪切图片,其性能不会太好,建议先将须要使用的部分用一个离屏 canvas 保存起来,须要用到的时候直接使用便可。canvas
requestAnimationFrame 相对于 setinterval 处理动画有如下几个优点:浏览器
通过浏览器优化,动画更流畅缓存
窗口没激活时,动画将中止,省计算资源性能优化
这个 API 不须要传入动画间隔时间,这个方法会告诉浏览器以最佳的方式进行动画重绘。
因为兼容性问题,可使用如下方法对 requestAnimationFrame 进行重写:
window.requestAnimationFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })();
对于其余 API 烦请查阅文档。
有一个大概想法后就开心的开始写代码了,基本思路就是使用 requestAnimationFrame 来刷新 canvas 画板。
因为雪花不规则,因此雪花是 UI 提供的图片,既然是图片咱们就须要先将图片预加载好,要否则在转换图片的时候极可能影响性能。
使用的预加载方法以下:
function preloadImg(srcArr){ if(srcArr instanceof Array){ for(let i = 0; i < srcArr.length; i++){ let oImg = new Image(); oImg.src = srcArr[i]; } } }
前先后后写了一个下午,算是写好了,在手机上查看的效果发现非常卡顿。100 片雪花 FPS 居然才 40 多。并且在某些机型会出现抖动的状况。
要是产品看到这个效果,恐怕是又要召集相关人员开相关会议了。这么卡顿确定是写了些开销大的代码,因而乎须要第二次尝试。
晚上仍是须要按时下班的。不过下班回家后也不能闲着,开始找相关的资料,以便次日快速的完成。
通过一个晚上的查找学习,大概知道了如下几个优化 canvas 性能的方法:
分层的目的是下降彻底没必要要的渲染性能开销。
即:将变化频率高、幅度大的部分和变化频率小、幅度小的部分分红两个或两个以上的 canvas 对象。也就是说生成多个 canvas 实例,把它们重叠放置,每一个 Canvas 使用不一样的 z-index 来定义堆叠的次序。
<canvas style="position: absolute; z-index: 0"></canvas> <canvas style="position: absolute; z-index: 1"></canvas> // js 代码
上面有提到。
通常状况下的性能: clearRect > fillRect > canvas.width=canvas.width;
当时用 drawImage 绘制一样的一块区域:
若数据源(图片、canvas)和 canvas 画板的尺寸相仿,那么性能会比较好;
第二种状况咱们就能够先把待绘制的区域裁剪好,保存在一个离屏的 canvas 对象中。在绘制每一帧的时候,在将这个对象绘制到 canvas 画板中。
drawImage 方法的第一个参数不只能够接收 Image 对象,也能够接收另外一个 Canvas 对象。并且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎彻底一致。
当每一帧须要调用的对象须要屡次调用 canvasAPI 时,咱们也可使用离屏绘制进行预渲染的方式来提升性能。
即:
let cacheCanvas = document.createElement("canvas"); let cacheCtx = this.cacheCanvas.getContext("2d"); cacheCtx.save(); cacheCtx.lineWidth = 1; for(let i = 1;i < 40; i++){ cacheCtx.beginPath(); cacheCtx.strokeStyle = this.color[i]; cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI); cacheCtx.stroke(); } this.cacheCtx.restore(); // 在绘制每一帧的时候,绘制这个图形 context.drawImage(cacheCtx, x, y);
cacheCtx 的宽高尽可能设置成实际使用的宽高,不然过多空白区域也会形成性能的损耗。
下图显示了使用离屏绘制进行预渲染技术所带来的性能改善状况:
以下代码:
for (var i = 0; i < points.length - 1; i++) { var p1 = points[i]; var p2 = points[i + 1]; context.beginPath(); context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); context.stroke(); }
能够改为:
context.beginPath(); for (var i = 0; i < points.length - 1; i++) { var p1 = points[i]; var p2 = points[i + 1]; context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); } context.stroke();
tips: 写粒子效果时,可使用方形替代圆形,由于粒子小,因此方和圆看上去差很少。有人问为何?很容易理解,画一个圆须要三个步骤:先 beginPath,而后用 arc 画弧,再用 fill。而画方只须要一个 fillRect。当粒子对象达必定数量时性能差距就会显示出来了。
进行 canvas 动画绘制时,若坐标是浮点数,可能会出现 CSSSub-pixel 的问题.也就是会自动将浮点数值四舍五入转为整数,在动画的过程当中就可能出现抖动的状况,同时也可能让元素的边缘出现抗锯齿失真状况。
虽然 javascript 提供了一些取整方法,像 Math.floor, Math.ceil, parseInt,但 parseInt这个方法作了一些额外的工做(好比检测数据是否是有效的数值、先将参数转换成了字符串等),因此,直接用 parseInt 的话相对来讲比较消耗性能。
能够直接用如下巧妙的方法进行取整:
function getInt(num){ var rounded; rounded = (0.5 + num) | 0; return rounded; }
另 for 循环的效率是最高的,感兴趣的能够自行实验。
经过昨天晚上的查阅,对这个动画作了如下几点优化:
使用离屏绘制进行预渲染
减小部分 API 的使用
浮点数取整
缓存变量
使用 for 循环,替代 forEach
方案写好了就开始愉快的写代码了。
200 片雪花的时候 FPS 基本稳定在 60,并且抖动的状况也没了;
增长到 1000 片的时候, FPS 仍是基本稳定在 60;
增长到 1500 片的时候,稍微有点零星的卡帧;
增长到 2000 片的时候,开始卡顿。
这说明这个动画仍是没有优化好,还有优化空间,请各位大佬不吝指教。
推荐使用 stats.js 插件,这个插件能够显示动画运行时的 FPS。
let snowBox = function () { let canvasEl = document.getElementById("snowFall"); let ctx = canvasEl.getContext('2d'); canvasEl.width = window.innerWidth; canvasEl.height = window.innerHeight; let lineList = []; // 雪的容器 let snow = function () { let _this = this; _this.cacheCanvas = document.createElement("canvas"); _this.cacheCtx = _this.cacheCanvas.getContext("2d"); _this.cacheCanvas.width = 10; _this.cacheCanvas.height = 10; _this.speed = [1, 1.5, 2][Math.floor(Math.random()*3)]; // 雪花下落的三种速度,便于取整 _this.posx = Math.round(Math.random() * canvasEl.width); // 雪花x坐标 _this.posy = Math.round(Math.random() * canvasEl.height); // 雪花y坐标 _this.img = `./img/snow_(${Math.ceil(Math.random() * 9)}).png`; // img _this.w = _this.getInt(5 + Math.random() * 6); _this.h = _this.getInt(5 + Math.random() * 6); _this.cacheSnow(); }; snow.prototype = { cacheSnow: function () { let _this = this; // _this.cacheCtx.save(); let img = new Image(); // 建立img元素 img.src = _this.img; _this.cacheCtx.drawImage(img, 0, 0, _this.w, _this.h); // _this.cacheCtx.restore(); }, fall: function () { let _this = this; if (_this.posy > canvasEl.height + 5) { _this.posy = _this.getInt(0 - _this.h); _this.posx = _this.getInt(canvasEl.width * Math.random()); } if (_this.posx > canvasEl.width + 5) { _this.posx = _this.getInt(0 - _this.w); _this.posy = _this.getInt(canvasEl.height * Math.random()); } // 若是雪花在可视区域 if (_this.posy <= canvasEl.height || _this.posx <= canvasEl.width) { _this.posy = _this.posy + _this.speed; _this.posx = _this.posx + _this.speed * .5; } _this.paint(); }, paint: function () { ctx.drawImage(this.cacheCanvas, this.posx, this.posy) }, getInt: function(num){ let rounded; rounded = (0.5 + num) | 0; return rounded; } }; let control; control = { start: function (num) { for (let i = 0; i < num; i++) { let s = new snow(); lineList.push(s); } (function loop() { ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); for (let i = 0; i < num; i++) { lineList[i].fall(); } requestAnimationFrame(loop) })(); } }; return control; }(); window.onload = function(){ snowBox.start(2000) };
建议从 github clone 代码到本地运行。
这篇文章虽说是关于 canvas 动画的性能优化。一些大佬也已经看出,其余方面的性能优化方案和这个大抵相同,无非是:
减小 API 的使用 使用缓存(重点) 合并频繁使用的 API 避免使用高耗能的 API 用 webWorker 来处理一些比较耗时的计算 ……
但愿经过阅读这篇文章,能够在性能优化方面给你做一个参考,多谢阅读。
《前端词典》这个系列会持续更新,每一期我都会讲一个出现频率较高的知识点。但愿你们在阅读的过程中能够斧正文中出现不严谨或是错误的地方,本人将不胜感激;若经过本系列而有所得,本人亦将不胜欣喜。
内容: 前端以及网络相关知识点的介绍并加以实际应用做为辅助。
目的: 这个系列的文章能够对读者起到一点帮助,解开一些迷惑。
但愿各位多指点一二,不吝赐教。
若是你以为个人文章写的还不错,就关注我呗!