首页 专题 H5案例 前端导航 UI框架

canvas动画包教不包会:缓动与弹动

阅读 1477 评论 0
到目前为止,《canvas动画包教不包会》系列已经进行了7章,主要讲解了用户交互与基础动画,从本章开始,将进入高级动画篇。这一章主要讲解缓动(比例速度)和弹动(比例加速度)。

1、比例运动
比例运动 是指运动与距离成比例的运动。

缓动弹动都是比例运动,两者关系紧密,都是将对象从已有位置移动到目标位置的方法。缓动是指物体滑动到目标点就停下来了。弹动是指物体来回地反弹一会儿,最终停在目标点的运动。

两者的共同点
  • 有一个目标点
  • 确定物体到目标点的距离
  • 运动与距离是成正比的----距离越远,运动的程度越大

两者的不同点
  • 运动和距离成正比的方式不一样。缓动是指 速度 距离 成正比(物体离目标越远,物体运动的速度越快,当物体运动到很接近目标点时,物体几乎就停下来了);而弹动是指 加速度 距离 成正比(物体离目标点越远,加速度就快速增大,当物体很接近目标点时,加速度变得很小,但它还是在加速;当它越过目标点之后,随着距离的变大,反向加速度也随之变大,就会把它拉回了,最终在摩擦力的作用下停住。)

2、缓动
缓动的类型不止一种,我们可以“缓入”(ease in)到一个位置,也可以从一个位置“缓出”(ease out)。

在现实生活中,相信大家都坐过公交(自动过滤土豪),在宽敞的马路上时,公交会高速前进,特别是车少的道路,司机会开的尽可能快(限速之内),当快要达到一个站点时,司机就会适当的减速。当公交还有几米就要停下来的时候,速度已经很慢很慢了。这就是一种缓动。

如何实现缓动呢?
一般来说,我们会如下处理:
  • 为运动确定一个小于1且大于0的小数作为比例系数(easing)
  • 确定目标点
  • 计算物体与目标点的距离
  • 计算速度,速度=距离 * 比例系数
  • 用当前位置加上速度来计算新的位置
  • 不断重复第3步到第5步,直到物体到达目标点

缓动的整个过程并不复杂,我们需要知道距离(物体与目标点(target)之间,变化值)、比例系数(easing,速度除以距离)。

dx = targetX - ball.x;

dy = targetY - ball.y;


easing = vx / dx;  =>   vx = dx * easing;

easing = vy / dy;  =>   vy = dy * easing;

根据《速度与加速度》那一章的公式:

ball.x += vx;  =>  ball.x += dx*easing;  =>  ball.x += (targetX - ball.x) * easing;

ball.y += vy;  => ball.y += dy*easing; => ball.y += (targetY - ball.y) * easing;

最终缓动公式:

ball.x += (targetX - ball.x) * easing;

ball.y += (targetY - ball.y) * easing;

来看看例子:

关键代码:

var easing = 0.05;   

var targetX = canvas.width - 10;   

var targetY = canvas.height - 10;

在上面的例子中,我们将比例系数设为0.05,用变量easing表示,然后在循环中调用下面的代码:

ball.x += (targetX- ball.x)*easing; //每次循环中调用

这样简单的处理,就能实现刹车模式,这就是缓动的一种效果,你可以改变easing看看。


上面的例子中的目标点是canvas边界,其实,目标点是可以 变动 的,因为我们每次都会重新计算距离,所以只须在播放每一帧的时候知道目标点的位置,然后就可以计算距离和速度了。比如:将鼠标位置(mouse.x和mouse.y)作为目标点,你可以试试,会发现鼠标里的越远,小球就运动的越快。


这里还有一个关键性问题:何时停止缓动


不是到达目标点就停止缓动吗?估计这是你看到这的第一想法,你还可能立即想到下面判断公式:

if(ball.x === targetX && ball.y === targetY){

  //到达目标点

}

这是理论上的判断,但是从数学的角度来看,下面的公式永远不会相等:

(ball.x + (targetX - ball.x) * easing) !== targetX

这是为什么呢?

