JavaScript 运动 01 —— 匀速运动

最近想写一个收缩展开的菜单特效,希望用原生的 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 函数,包括:

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

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容