示例代码托管于:http://www.github.com/dashnowords/blogs博客园地址:《大史住在大前端》原博文目录华为云社区地址:【你want前端打怪升级指南】[TOC]经过前几章相对枯燥的练习,相信大家已经可以上手canvas原生API了,那么从这一节开始,我们就开始接触一些有趣的东西——动画片。1.canvas的能力如果你认为canvas只能画图表,那就真的很烂,更不用说webgl的绘图上下文了,光是2d空间的画笔就可以做很多有趣的事情,比如实现一些炫酷的动画效果,比如做一些物理模拟,图片滤镜,直播弹幕,甚至做游戏开发等等,大部分的画面变化都依赖于canvas提供的像素操作能力,短时间内几乎所有的动画效果都依赖于canvas.它是逐帧绘制而成,胶片的原理是一样的。我们知道javascript中与时间控制相关的函数setTimeout()和setInterval()在最终执行时是不准确的,因为它们会受到事件队列中其他异步任务的影响甚至直接阻塞,所以在重复绘制,另一方面,假设我们使用的电脑屏幕的刷新率是每秒60帧,也就是大约16.7ms重绘一次,那么即使我们在16.7ms内执行了很多次的计算和绘图命令,其实最后的结果只是最后的结果,就像每隔一段时间采样一段非常密集的数据,至少可能会浪费性能,或者在屏幕呈现时跳帧。为了配合显示器的刷新,我们可以使用另外一个方法——requestAnimationFrame(fn),它是javascript中专门用来绘制逐帧动画的,它会根据显示器的刷新率进行必要的图像更新,节省不必要的性能浪费。2.动画框架在canvas上实现基本的动画,可以遵循一个基本的编程框架:functionstep(){/***每一帧要执行的逻辑*......*/requestAnimationFrame(step);}step();//开始执行你没看错,这是canvas动画的核心代码,step()函数会在每个绘制周期重复执行。那么每一帧都需要做哪些工作呢?我们把画布想象成一个舞台。每个需要在画布上绘制的元素都称为精灵。不管它们有什么属性,它们都有两个基本方法:update()和paint()。一帧计算更新用于在画布上绘制此精灵对象的精灵的参数属性。然后step函数在每一帧中执行的逻辑就变得清晰起来,必要时擦除画布,然后更新每个精灵的状态(可能是位置,颜色等),然后绘制到画布上。比如你想在画布上显示一个太阳东升西落的动画,对应的伪代码如下:letstage=[];stage.push(background,tree,cloud,sun);functionstep(){cleanStage();//根据需要擦除画布background.update();//更新土地的属性tree.update();//更新树的属性cloud.update();//更新云的属性sun.update();//更新太阳的属性(属性必须包含太阳的位置数据)background.paint();//绘制陆地tree.paint();//画树cloud.paint();//画云太阳.paint();//画太阳requestAnimationFrame(step);}如果你理解了上面的过程,那我们就抽象重写上面代码://创建舞台和添加元素的代码letstage=[];stage.push(background,tree,cloud,sun....);//逐帧动画代码functionstep(){清洁阶段();stage.map(sprite=>{sprite,update();sprite.paint(ctx);});requestAnimationFrame(step);}每个sprite对象都需要实现自己的update()和paint()方法来描述它的参数如何变化,以及如何在每一帧中绘制和添加所有进入stage数组的item都是sprite的实例.一般会将canvas的绘制上下文传递给paint(context)方法,这样精灵就可以绘制到指定的canvas上了。以上范式只是一个粗略的核心模型,但足以说明canvas动画的本质。3.在canvas中模拟碰撞下面我们将通过一个碰撞模拟的例子来学习canvas动画和基本的物理模拟分析。示例虽然简化了,但包含了画布动画的核心精灵动画和碰撞检测主题。为了方便二维向量的运算,隐藏各种数学计算的细节,我们直接使用了一个定义好的Vector2类,里面封装了很多向量的基本运算,这些都是初中数学知识,如果你还没有记清楚了,可以找一些相关的资料复习一下。3.1定义小球的属性把每一个小球当作一个精灵,我们需要给它添加一些基本的属性,这样它才能在每一帧中被绘制出来。通过位置、半径和颜色信息,可以画出小球;通过速度信息,可以计算出球的位置变化,用于下一帧的绘制。classBall{constructor(x,y,id){this.pos=newVector2(x,y);//初始化球的位置this.id=id;this.color='';//绘制颜色this.r=20;//球的半径,为了演示方便,这里使用给定值this.velocity=null;//球的速度ball}}3.2生成一个新的球为了增加演示效果,我们使用了一个计时函数来随机生成小球,每次生成时给它们一个颜色,并给它们一个随机的初始速度。//向全局balls数组中添加一个新球,初始位置为(50,30),functionaddBall(){letball=newBall(50,30,balls.length);ball.color=colorPalette[parseInt(steps/100,10)%10];ball.velocity=newVector2(5*Math.random(),5*Math.random());balls.push(ball);}为了方便我们使用一个全局的自增数值变量根据step中的条件执行addBall()方法:if(steps%100===0&&steps<1500){addBall();}每100次step循环(约1.5秒)会多生成一个随机方向发射的球,球的数量不能超过15个。3.3Frame动画绘制函数stepstep函数是动画的核心,在这里我们需要完成重新绘制背景、添加小球、更新每个小球、绘制小球的逻辑(因为背景是静态的,所以不在示例中抽象为动画精灵)。函数步骤(){步骤++;//重绘背景paintBg();//每隔一定时间添加一个小球if(steps%100===0&&steps<1500){addBall();}//更新每个小球的状态balls=balls.map((ball,index,originArr)=>{ball.update(index,originArr);ball.paint();//画线不画在画布回球上;});//绘制每个小球的位置requestAnimationFrame(step);}3.4定义小球的update方法sprite的paint方法一般只涉及canvas的基本绘制API,并不复杂。比如本例中,只需要在小球的pos属性记录的位置画一个闭合的圆弧填充即可。精灵的update()方法通常是最难写的部分。在该方法中,需要完成的基本逻辑包括状态更新和碰撞检测。状态更新状态更新一般包括自身状态更新和相对状态更新。自身状态的更新,比如你想让小球在运动过程中颜色发生变化,就属于自身状态的改变。相对状态变化一般是指小球相对于公共坐标系或参照物的宏观位置变化,如本例中小球的位置变化。碰撞检测碰撞检测一般包括精灵是否与其他精灵发生碰撞,需要模拟碰撞造成的影响。参考代码:/*Updatestatus由于检测碰撞需要知道其他球的位置,所以这里球数组的引用也可以直接用面向对象的方式定义*/update(index,balls){letnextPos;//模拟下一个着陆点//1.计算下一个落点nextPos=this.pos.add(this.velocity.multiply(dt));//2。判断新的位置是否碰到边界,如果是,则边界正常速度反转,假设碰撞过程没有能量损失if(nextPos.x+this.r>rightBorder||nextPos.x
