canvas实现绚丽的倒计时效果与动画基础(二)
我们要实现的这个 demo 具有动画的效果,所以接下来将会给大家介绍 canvas 制作动画的基础内容,推荐大家先阅读《canvas 实现绚丽的倒计时效果与动画基础(一)》
实现动画的基础函数
实现动画的最简单的架构就是使用 setInterval 这个方法,关于 setInterval 方法推荐大家去阅读《定时器 setTimeout 和 setInterval 的工作原理以及优缺点》。
setInterval( function () { render(); update(); }, 50 )
setInterval 方法里面有两个参数,第一个参数传入的是匿名函数,表示在每一帧的时候要做的事情;第二个参数传入一个时间,单位是毫秒,表示每隔多长时间执行一次这个匿名函数。那么大家就可以想象使用 setInterval 就可以构建出一个逐帧的动画,其中的帧率有这个时间控制,我这里写的 50 毫秒,那么一秒钟有 1000 个毫秒,所以这样一个动画帧率就是 20。但是事实上这个计算并不是准确的,这是因为这个匿名函数里面执行的内容效率不同不见得能够在 20 毫秒里完成所有动画的绘制内容,那么这个也是绘画领域内更高级的话题,在以后的课程中有机会的话会向大家介绍更加精确的动画绘制方法,本教程我们使用 setInterval 方法就好了。
那么,对于这个每一帧执行的函数通常而言要干这么两件事情,第一件事情是绘制当前画面;第二件是根据绘制画面所需要的数据结构对数据结构进行一定的调整。那么我们在这个倒计时的例子中如何使用这个 setInterval 方法呢
在之前的代码中我们用的是 render(context)方法,我们使用 setInterval 改写,在这里我使用了一个新的函数 update()对当前数据进行一个调整,下面重点就是放在如何实现这个 update 函数。
其实大家只要再仔细分析一下我们这个 demo 就会发现我们这个动画比较复杂,主要包括两部分的变化,第一部分是时间在逐渐变化,第二部分是时间的变化产生了许多彩色的小球,这些小球产生了物理运动,这样的一个动画效果。
接下来这一小节我们先处理一下最简单时间的变化,大家分析一下可能会发现时间的变化很简单,假如我们不用 update 处理,再接在绘制 render 的时候取得当前的时间,然后进行绘制就好了,大家这样想也是完全正确的。但是为了这个结构我们还是把时间的处理放到 update 中,同时也是为我们下一步随着时间的变化产生小球动画做一个准备,在这里我的主要思路是这样的,首先,我声明一个新的 nextShowTimeSeconds 变量,这里要注意在 render 里头是绘制 curShowTimeSeconds 这个变量所表示的时间,那么在 update 里面看一下下次显示的时间是多少,具体的还是使用我们之前定义的 getCurrentShowTimeSeconds()来获取秒数,之后要看一下下次显示的时间和当前显示的时间有没有变化,一旦有变化的话我们就改变 curShowTimeSeconds,具体还要对 nextShowTimeSeconds 秒数进行时、分、秒的分解。这个产生这种分解不仅有助于产生时间变化的动画,同时后续每一个时间点的变化以后相应的时间点产生这种小球的变化也需要这步分解操作。
接下来我们分别获取 nextHours、nextMinutes、nextSeconds,同时根据 curShowTimeSeconds 获取到 curHours、curMinutes、curSeconds,这样我们就可以比较两个时间是不是一样了,具体比较 nextShowTimeSeconds 和 curShowTimeSeconds 是否一样,其实只需要比较秒数就可以了,所以我们看一下
nextSeconds 是否已经不等于 curSeconds,如果已经不等于了,那么我们当前的
curShowTimeSeconds 就需要变化,变成我们刚刚算出来的 nextShowTimeSeconds:
function update(){ var nextShowTimeSeconds = getCurrentShowTimeSeconds(); var nextHours = parseInt( nextShowTimeSeconds / 3600); var nextMinutes = parseInt( (nextShowTimeSeconds - nextHours * 3600)/60 ) var nextSeconds = nextShowTimeSeconds % 60 var curHours = parseInt( curShowTimeSeconds / 3600); var curMinutes = parseInt( (curShowTimeSeconds - curHours * 3600)/60 ) var curSeconds = curShowTimeSeconds % 60 if( nextSeconds != curSeconds ){ curShowTimeSeconds = nextShowTimeSeconds; } }
这里大家可以想象,一旦这个变化产生了,我在这个 setInterval 里再次执行 render 函数的时候,在 render 函数里会会自动对
curShowTimeSeconds 进行一个分解,从而显示新的时间。这里大家还需要注意一点,由于我们产生了动画,那么在 canvas 中进行逐帧动画,每帧都需要把改变的对象进行一次刷新,否则的话,之前的一副图像和新的图像就会叠加在一起,为此我们介绍一个新的函数,这个函数叫 clearRect,clearRect 函数对一个矩形空间内图形进行一个刷新操作,在这里我们对整个屏幕矩形进行刷新:
cxt.clearRect(0,0,WINDOW_WIDTH, WINDOW_HEIGHT);
render 函数完整代码:
function render( cxt ){ cxt.clearRect(0,0,WINDOW_WIDTH, WINDOW_HEIGHT); var hours = parseInt( curShowTimeSeconds / 3600); var minutes = parseInt( (curShowTimeSeconds - hours * 3600)/60 ) var seconds = curShowTimeSeconds % 60 renderDigit( MARGIN_LEFT , MARGIN_TOP , parseInt(hours/10) , cxt ) renderDigit( MARGIN_LEFT + 15*(RADIUS+1) , MARGIN_TOP , parseInt(hours%10) , cxt ) renderDigit( MARGIN_LEFT + 30*(RADIUS + 1) , MARGIN_TOP , 10 , cxt ) renderDigit( MARGIN_LEFT + 39*(RADIUS+1) , MARGIN_TOP , parseInt(minutes/10) , cxt); renderDigit( MARGIN_LEFT + 54*(RADIUS+1) , MARGIN_TOP , parseInt(minutes%10) , cxt); renderDigit( MARGIN_LEFT + 69*(RADIUS+1) , MARGIN_TOP , 10 , cxt); renderDigit( MARGIN_LEFT + 78*(RADIUS+1) , MARGIN_TOP , parseInt(seconds/10) , cxt); renderDigit( MARGIN_LEFT + 93*(RADIUS+1) , MARGIN_TOP , parseInt(seconds%10) , cxt); }
这样我们对时间变化的动画就做好了,此时看一下效果:
由上图我们看到此时这个时间动起来了,一秒一秒逼近我们之前设置的 endTime 这个倒计时的终点,是不是非常酷。
使用 canvas 做个物理实验
在为我们 demo 加上滚动的小球动画前,我们先做一个简单的实验,来看一下我们怎么通过 canvas 实现小球运动学运动效果,这里我们暂时先处理一个小球动画,为此我声明一个小球类对象:
var ball = {x:512, y:100, r:20, g:2, vx:-4, vy:0, color:"#005588"}
来看一下这个小球包含哪些数据,首先是坐标位置我设置为 x 轴 512,y 轴 100,其次是小球半径为 20,之后的 g、vx、vy 适合运动学相关的参数,我定义加速度 g 为 2,其次速度 vx 为-4,还有一个速度 vy 为 0,也就是 x 和 Y 两个方向的速度,这里的速度我们可以用向量的方式表示,也就是可以用复数表示,之后呢就是小球的颜色。
我们看一下 render 函数:
function render(cxt) { cxt.clearRect(0,0,cxt.canvas.width,cxt.canvas.height); cxt.fillStyle = ball.color; cxt.beginPath(); cst.arc(ball.x, ball.y, ball.r, 2*Math.PI); cxt.closePath(); cxt.fill(); }
上面的 render 函数没有什么稀奇的,我们用 clearRect 将整个屏幕进行 clear,这里我是用了 context 的新的一个属性,通过上下文的绘图环境 cxt 可以以调用 canvas 方法来找到这个上下文绘图环境属于哪一块画布(cxt.canvas),之后调用 width 和 height 来获得画布的宽高(cxt.canvas.width,cxt.canvas.height),剩下的代码就是对一个小球的绘制。
那么,真正实现运动学效果的关键在于 update 部分:
function update() { ball.x += ball.vx;//小球的 x 坐标加上它的 x 轴速度值 ball.y += ball.vy;//小球的 y 坐标加上它的 y 轴速度值 ball.vy += ball.g;//小球在 y 轴的速度值加上重力加速度 g }
这个 update 的部分就是对小球位置的改变,这段代码非常简单,ball.x 表示小球的 x 坐标加上它的 x 轴速度值 ball.vx,ball.y 表示小球的 y 坐标加上它的 y 轴速度值 ball.y,之后由于重力加速度的影响,这个重力加速度只影响在 y 轴方向的速度,所以我们对小球在 y 轴的速度值又加上了它的重力加速度 g,此时我们看一下效果:
通过这个简单的例子相信大家了解了,所谓的在 canvas 实现小球运动学效果,只不过是之前我们已经学过的物理公式,把小球的位置一帧一帧的计算出来。对于这个代码相信大家可以想到很多可以实验的地方,改变(g:2, vx:-4, vy:0,)这三个参数就可以产生不同的效果,比如想让小球落得慢点我们就可以把重力加速度 g 值改的小点,再比如说刚才我们让小球以左边抛物线运动,那么我们想让它以右边抛物线运动呢,vx 就应该为正的。如果想让左边运动更大一些呢,vx 值就相应的增大。再有刚才我们的小球在 Y 方向没有一个上抛的过程,我在这里给大家简单的实验一下,如果我把 vy 值改成“-10”的话,它就会有一个上抛的过程,如下图:
对于这个 demo 大家可以根据自己的喜好在实验一下不同参数会产生的不同效果。
现在我们来考虑另外一个问题,刚才我们的小球落下的时候就消失在画布的外面了,假如我们画布下面是一个地板的话,小球落下来会进行一个反弹,怎么做呢?这就是一个最简单的碰撞检测的过程。在这里我们可以想象,我们可以对小球的位置做一个判断,如果此时我们发现对小球 y 坐标的位置已经比当前的屏幕的高度就是“768”减去小球的半径还要大了,那么说明此时小球的底部已经触碰到屏幕的边缘了,此时小球的位置就应该就应该发生改变,那么在第一秒的时候小球的位置应该就等于这个 768 减去小球的半径(768-ball.r),也就是说它先着地,然后最关键的是小球的速度也应该发生转变,此时小球的速度 ball.vy 等于它原来速度相反的值。
function update() { ball.x += ball.vx; ball.y += ball.vy; ball.vy += ball.g; if(ball.y >= 768-ball.r){ ball.y = 768-ball.r; ball.vy = -ball.vy; } }
我们看一下效果:
可以看到小球落到地下就反弹了起来,刚才的代码只是对屏幕的下边缘进行了判断,那么大家学会了这个思路就可以试着对屏幕的上边缘、左边缘、右边缘都进行一个碰撞判断,有兴趣的同学可以下去尝试一下。
接下来我们要解决另外一个问题了,从上图可以看到小球每次反弹可以达到小球所能达到的最高位置,这不符合我们的要求,查一下代码可以发现问题出在“ball.vy = -ball.vy;”,(ball.vy)反弹小球的速度是没有损耗的,这个呢是不符合我们真实地物理现象的,这里我给它加一个系数即-ball.vy*0.5,意思就是一次反弹小球的速度损失 0.5 速度,此时小球的反弹效果就会更逼真了,一起看一下效果:
是不是非常的酷,那么大家掌握了这些知识就可以继续做我们的绚丽的倒计时效果了。
华丽的小球滚动效果
首先我们要存储这些生成的小球,我声明一个 balls 的数组,初始化是一个空的数组
var balls = [];
之后我们一旦产生新的小球直接添加到这个数组里就可以了,另外我们这些小球呢都是彩色的,为此我有设置了一个 colors 数组:
const colors = ["#33B5E5","#0099CC","#AA66CC","#9933CC","#99CC00","#669900","#FFBB33","#FF8800","#FF4444","#CC0000"]
在这里我存储了 10 个颜色,那么在具体生成小球的时候我会在这个数组里随机抽取颜色来为小球附上相应的颜色值。下面我们来写一下小球生成的代码,大家可以想象一下,我们把这个生成的代码应该放到 update 里,因为 update 是负责数据的改变,而 render 只是负责绘制,那么生成小球是数据上的改变,我们调在 update 相应的位置,同样相应位置也应该放在 if 里面:
if( nextSeconds != curSeconds ){ curShowTimeSeconds = nextShowTimeSeconds; }
一旦我们的时间发生了改变,就要根据当前时间的改变来生成一系列的小球,具体生成过程还要看当前所改变的时间到底改变了这些时间的那些数字,所以我们要对六个数字依次进行一次判断,对此呢我的做法如下:
if( parseInt(curHours/10) != parseInt(nextHours/10) ){ addBalls( MARGIN_LEFT + 0 , MARGIN_TOP , parseInt(curHours/10) ); }
大家可以看一下,以小时的十位数为例,如果我发现当前的小时的十位数已经不等于下次要显示的这个小时十位数,我在这里创建了一个新的函数 addBalls,addBalls 负责加小球,那么在具体的加小球怎么加呢?我要找到这个小时的十位数所在的位置,就是 MARGIN_LEFT 和 MARGIN_TOP 这个位置,以及相应的数字是多少,这个相应的数字就是当前这个小球所显示的十位数字。那么在这个 addBalls 函数里呢我将根据这些状态生成一系列的小球,那么类似的大家可以想象,我对这个小时的个位数也需要进行一次这样的操作:
if( parseInt(curHours%10) != parseInt(nextHours%10) ){ addBalls( MARGIN_LEFT + 15*(RADIUS+1) , MARGIN_TOP , parseInt(curHours/10) ); }
注意这里的位置是 MARGIN_LEFT + 15*(RADIUS+1)不知道大家还记不记得为什么是 15 倍的(RADIUS+1),在之前的文章中我们分析过。同时我们需要对时间的分钟进行这样的操作,时间的秒钟也需要进行这样的操作。代码如下:
if( parseInt(curMinutes/10) != parseInt(nextMinutes/10) ){ addBalls( MARGIN_LEFT + 39*(RADIUS+1) , MARGIN_TOP , parseInt(curMinutes/10) ); } if( parseInt(curMinutes%10) != parseInt(nextMinutes%10) ){ addBalls( MARGIN_LEFT + 54*(RADIUS+1) , MARGIN_TOP , parseInt(curMinutes%10) ); } if( parseInt(curSeconds/10) != parseInt(nextSeconds/10) ){ addBalls( MARGIN_LEFT + 78*(RADIUS+1) , MARGIN_TOP , parseInt(curSeconds/10) ); } if( parseInt(curSeconds%10) != parseInt(nextSeconds%10) ){ addBalls( MARGIN_LEFT + 93*(RADIUS+1) , MARGIN_TOP , parseInt(nextSeconds%10) ); }
至此问题的关键就是 addBalls 这个函数应该怎么实现,这这里大家可以想象到这个函数跟我们之前设计的 renderDigit 函数会非常的像,renderDigit 函数是在 x、y 的位置对 num 数字点阵化进行渲染,那 addBalls 则是在 x、y 的位置对 num 这个数字点阵化位置加上一个彩色的小球,那么这个函数大家可以想象一下也是进行一次二重循环,代码如下:
function addBalls( x , y , num ){ for( var i = 0 ; i < digit[num].length ; i ++ ) for( var j = 0 ; j < digit[num][i].length ; j ++ ) if( digit[num][i][j] == 1 ){ var aBall = { x:x+j*2*(RADIUS+1)+(RADIUS+1), y:y+i*2*(RADIUS+1)+(RADIUS+1), g:1.5+Math.random(), vx:Math.pow( -1 , Math.ceil( Math.random()*1000 ) ) * 4, vy:-5, color: colors[ Math.floor( Math.random()*colors.length ) ] } balls.push( aBall ) } }
第一重循环对 digit[num][i].length 进行一次循环,第二重循环对 digit[num][i].length 进行一次循环,在两重循环之后我们应该判断一下 digit[num][i][j]是否为 1,如果为 1 我们就应该在这个位置添加一个小球,具体的添加过程,首先我们建一个 aBall 即一个小球,这样一个类的对象,包含坐标位置 x,x 位置在前我们分析过了是这样一个位置 x+j2(RADIUS+1)+(RADIUS+1),同理,我们添加这个小球 y 位置 y 位置也在之前的文章课程中分析过是这样的一个位置 y+i2(RADIUS+1)+(RADIUS+1),之后小球应该有一个半径,那么在这里呢我们都用我们声明的全局变量 RADIUS 表示了,所以在这个小球的信息里可以不写它了。接下来就是小球运动相关的信息,首先我们设置一下它的加速度,我是用这样一个式子 1.5 加上 0~1 之间随机数字:1.5+Math.random(),这样小球的加速度呢就是在 1.5 到 2.5 之间,我产生这样一个变化呢就是让每个小球稍微不同,这样看起来表现更加活泼,之后就是小球在 x 轴方向的速度,我采用这样的式子:Math.pow( -1 , Math.ceil( Math.random()*1000 ) ) * 4,这个式子看起来有点复杂,仔细分析一下很简单,就是-1 的多少次方,这个多少次方我采用 0~1 之间随机数乘以 1000,换句话说,在 0 到 1000 之间用 ceil 方式取整,所以这段式子所表达的意思就是取负 1 或者是正 1,如果我们随机出来这个结果是偶数,那么结果为正 1,若果是奇数则结果为负 1,最后乘以 4,所以这个小球在水平方向 x 轴方向的速度是取负 4 或者是正 4 的。当然对于这个结果大家可以用更复杂的随机机制让这个结果更加随机化,在这里我只是举一个例子。那同样在 y 方向的速度为了简单起见我起了一个固定的值负 5,负 5 这个值呢会使所有小球在蹦出来的时候有一个稍微向上抛的这么一个动作。同样大家可以使用随机化手段让每一个小球速度稍有不同,这样表现出来的结果更加灵活。最后一个元素呢是这一个小球特有的 color 值,之前我建立了一个 colors 数组,在这个数组里我存了 10 个颜色,为此我要随机一个索引,随机这个索引的方法就是用 Math.random 这个函数取一个 0 到 1 的随机数之后乘以 colors.length,然后用下取整的方式,这样随机出 0 到 10 不包含 10 的随机数。
在这个方法里面我用了很多随机化的概念,使的每一个小球稍有不同,这样表现起来更加灵活。同时大家还可以改造这些式子,让自己的小球拥有更随机化的效果,使得小球表现的结果更加灵活。
这样呢我,我们就创建好了一个小球,之后我们在这个 balls 数据中 push()把我们刚刚建立的这个小球 push 进去,通过这样的一个循环过程,我们就在合适的位置产生了这些小球。我们新产生的这些小球都是静止的,那么为了让它们运动起来我们还需要编写相应的方法,这个方法呢还应该放在 update 函数里,这个 update 函数负责了时间的改变,负责了如果当前需要产生新的小球那么就要产生新的小球这样变化,与此同时还需要负责对已经产生的小球运动变化进行更新,这里面我们声明一个新的函数 updateBalls,这个函数对所有已经存在小球的状态进行更新。
function updateBalls(){ //对 balls 的数组进行遍历 for( var i = 0 ; i < balls.length ; i ++ ){ //x 轴坐标的位置加上他在 x 轴方向的速度值 balls[i].x += balls[i].vx; //y 轴坐标的位置加上他在 y 轴方向的速度值 balls[i].y += balls[i].vy; //y 的速度受重力的影响 balls[i].vy += balls[i].g; //对地板的部分进行一次碰撞检测 if( balls[i].y >= WINDOW_HEIGHT-RADIUS ){ balls[i].y = WINDOW_HEIGHT-RADIUS; //y 轴的速度取一个相反的速度 balls[i].vy = - balls[i].vy*0.75; } } }
上面的代码是对所有的小球更新进行操作,所以首先我们要进行一层循环,对 balls 的数组进行遍历,那么每一次就取得了一个小球,对于这一个小球我们对它的位置进行相应的变化,首先是 x 轴坐标的位置加上他在 x 轴方向的速度值,y 轴坐标的位置加上他在 y 轴方向的速度值,由于小球重力的影响,y 的速度受重力的影响进行这样一个操作。在上面我们也提到了应该对地板的部分进行一次碰撞检测,如果当前小球的 y 坐标的位置已经大于等于屏幕的高度减去小球半径值的话,那么 Y 坐标位置复原到地板上的位置,同时 y 轴的速度取一个相反的速度,这样小球的基本运动就完成了。
最后就剩下处理小球的绘制了,相信大家对小球的绘制非常熟悉了,我们找到 render 这个函数,在后面我们加上一段代码对小球的数组进行一次遍历,之后对每一个小球进行具体绘制,具体代码如下:
for( var i = 0 ; i < balls.length ; i ++ ){ cxt.fillStyle=balls[i].color; cxt.beginPath(); cxt.arc( balls[i].x , balls[i].y , RADIUS , 0 , 2*Math.PI , true ); cxt.closePath(); cxt.fill(); }
现在我们整个倒计时小球的绘制就出来,下面我们来看看这个程序运行的结果:
是不是非常的炫酷,在本章内容中对小球的运动进行了诸多的设置,大家也可以通过自己的想法进行改造,比如说碰撞检测可不可以对两边进行相应的碰撞检测,还有弹跳的数值可不可以改造呢,使得小球的表现方式截然不同。都有待大家去实验,相信是件非常好玩的事情,大家加油。
码云笔记 » canvas实现绚丽的倒计时效果与动画基础(二)
不错不错,思路很好,大佬有源码吗?分享一下,哈哈
有的,在第三篇文章,马上发布