javascript运动系列第五篇——缓冲运动和弹性运动

时间:2023-09-06 09:02:32

前面的话

  缓冲运动指的是减速运动,减速到0的时候,元素正好停在目标点。而弹性运动同样是减速运动,但元素并不是直接停在目标点,而是在目标点附近弹几下再停止。本文将以一种新的思路来详细介绍缓冲运动和弹性运动

缓冲运动

  在变速运动中,曾经用物理学的知识实现过缓冲运动。缓冲运动实际上就是减速运动的一种特殊形式,指元素做减速运动,速度减到0时,恰好停在目标点位置,学名叫加速度恒定的匀减速运动

  现在使用另一种思路,样式值等于当前值加上步长值,步长值的变化决定了运动的形式

test.style.left = cur + step + 'px';

  元素距离目标点越近,速度越小,所以step可以写成如下公式

step = (target - cur)*k;

  k表示减速系数,k不能随便取值,当k值过大时,效果将很不明显。因为step取值过大,使得定时器仅仅工作几次,元素就达到了目标点

  当k值过小时,也会出现问题。cur值取得的值是当前的计算样式,而计算样式的值由于计算机存储的限制,并不能储存全部小数位数,IE9+浏览器可以储存2位小数,IE8-浏览器不可以储存小数,其他浏览器可以储存3位小数。这样,在计算中,当step值为0.0009(chrome),或0.009(IE9+),或0.1(IE8-)时,test.style.left = cur + step + 'px'将不起作用,样式值将不会改变

  这时,只要将当前值向上取整(当前值为负数时,向下取整)即可

<button id="btn1">匀速运动</button>
<button id="btn2">缓冲运动</button>
<button id="reset">还原</button>
<div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<div style="background-color:red;width:1px;height:100px;position:absolute;left:500px;"></div>
<script>
function getCSS(obj,style){
if(window.getComputedStyle){
return getComputedStyle(obj)[style];
}
return obj.currentStyle[style];
}
function easyMove(json){
var obj = json.obj;
var attr = json.attr;
//样式默认值为'left'
attr = attr || 'left';
var value = json.value;
var target = json.target;
//目标点默认值为'200'
target = Number(target) || 200;
//声明步长值step
var step;
//声明当前值cur
var cur = parseFloat(getCSS(test,'left'));
var type = json.type;
var fn = json.fn;
//如果没有建立定时器对象,则在obj下建立定时器对象
if(!obj.timers){obj.timers = {};}
//清除定时器
if(obj.timers[attr]){return;}
//开启定时器
obj.timers[attr] = setInterval(function(){
//更新当前值
cur = parseFloat(getCSS(test,'left'));
switch(type){
case 'linear':
step = Number(value) || 10;
break;
case 'buffer':
//处理到不了目标点的问题
cur = cur > 0 ? Math.ceil(cur) : Math.floor(cur);
value = Number(value) || 0.1;
//更新步长值
step = (target - cur)*0.1;
break;
default:
step = 10;
}
//若步长设置值使得元素超过目标点时,将步长设置值更改为目标点值 - 当前值
if((cur + step - target)*step > 0){
step = target - cur;
}
//将合适的步长值赋值给元素的样式
obj.style[attr] = cur + step + 'px';
//当元素到达目标点时,停止定时器
if(step == target - cur){
clearInterval(obj.timers[attr]);
obj.timers[attr] = 0;
fn && fn.call(obj);
}
},20);
}
reset.onclick = function(){history.go();}
btn1.onclick = function(){
easyMove({obj:test,target:500})
}
btn2.onclick = function(){
easyMove({obj:test,target:500,type:'buffer'})
}
</script>

弹性运动

  要理解弹性运动,可以想象一个被拉伸的弹性绳子绑住的小球,小球向绑绳子的木桩运动,并围绕木桩左右运动,最终由于空气阻力的影响,停在木桩处

