JavaScript运动框架

222 阅读4分钟

1. 运动所运用的属性

  • 移动:offsetTop, offsetLeft, style.top, style.left
  • 透明度:style.opacity
  • 宽高:offsetWidth, offsetHeight, style.widthstyle.height

1.1 offsetstyle的优缺点

offset系列:

  1. 没有opacity
  2. offsetWidth的值为元素总宽,横向递增会附加左右边距,横向递减又会抵消左右边距(offsetHeight同理)

style系列:

  1. 属性齐全,但只能获得行内样式
<style>
    #box {
        width: 200px;
        height: 200px;
        border: 1px solid #fff;
        background-color: #d60231;
        /* box-sizing: border-box; */
    }
</style>
    <div></div>
<script>
    var oDiv = document.getElementsByTagName('div')[0];
    setInterval(function() {
        oDiv.style.width = oDiv.offsetWidth - 2 + 'px'; // !元素保持不动,可以设置box-sizing解决
    }, 20);
</script>

1.2 使用计算属性获取样式

要点: window.getComputedStyle(el, null)[attr]

function getStyle(el, attr) {
    return window.getComputedStyle(el, null)[attr];
}

2. 匀速运动

2.1 位置匀速运动

匀速移动:元素最终位置(style.left)= 当前位置(el.offsetLeft) + 递增值(pace

判断方位:起始点 < 目标点 ? +pace : -pace

function move(el, target) {
    var current = el.offsetLeft; // 1. 获取元素当前位置
    pace = current < target ? 2 : -2; // 2. 判断递增or递减

    clearInterval(el.timer); // 3. 清除定时器,防止累加
    el.timer = setInterval(function () { // 4. 设置定时器
        current = el.offsetLeft; // 4.1 获取当前位置

        if (current === target) { // 4.2 查看是否达到目标点并清除定时器
            clearInterval(el.timer);
            return false;   // 4.3 达到目标点清除定时器并退出函数(不然会多加一次)
        }

        el.style.left = current + pace + 'px'; // 4.3 设置定位置元素移动
    }, 20);
}

2.2 透明度匀速运动

  • 由于JS处理浮点数的精度问题,需要以(100 - 0)的区间处理透明度
  • 虽然 String * 100 会自动转为Number,但使用parseFloat()是个好习惯
  • 可能出现0.07 * 100 = 7.000000000000001 这种情况,需要用 Math.round()处理下
function move(el, target) {
    var current = Math.round( getStyle(el, 'opacity') * 100 ); // 1. 获取元素透明度
    pace = current < target ? 2 : -2; // 2. 判断递增or递减
    
    clearInterval(el.timer); // 3. 清除定时器,防止累加
    el.timer = setInterval(function () { // 4. 设置定时器
        current = Math.round( getStyle(el, 'opacity') * 100 ); // 4.1 获取透明度
    
        if (current === target) { // 4.2 查看是否达到目标点并清除定时器
            clearInterval(el.timer);
            return false;   // 4.3 达到目标点清除定时器并退出函数(不然会多加一次)
        }
    
        el.style.opacity = (current + pace) / 100; // 4.3 缩小100倍,取消'px'
    }, 20);
}

3. 缓冲运动(常用)

缓冲要点: pace会随着距离的缩短而逐渐衰减,呈现先快后慢的趋势

  • 当前值(current)放入定时器实时计算
  • 递增值 pace = (target - current) / 10
  • 如果pace的值小于1的话,获取的元素值会向下取整,就等于没有递增,陷入死循环,所以pace需要取整
  • pace = pace > 0 ? Math.ceil(pace) : Math.floor(pace);
function move(el, target) {
    var current = 0,
        pace = 0;
    
    clearInterval(el.timer);
    el.timer = setInterval(function() {
        current = parseInt( getStyle(el, 'left') ); // 1. 获取当前值,去'px'
        pace = (target - current) / 10;  // 2. 计算出步长
        pace = pace > 0 ? Math.ceil(pace) : Math.floor(pace); // 3. 取整步长
        
        if(current === target) {   // 4. 判断是否达到目标点,终止定时器
            clearInterval(el.timer);
            return false;
        }
        
        el.style.left = current + pace + 'px'; // 3. 元素实际移动
    }, 20);  // 20是因为 1000 / 20 = 50 帧, 比较流畅
}

3.1 多元素运动

多个元素同时运动,互不影响

多物体要点:定时器捆绑到元素自身(之前的例子也都这么做的)

    clearInterval(el.timer);
    el.timer = setInterval(function () {}, 20);

3.2 多属性封装

属性分为两类:

  • 有单位:top, left, width, height
  • 无单位:opacity

多属性要点

  • 使用 Object 传参,for in循环属性值
  • 有单位、无单位分别处理
move(el, { 'left': 200 }, 20);
function move(el, paras, speed) {
    var target = 0,
        current = 0,
        pace = 0;
        
    clearInterval(el.timer);
    el.timer = setInterval(function() {
        for(var attr in paras) {
            target = paras[attr];  // 1. 获取目标值
            // 2. 判断并获取当前值
            if(attr === 'opacity') {
                current = Math.round( parseFloat( getStyle(el, attr) ) * 100 );
            } else {
                current = parseInt( getStyle(el, attr) );
            }
            // 3. 判断当前值是否达到目标值
            if(current === target) {
                clearInterval(el.timer);
                return false;
            }
            // 4. 计算出步长
            pace = (target - current) / (speed || 10);
            pace = pace > 0 ? Math.ceil(pace) : Math.floor(pace);
            // 5. 应用
            if(attr === 'opacity') {
                el.style[attr] = (current + pace) / 100;
            } else {
                el.style[attr] = current + pace + 'px';
            }
        }
    }, 20);
}

3.3 多属性互不干扰

弥补缺点: 如果多个属性同时运动的话,会出现最先完成的属性终止定时器的情况,影响到其他属性

要点: 使用isReach,各个属性运动前重置该属性(false),如果都达成目标则为(true),清除定时器

function move(el, paras, callback, speed) { // 1. 添加 callback 形参
    var target = 0,
        current = 0,
        pace = 0,
        isReach = false;
        
    clearInterval(el.timer);
    el.timer = setInterval(function() {
        isReach = true;
        
        for(var attr in paras) {
            // ......
            if(current !== target) {
                isReach = false;
            }
            // ......
        }
        
        if(isReach) {
            clearInterval(el.timer);
            return false;
        }
    }, 20);
}

3.4 链式运动

链式运动: 移动 --> 透明度 --> dispaly: none;

链式运动要点: 回调函数(异步改同步)

  • 增加了 callback参数
  • 使用 call改变 this指向方便使用
var oBox = document.getElementById('box');

move(oBox, { 'left': 200 }, function() {
    move(this, { 'opacity': 0 }, function() {
        this.style.display = 'none';
    }
});

function move(el, paras, speed, callback) { // 1. 添加 callback 形参
    // var target......
        
    clearInterval(el.timer);
    el.timer = setInterval(function() {
        isReach = true;
        for(var attr in paras) {
            // ......
        }
        if(isReach) {
            clearInterval(el.timer);
            callback && callback.call(el); // 2. 如果callback存在则运行
            return false;
        }
    }, 20);
}

8. IE兼容性

8.1 计算属性

function getStyle(el, attr) {
    if(el.currentStyle) {
        return el.currentStyle[attr];
    } else {
        return window.getComputedStyle(el, null)[attr];
    }
}

8.2 透明度

    el.style.filter = 'alpha(opacity=100)'; // IE

End

function move(el, paras, callback, speed) {
    var target = 0,
        current = 0,
        pace = 0,
        isReach = false;

    clearInterval(el.timer);
    el.timer = setInterval(function () {
        isReach = true
        for (var attr in paras) {
            target = paras[attr];

            if (attr === 'opacity') {
                current = Math.round(parseFloat(getStyle(el, attr)) * 100);
            } else {
                current = parseInt(getStyle(el, attr));
            }

            if (current !== target) {
                isReach = false;
            }

            pace = (target - current) / (speed || 10);
            pace = pace > 0 ? Math.ceil(pace) : Math.floor(pace);

            if (attr === 'opacity') {
                el.style[attr] = (current + pace) / 100;
            } else {
                el.style[attr] = current + pace + 'px';
            }
        }
        if (isReach) {
            clearInterval(el.timer);
            callback && callback.call(el);
            return false;
        }
    }, 20);
}