先看看我们要做的效果

一、canvas动画核心概念

完全没有canvas基础的同学建议先刷一下[Canvas的基本用法 - Web API 接口参考 | MDN]

重点是理解canvas动画的基本步骤,在[基本的动画 - MDN]中,动画分为4步走

初学者可以再简单一些,我们先不管状态保存,直接两步走:

  • 清空canvas
  • 绘制新的一帧动画

用定时器或者window.requestAnimationFrame定时重复以上两步即可

二、抢金币核心原理

想象一下整个业务场景,我们先梳理出3个要解决的核心问题:

  • 1、生成红包,这里有两种解决方案

    • 统一生成所有的红包对象,从上到下分布在y轴,触发运动后后整体向下运动
    • 在屏幕上方持续生成新红包对象,红包一旦生成,立刻开始运动(本次选择此方案)
  • 2、运动,canvas动画原理
  • 3、用户点击红包,计算是否点中红包(事件只能绑定在canvas这一层,需要根据点击位置进行计算)

三、核心功能

  • 1、预缓存图片/离屏canvas
  • 2、canvas绘制多图,改变每一帧形成动画
  • 3、判断点击位置,冒泡+1效果

下面都是基于vue的代码,不能直接跑的,主要用于理解核心功能

最好是自己理解核心原理后亲自动手做个最简单的demo,有助于加深理解

1、预缓存图片/离屏canvas

页面上感觉有很多很多金币在按各种角度掉落

其实页面上一共就4种金币图片,只是他们的大小、速度不一样,看起来有每一个都不一样

我们可以先把这4张图片全都加载好

 // 缓存几种金币图片为DOM元素,避免canvas绘制时还需要异步读取图片
