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

前面的话

  缓冲运动指的是减速运动,减速到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>    

弹性运动

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

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

  【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="https://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="https://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>

posted @ 2016-10-20 18:05  小火柴的蓝色理想  阅读(4604)  评论(7编辑  收藏  举报