热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Canvas动画,下雪了

前言去年圣诞节有一个下雪的背景动画的需求。在实现这个动画的过程中加深了对canvas动画的一些了解,在这里我仅是抛砖引玉的分享一下,欢迎各位大佬批评。代

前言

去年圣诞节有一个下雪的背景动画的需求。在实现这个动画的过程中加深了对 canvas 动画的一些了解,在这里我仅是抛砖引玉的分享一下,欢迎各位大佬批评。

代码已上传至 github 【https://github.com/wanqihua/blog】,感兴趣的可以 clone 代码到本地运行。

入题

需求给出的 UI 样式如下:

UI 的需求是雪花下落的方向有点倾斜角度,每片雪花的下落速度不一样但要保持在一个范围内。

需求了解的差不多就开始实现这个效果(在看这篇文章之前你需要对 canvas 的一些基本 API 了解)。

drawImage

drawImage 可传入 9 个参数,上图中的 5 个参数是比较常用的,另外几个参数是拿来剪切图片的。

直接使用 drawImage 来剪切图片,其性能不会太好,建议先将需要使用的部分用一个离屏 canvas 保存起来,需要用到的时候直接使用即可。

requestAnimationFrame

requestAnimationFrame 相对于 setinterval 处理动画有以下几个优势:

  1. 经过浏览器优化,动画更流畅

  2. 窗口没激活时,动画将停止,节省计算资源

  3. 更省电,尤其是对移动终端

这个 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 }

前前后后写了一个下午,算是写好了,在手机上查看的效果发现很是卡顿。100 片雪花 FPS 竟然才 40 多。而且在某些机型会出现抖动的情况。

要是产品看到这个效果,恐怕是又要召集相关人员开相关会议了。这么卡顿肯定是写了些开销大的代码,于是乎需要第二次尝试。

晚上还是需要按时下班的。不过下班回家后也不能闲着,开始找相关的资料,以便第二天快速的完成。

第二次尝试前的准备

经过一个晚上的查找学习,大概知道了以下几个优化 canvas 性能的方法:

1. 使用多层画布绘制复杂场景

分层的目的是降低完全不必要的渲染性能开销。

即:将变化频率高、幅度大的部分和变化频率小、幅度小的部分分成两个或两个以上的canvas 对象。也就是说生成多个 canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。



// js 代码

2. 使用 requestAnimationFrame 制作动画

上面有提到。

3. 清除画布尽量使用 clearRect

一般情况下的性能:clearRect > fillRect > canvas.width=canvas.width;

4. 使用离屏绘制进行预渲染

当时用 drawImage 绘制同样的一块区域:

  1. 若数据源(图片、canvas)和 canvas 画板的尺寸相仿,那么性能会比较好;

  2. 若数据源只是大图上的一部分,那么性能就会比较差;因为每一次绘制还包含了裁剪工作。

第二种情况我们就可以先把待绘制的区域裁剪好,保存在一个离屏的 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 &#61; 1;i <40; i&#43;&#43;){cacheCtx.beginPath();cacheCtx.strokeStyle &#61; this.color[i];cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);cacheCtx.stroke();
}
this.cacheCtx.restore();
// 在绘制每一帧的时候&#xff0c;绘制这个图形
context.drawImage(cacheCtx, x, y);

cacheCtx 的宽高尽量设置成实际使用的宽高&#xff0c;否则过多空白区域也会造成性能的损耗。

下图显示了使用离屏绘制进行预渲染技术所带来的性能改善情况&#xff1a;

5. 尽量少调用 canvasAPI &#xff0c;尽可能集中绘制

如下代码&#xff1a;