javascript运动系列第五篇——缓冲运动和弹性运动

  接下来,我们对小球的运动过程进行详细分析

  【1】小球在起始点时,受到绳子的弹力f=k*s,k为弹性系数,s=max为小球和木桩的距离。于是,小球向右运动

  【2】小球在向右运动的过程中,由于距离木桩的距离s逐渐变小,f=k*s,所以弹力逐渐变小,而小球受到的阻力fx是恒定的。所以,小球做加速度减小的加速运动(加得越来越慢)

  【3】当f = fx时,小球此时的加速度为0。此后,小球开始向右做加速度增大的减速运动(减得越来越快)

  【4】当小球运动到木桩处时,弹力突然消失。这时,只剩余空气阻力

  【5】小球继续向右运动,小球有反向的弹力和阻力。阻力一直不变,而弹力越来越大。所以,小球做加速度增大的减速运动(减得越来越快)

  【6】最终,小球运动到右边最远处时,速度减成0。此时,小球不受阻力,只受到反向的弹力。于是,小球开始向左做加速度减小的加速运动

  【7】在向左运动的过程中,小球受到了向右的阻力,于是,加速度继续减小

  【8】在某一时刻,阻力等于弹力,加速度减成0。此后,阻力将大于弹力,小球将做减速运动

  【9】若小球运动到木桩处,速度减成0,则运动停止。否则,小球将继续向左做减速运动

  【10】在某一时刻,小球速度减成0。此后,小球向右做先加速再减速的运动。若小球运动到木桩处,速度减成0,则运动停止。否则,小球将继续向右做减速运动。接下来,将重复第6步的内容

  如果要按照物理学公式实现弹性运动时,元素的运动涉及到变加速运动,需要用到微积分的知识,处理起来相对复杂

距离分析

  下面用一个简单的思路来实现弹性运动。如果以距离来分析,弹性运动就是每一次运动距离不断减小的运动

  例如,元素刚开始时距离目标点为100。第一次运动向右运动150,到达150处;第二次运动向左运动75,到达75处;第三次运动向右运动37.5,到达112.5处;第四次运动向左运动18.75,到达93.75。以此反复,最终无限接近于目标点100

    set =  init + target + len*k;
k*=0.5;

  其中init为样式初始值,target为目标值,len为弹性最远值,k为衰减系数

  由于利用距离分析实现的弹性运动,实际上是一个无限接近于目标点的运动,需要为其设置停止条件,并将其位置置于目标点

  当len*k的四舍五入值等于0时,停止运动

    if(Math.round(len*k)  == 0){
obj.style[attr] = target + 'px';
clearInterval(obj.timer);
}
<button id="btn">弹性运动</button>
<button id="reset">还原</button>
<div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<div style="background-color:red;width:1px;height:100px;position:absolute;left:500px;"></div>
<script>
function getCSS(obj,style){
if(window.getComputedStyle){
return getComputedStyle(obj)[style];
}
return obj.currentStyle[style];
}
function elasticMove(json){
var obj = json.obj;
var attr = json.attr;
//样式默认值为'left'
attr = attr || 'left';
var target = json.target;
//目标点默认值为'200'
target = Number(target) || 200;
//声明元素从目标点到最远点的距离
var len = json.len;
//默认值为target的1/5
len = len || target/5;
//声明初始值init
var init = parseFloat(getCSS(obj,attr));
//如果初始值等于目标点,则返回
if(init == target) return;
var fn = json.fn;
//声明当前设置值
var set = 0;
//声明衰减系数
var k = 1;
//如果没有建立定时器对象,则在obj下建立定时器对象
if(!obj.timers){obj.timers = {};}
//清除定时器
if(obj.timers[attr]){return;}
//开启定时器
obj.timers[attr] = setInterval(function(){
//更新当前值
set = init + target + len*k;
k*=-0.5;
obj.style[attr] = init + set + 'px';
//当元素到达目标点时,停止定时器
if(Math.round(len*k) == 0){
obj.style[attr] = target + 'px';
clearInterval(obj.timers[attr]);
obj.timers[attr] = 0;
fn && fn.call(obj);
}
},50);
}
reset.onclick = function(){history.go();}
btn.onclick = function(){
elasticMove({obj:test,target:500})
}
</script>
</body>
</html>

步长分析

  弹性运动的另一种方法是使用步长分析法

  步长分析法是分析定时器每一次运行时样式的变化值。我们可以将弹性运动分解为受弹力影响的运动和受阻力影响的运动

  由于受到弹力的影响,所以元素距离目标点越远,速度越大;由于受到阻力的影响,所以元素每次运动都会有速度损耗

    //声明弹性距离