这就涉及了 芝诺饽论 ,简单的理解是这样:为了把一个物体从A点移到B点,就必须把它先移到到A和B的中间点C,然后再移到C和B的中间点,然后再折半,不断地重复下去,每次移到到物体到距离目标点的一半,这样就会进入无穷循环下去,物体永远不会到达目标点。


我们来看看数学例子:物体从0的位置,要将它移到100,比例系数easing设为0.,5,然后将它每次移动距离的一半,过程如下:

  • 从原点开始,在第一帧后,它移到到50
  • 在第二帧后,移动到75
  • 在第三帧后,移动到87.5
  • 就这样循环下去,物体位置变化是93.75、96.875等,经过20帧后,它的位置是99.999809265

看到没有,它会离目标点越来越近,可是理论上是永远不会到达目标点的,所以上面的判断公式是永远不会返回true的。


但毕竟肉眼是无法分辨这么精确的位置变化的,有时候当ball.x 等于99的时候,我们在canvas上看就已经是到达终点了,所以这就产生了一个问题:多近才是足够近呢?


这就需要我们人为的指定一个特定值,判断物体到目标点的距离是否小于特定值,如果小于特定值,那我们就认为它到达终点了。

/*二维坐标*/

distance = Math.sqrt(dx * dx + dy * dy);


/*一维坐标*/

distance = Math.abs(dx)


if(distance < 1){

  console.log('到达终点');

  cancelAnimationFrame(requestID);

}

一般采取是否小于1来判断是否到达目标点,是为了停止动画,避免资源的浪费。


在tool.js工具类中,我们已经封装了停止 requestAnimaitonFrame 动画的方法,就是 cancelRequestAnimationFrame ,参数是requestID。

var cancelAnimationFrame = function() {   

  return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || function(id) {   

    clearTimeout(id);   

  };  

}();

当然,缓动并不仅仅适用于运动,它还可以应用很多属性:


(1)旋转

定义起始角度:

var rotation = 0;

var targetRotation = 360;

然后缓动:

rotation += (targetRotation - rotation) * easing;

object.rotation = rotation * Math.PI / 180;

别忘了弧度与角度的转换。


(2)透明度

设置起始透明度

var alpha = 0;

var targetAlpha = 1;

设置缓动:

alpha += (targetAlpha - alpha) * easing;

object.color = 'rgba(255,0,0,' + alpha + ')';


2、弹动

前面提到过,在弹动中,物体的 加速度 与它到目标点的 距离 成正比。


现实中的弹动例子:在橡皮筋的一头系上一个小球(悬空,静止时的点就是目标点),另一头固定起来。当我们用力(力足够大)去拉小球然后松开,我们会看到小球反复的上下运动几次后,速度逐渐慢下来,停在目标点上。(没玩过橡皮筋的,可以去实践一下)


2.1 一维坐标上的弹动

实现弹动的代码和缓动类似,只不过将速度换成了加速度(spring)

var spring = 0.1;

var targetX = canvas.width / 2;

var vx = 0;

计算小球到目标点的距离:

var dx = targetX - ball.x;

计算加速度,与距离是成比例的:

var ax = dx * spring;

将加速度加在速度上,然后添加到小球的位置上:

vx += ax;

ball.x += vx;

我们先模拟一下整个弹动过程,假设小球的x是0,vx也是0,目标点的x是100,spring变量的值为0.1:

  • 用距离(100)乘以spring,得到10,将它加在vx上,vx变为10,把vx加在小球的位置上,小球的x为10
  • 下一帧,距离(100-10)为90,加速度为90乘以0.1,等于9,加在vx上,vx就变为19,小球的x变为了29
  • 再下一帧,距离是71,加速度是7.1,vx是26.1,小球的x为55.1


重复几次后,随着小球一帧一帧的靠近目标,加速度变得越来越小,速度越来越快,虽然增加的幅度在减小,但还是在增加。


当小球越过了目标点,到底了x轴上的117点时,与目标点的距离是-17(100-117)了,也就是加速度会是-1.7,当速度加上这个加速度时,小球就会减速运动。


这就是弹动的过程。


看看实例(目标点定在canvas的中心点,相当于将球从中心点拉到左边,然后松开)