for (var i &#61; 0; i }

可以改成&#xff1a;

context.beginPath();
for (var i &#61; 0; i }
context.stroke();

tips: 写粒子效果时&#xff0c;可以使用方形替代圆形&#xff0c;因为粒子小&#xff0c;所以方和圆看上去差不多。有人问为什么&#xff1f;很容易理解&#xff0c;画一个圆需要三个步骤&#xff1a;先 beginPath&#xff0c;然后用 arc 画弧&#xff0c;再用 fill。而画方只需要一个 fillRect。当粒子对象达一定数量时性能差距就会显示出来了。

6. 像素级别操作尽量避免浮点运算

进行 canvas 动画绘制时&#xff0c;若坐标是浮点数&#xff0c;可能会出现 CSSSub-pixel 的问题.也就是会自动将浮点数值四舍五入转为整数&#xff0c;在动画的过程中就可能出现抖动的情况&#xff0c;同时也可能让元素的边缘出现抗锯齿失真情况。

虽然 Javascript 提供了一些取整方法&#xff0c;像 Math.floor&#xff0c; Math.ceil&#xff0c; parseInt&#xff0c;但 parseInt这个方法做了一些额外的工作&#xff08;比如检测数据是不是有效的数值、先将参数转换成了字符串等&#xff09;&#xff0c;所以&#xff0c;直接用 parseInt 的话相对来说比较消耗性能。
可以直接用以下巧妙的方法进行取整&#xff1a;

function getInt(num){var rounded;rounded &#61; (0.5 &#43; num) | 0;return rounded;
}

另 for 循环的效率是最高的&#xff0c;感兴趣的可以自行实验。

第二次尝试

通过昨天晚上的查阅&#xff0c;对这个动画做了以下几点优化:

  1. 使用离屏绘制进行预渲染

  2. 减少部分 API 的使用

  3. 浮点数取整

  4. 缓存变量

  5. 使用 for 循环&#xff0c;替代 forEach

  6. 将整体代码使用原型链方式改写了一遍

方案写好了就开始愉快的写代码了。

200 片雪花的时候 FPS 基本稳定在 60&#xff0c;而且抖动的情况也没了&#xff1b;
增加到 1000 片的时候&#xff0c; FPS 还是基本稳定在 60&#xff1b;
增加到 1500 片的时候&#xff0c;稍微有点零星的卡帧&#xff1b;
增加到 2000 片的时候&#xff0c;开始卡顿。

这说明这个动画还是没有优化好&#xff0c;还有优化空间&#xff0c;请各位大佬不吝指教。

推荐使用 stats.js 插件&#xff0c;这个插件可以显示动画运行时的 FPS。

主要代码

let snowBox &#61; function () {let canvasEl &#61; document.getElementById("snowFall");let ctx &#61; canvasEl.getContext( 2d );canvasEl.width &#61; window.innerWidth;canvasEl.height &#61; window.innerHeight;let lineList &#61; []; // 雪的容器let snow &#61; function () {let _this &#61; this;_this.cacheCanvas &#61; document.createElement("canvas");_this.cacheCtx &#61; _this.cacheCanvas.getContext("2d");_this.cacheCanvas.width &#61; 10;_this.cacheCanvas.height &#61; 10;_this.speed &#61; [1, 1.5, 2][Math.floor(Math.random()*3)]; // 雪花下落的三种速度&#xff0c;便于取整_this.posx &#61; Math.round(Math.random() * canvasEl.width); // 雪花x坐标_this.posy &#61; Math.round(Math.random() * canvasEl.height); // 雪花y坐标_this.img &#61; &#96;./img/snow_(${Math.ceil(Math.random() * 9)}).png&#96;; // img_this.w &#61; _this.getInt(5 &#43; Math.random() * 6);_this.h &#61; _this.getInt(5 &#43; Math.random() * 6);_this.cacheSnow();};snow.prototype &#61; {cacheSnow: function () {let _this &#61; this;// _this.cacheCtx.save();let img &#61; new Image(); // 创建img元素img.src &#61; _this.img;_this.cacheCtx.drawImage(img, 0, 0, _this.w, _this.h);// _this.cacheCtx.restore();},fall: function () {let _this &#61; this;if (_this.posy > canvasEl.height &#43; 5) {_this.posy &#61; _this.getInt(0 - _this.h);_this.posx &#61; _this.getInt(canvasEl.width * Math.random());}if (_this.posx > canvasEl.width &#43; 5) {_this.posx &#61; _this.getInt(0 - _this.w);_this.posy &#61; _this.getInt(canvasEl.height * Math.random());}// 如果雪花在可视区域if (_this.posy <&#61; canvasEl.height || _this.posx <&#61; canvasEl.width) {_this.posy &#61; _this.posy &#43; _this.speed;_this.posx &#61; _this.posx &#43; _this.speed * .5;}_this.paint();},paint: function () {ctx.drawImage(this.cacheCanvas, this.posx, this.posy)},getInt: function(num){let rounded;rounded &#61; (0.5 &#43; num) | 0;return rounded;}};let control;control &#61; {start: function (num) {for (let i &#61; 0; i }();
window.onload &#61; function(){snowBox.start(2000)
};

建议从 github clone 代码到本地运行。

后话

这篇文章虽然说是关于 canvas 动画的性能优化。一些大佬也已经看出&#xff0c;其他方面的性能优化方案和这个大抵相同&#xff0c;无非是&#xff1a;

  1. 减少 API 的使用

  2. 使用缓存&#xff08;重点&#xff09;

  3. 合并频繁使用的 API

  4. 避免使用高耗能的 API

  5. 用 webWorker 来处理一些比较耗时的计算

  6. ……

希望通过阅读这篇文章&#xff0c;可以在性能优化方面给你作一个参考。


推荐阅读
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • 树莓派语音控制的配置方法和步骤
    本文介绍了在树莓派上实现语音控制的配置方法和步骤。首先感谢博主Eoman的帮助,文章参考了他的内容。树莓派的配置需要通过sudo raspi-config进行,然后使用Eoman的控制方法,即安装wiringPi库并编写控制引脚的脚本。具体的安装步骤和脚本编写方法在文章中详细介绍。 ... [详细]
  • Commit1ced2a7433ea8937a1b260ea65d708f32ca7c95eintroduceda+Clonetraitboundtom ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • PHP反射API的功能和用途详解
    本文详细介绍了PHP反射API的功能和用途,包括动态获取信息和调用对象方法的功能,以及自动加载插件、生成文档、扩充PHP语言等用途。通过反射API,可以获取类的元数据,创建类的实例,调用方法,传递参数,动态调用类的静态方法等。PHP反射API是一种内建的OOP技术扩展,通过使用Reflection、ReflectionClass和ReflectionMethod等类,可以帮助我们分析其他类、接口、方法、属性和扩展。 ... [详细]
  • 1.利用node实现页面实时更新,主要 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Matlab 中的一些小技巧(2)
    1.Ctrl+D打开子程序  在MATLAB的Editor中,将输入光标放到一个子程序名称中间,然后按Ctrl+D可以打开该子函数的m文件。当然这个子程序要在路径列表中(或在当前工作路径中)。实际上 ... [详细]
  • 开发笔记:读《分布式一致性原理》JAVA客户端API操作2
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了读《分布式一致性原理》JAVA客户端API操作2相关的知识,希望对你有一定的参考价值。创 ... [详细]
  • Arduino + ESP32C3 + TFT(1.8‘ ST7735S)基础平台(实验四)直接显示网络图片
    ------------------------------------------------------------------------------------------ ... [详细]
  • 嵌套函数定义时先判断function_exists防止递归调用外部函数导致两次定义内部函数导致致命错误看一下PHP手册中是如何说的: ... [详细]
  • 【图像边缘检测】基于matlab GUI Sobel+Prewitt+Robert算子图像边缘检测【含Matlab源码 203期】
    一、获取代码方式获取代码方式1:完整代码已上传我的资源:【图像边缘检测】基于matlabGUISobelPrewittRobert算子图像边缘检测【含 ... [详细]
author-avatar
书友73892718
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有