loadImgs(arr) {return new Promise(resolve => {let count = 0;// 循环图片数组,每张图片都生成一个新的图片对象const len = arr.length;for (let i = 0; i < len; i++) {// 创建图片对象const image = new Image();// 成功的异步回调image.onload = () => {count++;arr.splice(i, 1, {// 加载完的图片对象都缓存在这里了,canvas可以直接绘制img: image,// 这里可以直接生成并缓存离屏canvas,用于优化性能,但本次不用,只是举个例子offScreenCanvas: this.createOffScreenCanvas(image)});// 这里说明 整个图片数组arr里面的图片全都加载好了if (count == len) {this.preloaded = true;resolve();}};image.src = arr[i].img;}});
},
复制代码

创建离屏canvas的方法如下

createOffScreenCanvas(image) {const offscreenCanvas = document.createElement("canvas");const offscreenContext = offscreenCanvas.getContext("2d");// 这里可以是动态宽高offscreenContext.width = 30;offscreenContext.height = 30;offscreenContext.drawImage(image,0,0,offscreenContext.width,offscreenContext.height);// return这个offscreenCanvasreturn offscreenCanvas;
},复制代码

2、canvas绘制多图,改变每一帧形成动画

首先初始化canvas

这里我们直接把canvas的上下文ctx存在data里面,方便在各个方法里面读取。

在vue里面写不像单独的一个JS模块,可以用闭包来封装一个独立的上下文,而在vue里面也不建议声明全局变量

initCanvas() {const canvas = document.getElementById("canvas");if (canvas.getContext) {this.ctx = canvas.getContext("2d");// 初始化时同步进行图片预加载this.loadImgs(this.imgArr);}
},复制代码

绘制多图,其实就是循环遍历上面创建好的图片数组imgArr,然后对于每个图片对象,都调用this.ctx.drawImage()方法即可

下面我们把图片转变化金币对象

把图片数组imgArr替换成金币对象数组coinArr,这个数组是由一个个的金币对象Coin组成,金币对象自身除了有图片,还有大小、物理位置、下落速度等参数,也就是说,每个金币对象缓存自己的所有绘制信息,这里用的是面向对象的思维

const Coin = {x: 'x轴位置',y: 'y轴位置', // 运动的关键是在每一帧都改变yradius: '金币大小',img: '前面缓存好的金币图片',speed: '金币的下落速度'
};复制代码

每一帧,循环这个金币数组,然后绘制出所有的金币对象

如果要运动起来,每一帧让每个金币的y轴位置往下掉一点,就是这句y: coin.y + coin.speed

那么绘制下一帧时,其他信息都不变,每个金币都往下移动了一点点,连贯起来,这不同的一帧一帧组合起来就成了运动的动画了

先看绘制的代码

drawCoins() {// 遍历这个金币对象数组this.coinArr.forEach((coin, index) => {const newCoin = {x: coin.x,// 运动的关键  每次只有y不一样y: coin.y + coin.speed,radius: coin.radius,img: coin.img,speed: coin.speed};// 绘制某个金币对象时,也同时生成一个新的金币对象,替换掉原来的它,唯一的区别就是它的y变了,下一帧绘制这个金币时,就运动了一点点距离this.coinArr.splice(index, 1, newCoin);this.ctx.drawImage(coin.img,coin.x,coin.y,coin.radius,coin.radius * 1.5);});
},复制代码

那么怎么连贯运动起来呢,不断的执行this.drawCoins()方法即可

既然做动画,我们肯定得知道【window.requestAnimationFrame】这个api

还记得刚开始说的动画核心两步走吗

  • 清空canvas
  • 绘制新的一帧动画
moveCoins() {// 清空canvasthis.ctx.clearRect(0, 0, this.innerWidth, this.innerHeight);// 绘制新的一帧动画this.drawCoins();// 不断执行绘制,形成动画this.moveCoinAnimation = window.requestAnimationFrame(this.moveCoins);
},复制代码

到这里,我们其实已经能让金币运动起来了,不过我们要做的是让很多很多金币不断的往下掉,所以我们选择在运动的过程中,不断生成新的金币对象,然后push到this.coinArr

pushCoins() {// 每次随机生成1~3个金币const random = this.randomRound(3, 6);let arr = [];for (let i = 0; i < random; i++) {// 创建新的金币对象const newCoin = {x: this.random(this.calculatePos(10),this.innerWidth - this.calculatePos(150)), // 横向随机  金币不要贴近边边y: 0 - this.calculatePos(Math.random() * 150), // -150内高度 随机radius: this.calculatePos(120 + Math.random() * 30), // 100宽  大小浮动15img: this.coinObjs[this.randomRound(0, 3)].img, // 随机取一个金币图片对象,这几个图片对象在页面初始化时就已经缓存好了speed: this.calculatePos(Math.random() * 7 + 5) // 下落速度 随机};arr.push(newCoin);}// 每次都插入一批新金币对象arr到运动的金币数组this.coinArrthis.coinArr = [...this.coinArr, ...arr];// 间隔多久生成一批金币this.addCoinsTimer = setTimeout(() => {this.pushCoins();}, 600);
},复制代码

因为每个金币的初始y的位置都是屏幕上方,所以看起来都是不断生成金币然后往下掉的

至于计算大小的方法,这个比较随意了

最后,把上面的汇总起来,开启动画的方法是这样的

start() {this.pushCoins(); // 不断增加金币this.moveCoins(); // 金币开始运动// 开始10秒倒计时this.runCountdownTimer = setInterval(() => {//...倒计时10s后,做一些停止动画的工作}, 1000);
},复制代码

到这里,运动过程就已经结束了,先总结一下上面的内容

  • 1、初始化canvas
  • 2、缓存金币图片,生成金币对象,每个金币对象包含自身信息
  • 3、不断生成金币对象,并增加到要遍历运动的数组this.coinArr 
  • 4、通过window.requestAnimationFrame,每一帧都用canvas重新遍历绘制this.coinArr,每一帧都改变this.coinArr里面的每一个对象的y值大小,形成运动感

3、判断点击位置,冒泡+1效果

通过上面的效果图,我们可以看到,点击金币时,对应的这个金币会消失(如果有重叠,只会消失最上面的那个金币),而且还会有个+1的效果,并缓慢上移消失

先思考一下逻辑

  • 1、绑定点击事件
  • 2、计算位置,遍历当前整个金币数组,看看点击在哪个金币上,找出最上面那个,然后删除这个金币对象
  • 3、在点击位置上,绘制一个+1效果

首先,canvas本身就是一个DOM对象,绘制在它上面的金币并不是dom对象,无法绑定点击事件,所以只能绑定在canvas上面,通过event拿到点击位置,有点事件代理的味道吧

    listenClick() {      const canvas = document.getElementById("canvas");      canvas.addEventListener("click", e => {        const pos = {          x: e.clientX,          y: e.clientY        };      });    },复制代码

既然拿到此刻的点击位置,而当前的金币数组this.coinArr也知道,数组里面的每个金币对象都维护了自身的信息,其中就包括了位置和金币大小

那么,只要遍历一下,如果点击位置在这个金币的大小范围之内,那么是不是可以认为点击中了这个金币?

// 判断点击位置  是否处于某个coin之中
isIntersect(point, coin) {const distanceX = point.x - coin.x;const distanceY = point.y - coin.y;const withinX = distanceX > 0 && distanceX < coin.radius;// 金币图片是长方形的 我们只计算下半部的正方形  不计算金币尾巴const withinY =distanceY > 0 &&distanceY > coin.radius * 0.5 &&distanceY < coin.radius * 1.5;return withinX && withinY;
},复制代码

但,同一时刻,有可能点中了很多个重叠的金币,那么我们遍历时,把这几个金币都拿出来,只要最上面那个就好了

listenClick() {const canvas = document.getElementById("canvas");canvas.addEventListener("click", e => {// 点击位置const pos = {x: e.clientX,y: e.clientY};// 所有点中的金币都存这const clickedCoins = [];this.coinArr.forEach((coin, index) => {// 判断点击位置是否在该金币范围内if (this.isIntersect(pos, coin)) {clickedCoins.push({x: e.clientX,y: e.clientY,// 索引很重要,用于删除this.coinArr内的该金币index: index});}});// 如果点击中了重叠的金币,只取第一个即可  也只删除第一个金币  count也只增加一次if (clickedCoins.length > 0) {this.count += 1;const bubble = {x: clickedCoins[0].x,y: clickedCoins[0].y,opacity: 1};// 这跟生成+1冒泡效果相关,下面马上讲this.bubbleArr.push(bubble);// 移除被点中的第一个金币对象this.coinArr.splice(clickedCoins[0].index, 1);}});
},复制代码

既然拿到了此刻的位置,在当前位置绘制一个冒泡效果应该不是难事,只要处理好冒泡的移动和消失即可,本质上就跟上面绘制金币是一样的

  • 1、存一个this.bubbleArr数组,动画中循环遍历绘制它里面的对象bubble
  • 2、bubble有位置信息,加多一个透明度opacity,运动的过程中,不断减小透明度,直到变为0,就把这个bubble从数组上删除即可
drawBubble() {this.bubbleArr.forEach((ele, index) => {if (ele.opacity > 0) {// 透明度渐变this.ctxBubble.globalAlpha = ele.opacity;this.ctxBubble.drawImage(this.bubbleImage,ele.x,ele.y,this.calculatePos(60),this.calculatePos(60));// 更新:每次画完就减少0.02透明度,同时位置移动const newEle = {x: ele.x + this.calculatePos(1),y: ele.y - this.calculatePos(2),opacity: ele.opacity - 0.02};this.bubbleArr.splice(index, 1, newEle);}});
},
keepDrawBubble() {this.ctxBubble.clearRect(0, 0, this.innerWidth, this.innerHeight);// 把opacity为0的全部清除this.bubbleArr.forEach((ele, index) => {if (ele.opacity < 0) {this.bubbleArr.splice(index, 1);}});this.drawBubble();this.bubbleAnimation = window.requestAnimationFrame(this.keepDrawBubble);
},复制代码

四、性能测试

到这里,整个运动的核心原理就讲完了,我们测试一下动画的性能

在chrome的性能测试里面可以看到,整个运动过程的fps稳稳保持在60帧每秒,可以说是性能很不错了

后话

感谢您耐心看到这里,希望有所收获!

我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步,更多文章请看【amandakelake的Github博客】

canvas+vue实现60帧FPS的抢金币动画(类天猫红包雨)相关推荐

  1. 5e服务器显示fps被锁定,csgo强制被锁60帧 被锁60fps解决方法

    帧数是csgo中十分重要的参数,锁定好帧数后游戏画面会变得更加流畅.近期有玩家打开游戏发现csgo强制被锁60帧,那么被锁60fps解决方法是什么?一起来看看吧! csgo强制被锁60帧 被锁60fp ...

  2. win10计算机跑分,Win10使用鲁大师对显卡跑分测试时出现FPS锁在60帧如何解决

    鲁大师是一款免费系统工具,可以帮助我们对电脑的性能进行跑分测试,然而近日有win10系统用户在用鲁大师对显卡进行跑分测试时,却出现FPS锁在60帧的情况,导致显卡跑分很低,很多用户不知道这是怎么回事, ...

  3. 为什么游戏帧数要到 60 帧每秒才流畅,而电影帧数只有24FPS?

    首先要说的是电影就是24FPS也不如60FPS的流畅,对比就可以看出来,但是24FPS不会让人觉得卡,甚至12FPS都不会让人觉得卡,而24FPS的游戏能让人很明显的感受到卡,12FPS就是幻灯片了, ...

  4. ios屏幕录制60帧_探索iOS屏幕帧缓冲区–内核反转实验

    ios屏幕录制60帧 It's been over two years since I last published a blog, so I thought I'd give this anothe ...

  5. 视频录制30帧还是60帧清晰?

    您在日常生活中可能已经注意到,有些视频的质量很好,有些则很差.视频质量取决于帧率吗?帧率是多少才更清晰呢? 答案:更高的帧率并不意味着更高的视频质量.但是,使用高帧率的摄像机可以获得更流畅的视频.没有 ...

  6. 60帧/秒摄像头 视频帧数最佳选择!

    随着网络的普及,作为电脑外设产品的摄像头也迅速进入千家万户.这一重大商机也给摄像头行业的发展带来一片繁荣景象.在这个进入门槛低.公模横行的行业,摄像头产品在外观设计.用户应用范围.新功能技术指标等方面 ...

  7. 流畅度游戏60帧,视频24帧的原因

    流畅度游戏60帧,视频24帧的原因 虽然电影24FPS也不如60FPS的流畅,但是24FPS不会让人觉得卡,甚至12FPS都不会让人觉得卡,而24FPS的游戏能让人很明显的感受到卡,12FPS就是幻灯 ...

  8. 海盗王3.0版本60帧版的体验

    默认情况下,海盗的版本都是设置了30帧的FPS. 之前有人改过1.38版本的60帧,刚开始看到所有的动作都想快进一样,很不协调,或者走路就像太空漫步一样. 后来无意中修复了一个残缺的3.0端,发现它的 ...

  9. 为什么游戏帧数一般要到 60 帧每秒才流畅,而过去的大部分电影帧数只有 24 帧每秒却没有不流畅感?

    作者:蔡小帅 链接:https://www.zhihu.com/question/21081976/answer/34748080 虽然电影24FPS也不如60FPS的流畅,但是24FPS不会让人觉得 ...

  10. 平滑动画 每秒60帧 -- 16ms内绘完一帧

    大多数Android显示系统是以每秒钟60帧的频率工作的(专业点说,叫60Hz).为获得更平滑的动画,就必须具有每秒钟处理60帧的能力--意味着每帧只能花费16毫秒的时间.如果这个过程超过16毫秒,动 ...

最新文章

  1. 对称加密、工作模式和填充模式
  2. 8.使用hydra对端口进行爆破
  3. python mro c3_python的MRO和C3算法
  4. Frame和Iframe横向滚动条的解决方案
  5. zigbee上位机通过vs2019的mfc实现
  6. python+unittest框架整理(一点点学习前辈们的封装思路,一点点成长。。。)
  7. 原生node创建路由的分层
  8. 超分辨率分析(四)--Deep Image Prior
  9. Windows 7安装.net framework 4 安装
  10. cimoc 最新版_Cimoc1.49版下载
  11. Linux抢购脚本,在操作系统中设置定时自动执行抢飞天茅台脚本的方法
  12. 阿里云物联网平台如何通过云产品流转使两个设备互相通信
  13. 含有使字的诗句_带有一字的诗句
  14. 老树开新花,慧聪尚能饭否?
  15. 华为手机鸿蒙切换主页,京东APP可一键切换“华为鸿蒙版界面”:简洁多了
  16. 2017年2月16日-----------乱码新手自学.net 之MVC模型
  17. 3D程序设计离不开各种坐标系统
  18. 增程式电动汽车建模与仿真(一)
  19. 矢量数据向栅格数据的转换算法
  20. 董明珠“接班人”孟羽童被解雇?因直播带货能力差 本人回应了...

热门文章

  1. pdf转word:扫描全能王 vs WPS(会员功能)对比,过程记录和反思
  2. STM32F401CCU6移植华为LiteOS
  3. 计算机课数据排序与筛选ppt,《EXCEL 数据排序与筛选》教学设计
  4. pytorch 计算模型的GFlops和total params的方法
  5. [渝粤教育] 四川大学 土木工程概论 参考 资料
  6. setw()函数使用
  7. 关于网站中Logo部分的写法
  8. 定量数据和定性数据_定性数据:赋予大数据意义的上下文
  9. 怎么用计算机名称共享打印机设置,如何共享打印机设置教程
  10. 救命稻草VirtualBox,失之交臂VMware—— 2者的guest OS对 恒通笔记本并口卡的支持