上面的例子中,小球是不是有种被弹簧拉扯的效果,但是,由于小球的摆动幅度不变,它现在貌似停不下来,这不科学,现实中,它的摆动幅度应该是越来越小(由于阻力),弹动的越来越慢,直到停下来,所以为了更真实,我们应该给它添加一个摩擦力friction:

var friction = 0.95;

然后改变速度:

vx += ax;

vx *= friction;

ball.x += vx;

当小球停止时,我们就不需去执行动画了,所以我们还需要判断是否停止:

if(Math.abs(vx) < 0.001){

  vx += ax;  

  vx *= friction;  

  ball.x += vx;

};

注意:当你的初始速度vx为0时,这样是无法进入弹动的,对我来说,我会加入一个变量判断是否开始弹动:

var isBegin = false;

if(!isBegin || Math.abs(vx) < 0.001){

  vx += ax;     

  vx *= friction;     

  ball.x += vx;

  isBegin = true;

};


2.2 二维坐标上的弹动

二维坐标上的弹动与一维坐标上的弹动并没有大区别,只不过前者多了y轴上的弹动。

初始化变量:

var vx = 0;   

var ax = 0;   

var vy = 0;   

var ay = 0;

var dx = 0;

var dy = 0;

设置x、y轴上的弹动:

if(Math.abs(vx) > 0.001){

  dx = targetX - ball.x;

  ax = dx * spring;   

  vx += ax;   

  vx *= friction;   

  ball.x += vx;

  dy = targetY - ball.y;   

  ay = dy * spring;   

  vy += ay;   

  vy *= friction;   

  ball.y += vy;   

};

例子(将canvas的中心点作为目标点,相当于一开始将球从中心点拉到左上角,然后松开):



上面的例子依旧是一个直线弹动,你可以试试将vx或vy的初始值增大一点,设为50,会有意想不到的动画。


2.3  向移动的目标点弹动

在缓动中也说过,目标点不一定是固定,而对于弹动也一样,目标点可以是移动的,只需在每一帧改变目标点的坐标值即可,比如:鼠标坐标是目标点:

dx = targetX - ball.x;

dy = targetY - ball.y;


/*改成如下*/

dx = mouse.x - ball.x;  

dy = mouse.y - ball.y;


2.4 绘制弹簧

在上面的几个例子中,虽然有了弹簧的效果,可是始终还是没看到橡皮筋所在,所以我们有必要来将橡皮筋绘画出来:

ctx.beginPath();

ctx.moveTo(ball.x,ball.y);

ctx.lineTo(mouse.x,mouse.y);

ctx.stroke();

实例:



为了更真实,你还可以加上重力加速度:

var gravity = 2;


vy += gravity;

注意:在物理学中,重力是一个常数,只由你所在星球的质量来决定的。理论上,应该保持gravity值不变,比如0.5,然后给物体增加一个mass(质量)属性,比如10,然后用mass乘以gravity得到5(依旧用gravity变量表示)。


2.5 链式弹动

链式运动是指物体A以物体B为目标点,物体B又以物体C为目标点,诸如此类的运动。


看看例子,然后再来分析:


在上面的例子中,我们创建了四个球,每个球都有自己的属性 vx vy ,初始为0。在动画函数 animation 里,我们使用Array.forEach()方法来绘制每一个球,然后连线。在 connect 方法中,你可以看到第一个球的目标点是鼠标位置,剩余的球都是以上一个球(balles[i-1])的坐标位置为目标点来弹动。


我还给球添加了重力:

ball.vy += gravity;

运动结束时,四个球会连成一串。


2.6 目标偏移量

在上面的所有例子中,我们使用的都是模拟橡皮筋,如果我们模拟的是一个弹性金属材料制作的弹簧会怎样呢?是不是球还可以这样自由的运动呢?


答案是否定,在现实中,你无法让物体顶着弹簧从一头运动到另一头,还不明白?看下图:


假设上面的图中连接球和固定点是金属弹簧,那么球是永远都到不了固定点的位置的,因为弹簧是有体积的,会把球挡住,而且一旦弹簧收缩到它正常的长度,它就不会对小球施加拉力了,所以,真正的目标点,其实是弹簧处于松弛(拉伸)状态时,系着小球那一端的那个点(这个点是变化的)。


那如何确定目标点呢?


