JavaScript 运动 01 —— 匀速运动

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

运动

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

JavaScript 实现运动

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

场景搭建

先来搭建运动场景:

JavaScript 运动 01 —— 匀速运动_第1张图片
匀速运动场景.png

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




    
    匀速运动
    


    
小滑块

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);
}

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

...

    
小滑块
...

效果如图所示:

JavaScript 运动 01 —— 匀速运动_第2张图片
开始运动.gif

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

  • 重复点击开始按钮,速度越来越快
  • 改变速度值后,物体可能超出边界

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

JavaScript 运动 01 —— 匀速运动_第3张图片
重复点击速度加快.gif

2)改变速度后超出边界

...
const ele = document.getElementById("inner");
function start(){
    const target = (600 - ele.offsetWidth);
    animate(ele,"left",target,21);
}
...

JavaScript 运动 01 —— 匀速运动_第4张图片
改变速度后超出边界.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);
}

看下效果:

JavaScript 运动 01 —— 匀速运动_第5张图片
解决重复点击速度加快.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);
}

现在看下效果:

JavaScript 运动 01 —— 匀速运动_第6张图片
解决改变速度物体超出边界.gif

反方向运动

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

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

调用运动函数:

...

...

效果图:

JavaScript 运动 01 —— 匀速运动_第7张图片
反向运动.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);
}

调用运动函数:

...

...

效果如下:

JavaScript 运动 01 —— 匀速运动_第8张图片
透明度特殊服务.gif

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

总结

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

  • 重复点击速度加快问题:通过每次调用函数时清除定时器解决
  • 运动越界问题:使用绝对值进行判断处理
  • 反方向问题:根据当前位置和目标位置的差值判断速度方向
  • 透明度问题:对透明度进行特殊处理
  • 小数精度问题:将透明度转换为整数进行处理

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

  • 多个属性值同时运动
  • 链式运动
  • 利用 async/await 和 Promise 解决链式运动多层回调问题

完。

你可能感兴趣的:(JavaScript 运动 01 —— 匀速运动)