var len;
//声明弹性系数
var k;
//声明损耗系数
var z;
//获取当前样式值cur
cur = parseFloat(getCSS(obj,attr));
//更新弹性距离
len = target - cur;
//弹力影响
step += len*k;
//阻力影响
step = step*z;
obj.style[attr] = cur + step + 'px';
//当元素的步长值接近于0,并且弹性距离接近于0时,停止定时器
if(Math.round(step) == 0 && Math.round(len) == 0){
obj.style[attr] = target + 'px';
clearInterval(obj.timers[attr]);
obj.timers[attr] = 0;
fn && fn.call(obj);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id="btn">弹性运动</button>
<button id="reset">还原</button>
<div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<div style="background-color:red;width:1px;height:100px;position:absolute;left:300px;"></div>
<script>
function getCSS(obj,style){
if(window.getComputedStyle){
return getComputedStyle(obj)[style];
}
return obj.currentStyle[style];
}
function elasticMove(json){
var obj = json.obj;
var attr = json.attr;
//样式默认值为'left'
attr = attr || 'left';
var target = json.target;
//目标点默认值为'200'
target = Number(target) || 200;
var fn = json.fn;
//声明步长值
var step = 0;
//声明弹性距离
var len = target;
//声明弹性系数
var k=json.k;
//默认值为0.7
k = Number(k) || 0.7;
//声明损耗系数
var z=json.z;
//默认值为0.7
z = Number(z) || 0.7;
//声明当前值
var cur = parseFloat(getCSS(obj,attr));
//如果没有建立定时器对象,则在obj下建立定时器对象
if(!obj.timers){obj.timers = {};}
//清除定时器
if(obj.timers[attr]){return;}
//开启定时器
obj.timers[attr] = setInterval(function(){
//获取当前样式值cur
cur = parseFloat(getCSS(obj,attr));
//更新弹性距离
len = target - cur;
//弹力影响
step += len*k;
//阻力影响
step = step*z;
obj.style[attr] = cur + step + 'px';
//当元素的步长值接近于0,并且弹性距离接近于0时,停止定时器
if(Math.round(step) == 0 && Math.round(len) == 0){
obj.style[attr] = target + 'px';
clearInterval(obj.timers[attr]);
obj.timers[attr] = 0;
fn && fn.call(obj);
}
},20);
}
reset.onclick = function(){history.go();}
btn.onclick = function(){
elasticMove({obj:test,target:300})
}
</script>
</body>
</html>

弹性过界

  IE8-浏览器存在弹性过界问题,当宽度width或高度height等不能出现负值的样式出现负值时将会报错。所以,需要判断样式为高度或宽度时,样式值小于0时,等于0

  把弹性运动封装成elasticMove.js

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id="btn">弹性运动</button>
<button id="reset">还原</button>
<div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<script>
function getCSS(obj,style){
if(window.getComputedStyle){
return getComputedStyle(obj)[style];
}
return obj.currentStyle[style];
}
function elasticMove(json){
var obj = json.obj;
var attr = json.attr;
//样式默认值为'left'
attr = attr || 'left';
var target = json.target;
//目标点默认值为200
if(isNaN(Number(target))){
target = 200;
}else{
target = Number(target);
}
var fn = json.fn;
//声明步长值
var step = 0;
//声明弹性距离
var len = target;
//声明弹性系数
var k=json.k;
//默认值为0.7
k = Number(k) || 0.7;
//声明损耗系数
var z=json.z;
//默认值为0.7
z = Number(z) || 0.7;
//声明当前值
var cur = parseFloat(getCSS(obj,attr));
//如果没有建立定时器对象,则在obj下建立定时器对象
if(!obj.timers){obj.timers = {};}
//清除定时器
if(obj.timers[attr]){return;}
//开启定时器
obj.timers[attr] = setInterval(function(){
//获取当前样式值cur
cur = parseFloat(getCSS(obj,attr));
//更新弹性距离
len = target - cur;
//弹力影响
step += len*k;
//阻力影响
step = step*z;
//防止弹性过界
if((attr == 'height' || attr == 'width') && (cur + step) < 0){
obj.style[attr] = 0;
}else{
obj.style[attr] = cur + step + 'px';
}
//当元素的步长值接近于0,并且弹性距离接近于0时,停止定时器
if(Math.round(step) == 0 && Math.round(len) == 0){
obj.style[attr] = target + 'px';
clearInterval(obj.timers[attr]);
obj.timers[attr] = 0;
fn && fn.call(obj);
}
},20);
}
reset.onclick = function(){history.go();}
btn.onclick = function(){
elasticMove({obj:test,attr:'width',target:20})
}
</script>
</body>
</html>

弹性菜单

  下面利用封装的elasticMove.js来实现一个弹性菜单的应用

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<base href="http://www.cnblogs.com/" target="_blank">
<style>
#nav{
list-style:none;
padding: 0;
margin: 0 50px 0;
text-align:center;
color:white;
font-weight:bold;
background-color: #25517A;
cursor:pointer;
overflow:hidden;
width: 500px;
}
.navItem{
line-height: 30px;
float:left;
width:100px;
text-decoration: none;
color:inherit;
}
#navActive{
width: 100px;
height: 30px;
background-color: rgba(0,0,0,0.3);
position:absolute;
margin-top: -30px;
cursor:pointer;
}
</style>
</head>
<body>
<nav id="nav">
<a class="navItem" href="/">首页</a>
<a class="navItem" href="/pick/">精华</a>
<a class="navItem" href="/candidate/">候选</a>
<a class="navItem" href="/news/">新闻</a>
<a class="navItem" href="/following">关注</a>
</nav>
<div id="navActive"></div>
<script src="http://files.cnblogs.com/files/xiaohuochai/elasticMove.js"></script>
<script>
//navActive默认处于导航栏最左侧
navActive.style.left = nav.offsetLeft + 'px';
navActive.target = nav.getElementsByTagName('a')[0];
nav.onmousemove = function(e){
e = e || event;
navActive.target = e.target || e.srcElement;
elasticMove({obj:navActive,attr:'left',target:navActive.target.offsetLeft})
}
//点击navActive触发其所在位置的点击事件
navActive.onclick = function(e){
navActive.target.click();
}
</script>
</body>
</html>