其实,从我上面的图你就应该想到,要用三角函数,因为我们知道球的位置、固定点的位置,那我们就可以获得球与固定点之间的夹角 θ ,当然,我们还需要定义一个弹簧的长度(springLength),比如:100。


计算目标点的代码如下:

dx = ball.x - fixedX;

dy = ball.y -fixedY;

angle = Math.atan2(dy,dx);

targetX = fixedX + Math.cos(angle) * springLength;

targetY = fixedY + Math.sin(angle) * springLength;


又到了例子时刻(以canvas的中心点为固定点,弹簧长度为100,小球可拖动):


试过上面例子了吗?我们再来看看上面的图:


图中的A点相当于例子中的固定点(也就是canvas的中心点),B点是弹簧(无压缩无拉伸)正常情况下的位置(也是弹动的目标点),C点就是你拖动小球然后松开鼠标的位置,那么AB之间的距离就是弹簧的长度100,而BC之间的距离就是小球弹动的距离了,同时,基于直角三角形,我们也很容易求得 θ 的值。


我们还定义了一个 getBound() 方法,传入球对象,返回一个矩形对象,也就是球的矩形边界。


例子的部分代码:

dx = ballA.x - mouse.x;   

dy = ballA.y - mouse.y;   

angle = Math.atan2(dy, dx); // 获取鼠标与球之间的夹角θ   

//计算目标点坐标   

targetX = mouse.x + Math.cos(angle) * springLength;   

targetY = mouse.y + Math.sin(angle) * springLength;   

ballA.vx += (targetX - ballA.x) * spring;   

ballA.vy += (targetY - ballA.y) * spring;   

ballA.vx *= friction;   

ballA.vy *= friction;   

ballA.x += ballA.vx;   

ballA.y += ballA.vy;


2.7 用弹簧连接多个物体

我们还可以用弹簧连接多个物体,先从连接两个物体开始,让它们互相向对方弹动,移动其中一个,另一个就要跟随弹动过去:


上例子:


在上面的例子中,我们创建了两个Ball实例 ball0 ball1 ,都是可拖动的,ball0向ball1弹动,ball1向ball0弹动,而且它们之间有一定的偏移量,两者用弹簧连接。


 springTo() 方法接受两个参数,第一个参数是移动物体,第二个参数是目标点。还要引入两个变量: ball0_dragging ball1_dragging ,作为是否拖动小球的标志。

if(!ball0_dragging) {   

  springTo(ball0, ball1);   

};   

if(!ball1_dragging) {   

  springTo(ball1, ball0);   

};


下面让我们加入第三个球ball2:



总结

本章主要介绍了两个比例运动:缓动弹动

  • 缓动是指 速度 距离 成正比(物体离目标越远,物体运动的速度越快,当物体运动到很接近目标点时,物体几乎就停下来了);
  • 弹动是指 加速度 距离 成正比(物体离目标点越远,加速度就快速增大,当物体很接近目标点时,加速度变得很小,但它还是在加速;当它越过目标点之后,随着距离的变大,反向加速度也随之变大,就会把它拉回了,最终在摩擦力的作用下停住。)


附录


重要公式:


(1)简单缓动

dx = targetX - object.x;

dy = targetY - object.y;

vx = dx * easing;

vy = dy * easing;

object.x += vx;

object.y += vy;

可精简:

vx = (targetX - object.x) * easing;

vy = (targetY - object.y) * easing;

object.x += vx;

object.y += vy;

再精简:

object.x += (targetX - object.x) * easing;

object.y += (targetY - object.y) * easing;


(2)简单弹动

ax = (targetX - object.x) * spring;

ay = (targetY - object.y) * spring;

vx += ax;

vy += ay;

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

可精简:

vx += (targetX - object.x) * spring;

vy += (targetY - object.y) * spring;

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

再精简:

vx += (targetX - object.x) * spring;

vy += (targetY - object.y) * spring;

object.x += (vx *= friction);

object.y += (vy *= friction);


(3)有偏移的弹动

dx = object.x - fixedX;

dy = object.y - fixedY;

targetX = fixedX + Math.cos(angle) * springLength;

targetY = fixedY + Math.sin(angle) * springLength;


下一章:碰撞检测


关注”全栈技术杂货铺“

全栈技术杂货铺