JavaScript

JavaScript 运动 01 —— 匀速运动

2017-05-22  本文已影响75人  柏丘君

最近想写一个收缩展开的菜单特效,希望用原生的 JavaScript 实现,不用 jQuery 或者 CSS3,思来想去居然毫无头绪,然后想起了以前看过的运动系列教程,于是又从头看了一遍,大体掌握了使用 JavaScript 编写一些常用运动的方式。这系列的博文就是我学习过程中的一些总结。

运动

物理学公式告诉俺们:路程 = 速度(平均速度) * 时间,就是在某一个时间段内,用某个速度走完某段路程。完成一项运动,路程、时间和速度这三个要素都不可或缺。

JavaScript 实现运动

JavaScript 实现运动的原理,就是通过定时器不断改变元素的位置,直至到达目标点后停止运动。通常,要让元素动起来,我们会通过改变元素的 left 和 top 值来改变元素的相对位置。这两句话看似简单,实际上有很多细节需要我们处理,其中也涉及到一些数学和物理的知识。常见的运动形式有:匀速运动、缓冲运动、弹性运动和碰撞运动。本系列博文将依次总结这些运动,首先是匀速运动。

场景搭建

先来搭建运动场景:

匀速运动场景.png

我们准备让小滑块从大盒子左侧匀速运动到大盒子右侧,下面是基础的布局代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>匀速运动</title>
    <style>
        #par{
            width: 600px;
            height: 300px;
            border: 1px solid #333;
            margin:50px auto;
            position: relative;
            text-align: center;
            padding-top: 10px;
        }

        #inner{
            width: 100px;
            height: 100px;
            background: orange;
            position: absolute;
            left: 0;
            top: 50%;
            margin-top: -50px;
            text-align: center;
            line-height: 100px;
        }
    </style>
</head>
<body>
    <div id="par">
        <button>开始</button>
        <div id="inner">小滑块</div>
    </div>
</body>
</html>

JavaScript代码

要完成运动效果,我们需要这些要素:

在运动过程中,元素的某个属性是不断变化的,首先需要一个函数来获取元素的属性:

function getCurrentStyle(ele,attr = ""){
    return ele.currentStyle?ele.currentStyle[attr]:getComputedStyle(ele,false)[attr];
}

接下来编写运动函数:

function animate(ele = null,attr = "",target = 0,speed = 0){
    ele.timer = setInterval(()=>{
        // 获取当前的样式
        let currentStyle = Number.parseInt(getCurrentStyle(ele,attr));
        // 运动到目标点后清除定时器
        if(currentStyle >= target){
            clearInterval(ele.timer)
        }else{
            // 根据当前样式动态改变物体的样式
            ele.style[attr] = (currentStyle + speed) + "px";
        }
    },30);
}

给开始按钮添加事件,调用运动函数:

...
<body>
    <div id="par">
        <button onclick = "start()">开始</button>
        <div id="inner">小滑块</div>
    </div>
</body>
<script src="animate.js"></script>
<script>
    const ele = document.getElementById("inner");
    function start(){
        const target = (600 - ele.offsetWidth);
        animate(ele,"left",target,10);
    }

</script>
...

效果如图所示:

开始运动.gif

现在,我们让小滑块动起来了,当然,还有一些问题需要处理:

看一下效果:
1)重复点击,速度越来越快

重复点击速度加快.gif

2)改变速度后超出边界

...
const ele = document.getElementById("inner");
function start(){
    const target = (600 - ele.offsetWidth);
    animate(ele,"left",target,21);
}
...
改变速度后超出边界.gif
原因分析:
1)关于速度越来越快的问题,是因为每次点击都会开启一个定时器,导致定时器中的回调函数多次执行,因此速度就越来越快,解决方案是函数一开始执行时就清除定时器
function animate(ele = null,attr = "",target = 0,speed = 0){
    // 清除定时器
    clearInterval(ele.timer);
    ele.timer = setInterval(()=>{
        // 获取当前的样式
        let currentStyle = Number.parseInt(getCurrentStyle(ele,attr));
        // 运动到目标点后清除定时器
        if(currentStyle >= target){
            clearInterval(ele.timer)
        }else{
            // 根据当前样式动态改变物体的样式
            ele.style[attr] = (currentStyle + speed) + "px";
        }
    },30);
}

看下效果:

解决重复点击速度加快.gif
2)关于改变速度后物体超出边界,是由于当前样式(currentStyle)加上速度(speed)后,不一定刚好等于目标距离,而我们判断运动停止的条件是当前样式 (currentStyle)大于等于目标距离(target),这个算法并不能限制物体刚好达到边界。
为什么会超出边界呢?我们拿速度 21 举例:
运动次数 位置 目标距离
1 21 550
2 42 550
... ... 550
26 546 550
27 561 550

