## 缓动与弹动 这一章主要讲解缓动(比例速度)和弹动(比例加速度)。 **1、比例运动** 比例运动 是指运动与距离成比例的运动。 缓动和弹动都是比例运动,两者关系紧密,都是将对象从已有位置移动到目标位置的方法。缓动是指物体滑动到目标点就停下来了。弹动是指物体来回地反弹一会儿,最终停在目标点的运动。 两者的共同点: - 有一个目标点 - 确定物体到目标点的距离 - 运动与距离是成正比的----距离越远,运动的程度越大 两者的不同点: - 运动和距离成正比的方式不一样。缓动是指 速度 与 距离 成正比(物体离目标越远,物体运动的速度越快,当物体运动到很接近目标点时,物体几乎就停下来了);而弹动是指 加速度 与 距离 成正比(物体离目标点越远,加速度就快速增大,当物体很接近目标点时,加速度变得很小,但它还是在加速;当它越过目标点之后,随着距离的变大,反向加速度也随之变大,就会把它拉回了,最终在摩擦力的作用下停住。) **2、缓动** 缓动的类型不止一种,我们可以“缓入”(ease in)到一个位置,也可以从一个位置“缓出”(ease out)。 在现实生活中,相信大家都坐过公交(自动过滤土豪),在宽敞的马路上时,公交会高速前进,特别是车少的道路,司机会开的尽可能快(限速之内),当快要达到一个站点时,司机就会适当的减速。当公交还有几米就要停下来的时候,速度已经很慢很慢了。这就是一种缓动。 **如何实现缓动呢?** 一般来说,我们会如下处理: - 为运动确定一个小于1且大于0的小数作为比例系数(easing) - 确定目标点 - 计算物体与目标点的距离 - 计算速度,速度=距离 * 比例系数 - 用当前位置加上速度来计算新的位置 - 不断重复第3步到第5步,直到物体到达目标点 ![](https://box.kancloud.cn/d2915bbcc9156a6d2daa7e18efa3a74c_332x221.jpg) 缓动的整个过程并不复杂,我们需要知道距离(物体与目标点(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; ``` 实例:canvas-demo/easing.html 关键代码: ``` 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的中心点,相当于将球从中心点拉到左边,然后松开):canvas-demo/spring.html 上面的例子中,小球是不是有种被弹簧拉扯的效果,但是,由于小球的摆动幅度不变,它现在貌似停不下来,这不科学,现实中,它的摆动幅度应该是越来越小(由于阻力),弹动的越来越慢,直到停下来,所以为了更真实,我们应该给它添加一个摩擦力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的中心点作为目标点,相当于一开始将球从中心点拉到左上角,然后松开):canvas-demo/spring2.html 上面的例子依旧是一个直线弹动,你可以试试将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(); ``` 实例:canvas-demo/spring3.html 为了更真实,你还可以加上重力加速度: ``` var gravity = 2; vy += gravity; ``` 注意:在物理学中,重力是一个常数,只由你所在星球的质量来决定的。理论上,应该保持gravity值不变,比如0.5,然后给物体增加一个mass(质量)属性,比如10,然后用mass乘以gravity得到5(依旧用gravity变量表示)。 **2.5 链式弹动** 链式运动是指物体A以物体B为目标点,物体B又以物体C为目标点,诸如此类的运动。 看看例子,然后再来分析:canvas-demo/spring4.html 在上面的例子中,我们创建了四个球,每个球都有自己的属性 vx 和 vy ,初始为0。在动画函数 animation 里,我们使用Array.forEach()方法来绘制每一个球,然后连线。在 connect 方法中,你可以看到第一个球的目标点是鼠标位置,剩余的球都是以上一个球(balles[i-1])的坐标位置为目标点来弹动。 我还给球添加了重力: ``` ball.vy += gravity; ``` 运动结束时,四个球会连成一串。 **2.6 目标偏移量** 在上面的所有例子中,我们使用的都是模拟橡皮筋,如果我们模拟的是一个弹性金属材料制作的弹簧会怎样呢?是不是球还可以这样自由的运动呢? 答案是否定,在现实中,你无法让物体顶着弹簧从一头运动到另一头,还不明白?看下图: ![](https://box.kancloud.cn/342215d247313fde21dda810e7ab51ef_325x270.jpg) 假设上面的图中连接球和固定点是金属弹簧,那么球是永远都到不了固定点的位置的,因为弹簧是有体积的,会把球挡住,而且一旦弹簧收缩到它正常的长度,它就不会对小球施加拉力了,所以,真正的目标点,其实是弹簧处于松弛(拉伸)状态时,系着小球那一端的那个点(这个点是变化的)。 那如何确定目标点呢? 其实,从我上面的图你就应该想到,要用三角函数,因为我们知道球的位置、固定点的位置,那我们就可以获得球与固定点之间的夹角 θ ,当然,我们还需要定义一个弹簧的长度(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,小球可拖动):canvas-demo/spring5.html 试过上面例子了吗?我们再来看看上面的图: ![](https://box.kancloud.cn/342215d247313fde21dda810e7ab51ef_325x270.jpg) 图中的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 用弹簧连接多个物体** 我们还可以用弹簧连接多个物体,先从连接两个物体开始,让它们互相向对方弹动,移动其中一个,另一个就要跟随弹动过去: 上例子:canvas-demo/spring6.html 在上面的例子中,我们创建了两个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:canvas-demo/spring7.html **总结** 本章主要介绍了两个比例运动:缓动和弹动 缓动是指 速度 与 距离 成正比(物体离目标越远,物体运动的速度越快,当物体运动到很接近目标点时,物体几乎就停下来了); 弹动是指 加速度 与 距离 成正比(物体离目标点越远,加速度就快速增大,当物体很接近目标点时,加速度变得很小,但它还是在加速;当它越过目标点之后,随着距离的变大,反向加速度也随之变大,就会把它拉回了,最终在摩擦力的作用下停住。) **附录** **重要公式:** (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; ```