弹性拖拽

  弹性运动的另一个常见应用是弹性拖拽效果。例如,iphone手机上滑屏时的缓动效果就是利用弹性拖拽实现的

  下面利用封装的elasticMove.js来实现一个弹性拖拽的应用

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
#box{
width: 200px;
height: 200px;
border: 2px solid black;
margin: 50px 0 0 100px;
cursor:pointer;
position:relative;
overflow:hidden;
}
#list{
width: 1000px;
height: 200px;
margin: 0;
padding: 0;
list-style:none;
text-align: center;
font-size:30px;
position:absolute;
left:0;
}
.listItem{
float: left;
width: 200px;
line-height: 200px;
}
</style>
</head>
<body>
<div id="box">
<ul id="list" class="clear">
<li class="listItem" style="background-color:lightblue">第一屏</li>
<li class="listItem" style="background-color:lightgreen">第二屏</li>
<li class="listItem" style="background-color:lightseagreen">第三屏</li>
<li class="listItem" style="background-color:lightgrey">第四屏</li>
<li class="listItem" style="background-color:lightcoral">第五屏</li>
</ul>
</div>
<script src="http://files.cnblogs.com/files/xiaohuochai/elasticMove.js"></script>
<script>
//表示当前屏幕处于第几屏,默认为第0屏
var index = 0;
//获取屏幕的宽度
var baseWidth = parseFloat(getCSS(box,'width'));
list.onmousedown = function(e){
e = e || event;
//获取元素距离定位父级的x轴及y轴距离
var x0 = this.offsetLeft;
//获取此时鼠标距离视口左上角的x轴距离
var x1 = e.clientX;
//如果拖拽时,弹性运动还没有走完,则拖拽操作无效
if(list.timers){
if(list.timers.left){
return;
}
}
document.onmousemove = function(e){
e = e || event;
//获取此时鼠标距离视口左上角的x轴距离
var x2 = e.clientX;
//计算此时元素应该距离视口左上角的x轴距离
var X = x0 + x2 - x1;
//将X的值赋给left,使元素移动到相应位置
list.style.left = X + 'px';
}
document.onmouseup = function(e){
//当鼠标抬起时,拖拽结束,则将onmousemove赋值为null即可
document.onmousemove = null;
//释放全局捕获
if(list.releaseCapture){
list.releaseCapture();
}
e = e || event;
var x3 = e.clientX;
//向右滑动
if(x3 > x1){
index--;
if(index < 0){
index = 0;
}
elasticMove({
obj:list,
target:-1*baseWidth*index
})
}
//向左滑动
if(x3 < x1)
{
index++;
if(index > 4){
index = 4;
}
elasticMove({
obj:list,
target:-1*baseWidth*index
})
}
}
//阻止默认行为
return false;
//IE8-浏览器阻止默认行为
if(list.setCapture){
list.setCapture();
}
}
//防止无关文字被选中
document.onmousedown = function(){
return false;
}
</script>
</body>
</html>