当进行第26次运动时,小滑块的位置是546,由于546<550,因此小滑块会以继续以21的速度向前运动,直到进行到第27次运动,此时小滑块的位置大于目标距离,运动停止。
正确的食用方式:
事实上,当小滑块进行第26次运动以后,他将无法再进行一次完整的运动了。此时小滑块右侧到边界的距离小于一个速度值。因此我们对代码进行如下修改:

function animate(ele = null,attr = "",target = 0,speed = 0){
    // 清除定时器
    clearInterval(ele.timer);
    ele.timer = setInterval(()=>{
        // 获取当前的样式
        let currentStyle = Number.parseInt(getCurrentStyle(ele,attr));
        // 运动到目标点后清除定时器
        if(Math.abs(target - currentStyle) < Math.abs(speed)){
            ele.style[attr] = target + "px";
            clearInterval(ele.timer);
        }else{
            // 根据当前样式动态改变物体的样式
            ele.style[attr] = (currentStyle + speed) + "px";
        }
    },30);
}

现在看下效果:

解决改变速度物体超出边界.gif

反方向运动

如果想要小滑块从右向左运动呢?这时就需要反向的速度,使小滑块的 left 值不断变小。
CSS 代码:

...
#inner{
...
    left: 500px;
...
}

调用运动函数:

...
<script>
    const ele = document.getElementById("inner");
    function start(){
        const target = 0;
        // 负值表示反向运动
        animate(ele,"left",target,-21);
    }
</script>
...

效果图:

反向运动.gif

从实际应用的角度考虑,我们可能不太愿意指定速度的方向,只希望指定速度的值,因此我们对 animate 函数做一些修改,在函数内部判断速度的方向:

function animate(ele = null,attr = "",target = 0,speed = 0){
    // 清除定时器
    clearInterval(ele.timer);
    let currentStyle = Number.parseInt(getCurrentStyle(ele,attr));
    // 根据当前样式值和目标位置的差值判断速度方向
    if(currentStyle - target > 0){
        speed = -speed;
    }
    ele.timer = setInterval(()=>{
        // 获取当前的样式
        let currentStyle = Number.parseInt(getCurrentStyle(ele,attr));
        // 运动到目标点后清除定时器
        if(Math.abs(target - currentStyle) < Math.abs(speed)){
            ele.style[attr] = target + "px";
            clearInterval(ele.timer);
        }else{
            // 根据当前样式动态改变物体的样式
            ele.style[attr] = (currentStyle + speed) + "px";
        }
    },30);
}

透明度处理

上边的运动属性都是带 px 单位的,而透明度是没有单位的,因此需要特殊服务。改变小滑块的样式:

#inner{
...
    opacity: 0.3;
}

修改 animate 函数,增加透明度判断:

function animate(ele = null,attr = "",target = 0,speed = 0){
    // 清除定时器
    clearInterval(ele.timer);
    // 获取当前的样式
    let currentStyle = (attr === "opacity")?(Number.parseInt(Number.parseFloat(getCurrentStyle(ele,attr))*100)):Number.parseInt(getCurrentStyle(ele,attr));

    // 如果改变的样式是 opacity,target乘以100
    if(attr === "opacity"){
        target *= 100;
    }

    // 根据当前样式值和目标位置的差值判断速度方向
    if(currentStyle - target > 0){
        speed = -speed;
    }

    ele.timer = setInterval(()=>{
        // 运动到目标点后清除定时器
        if(Math.abs(target - currentStyle) < Math.abs(speed)){
            ele.style[attr] = (attr === "opacity")? target / 100 : target + "px";
            clearInterval(ele.timer);
        }else{
            // 根据当前样式动态改变物体的样式
            ele.style[attr] = (attr === "opacity")?( currentStyle + speed)/100:(currentStyle + speed) + "px";
            currentStyle += speed;
        }
    },30);
}

调用运动函数:

...
<script>
    const ele = document.getElementById("inner");
    function start(){
        const target = 0;
        animate(ele,"opacity",1,5);
    }
</script>
...

效果如下:

透明度特殊服务.gif

为何需要将透明度的值乘以100?
因为浮点数并不是精确存储的,我们通过 getCurrentStyle 方法获取的透明度是浮点数,因此在运算的过程中是不精确的,所以需要将透明度转为整数进行计算,在设置样式时再除以100。在后面的其他运动形式中,还会看到很多这样的处理。

总结

这篇文章开始,我们初步接触了匀速运动,并解决了以下问题:

下篇文章,我们将在匀速运动的基础上,继续完善 animate 函数,包括:

完。

上一篇 下一篇

猜你喜欢

热点阅读