模仿 velocity.js 实现 DOM 动画类库(二)

separateValue

这次我们先来讨论如何正确分离属性值与单位,假设要实现下面的动画:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>测试</title>
    <script src="./src/fakeVelocity.js"></script>
    <style>
        #demo {
            width: 300px;
            height: 180px;
            background-color: red;
            transform: rotate(30deg)
        }
    </style>
</head>
<body>
    <div id="demo"></div>
    <button id="run">点击执行动画</button>
    <script>
        window.onload = function () {
            document.querySelector('#run').onclick = function () {
                const animateEl = new Animation(document.querySelector('#demo'))
                animateEl.animation({
                    opacity: 0.5,
                    width: '300px',
                    rotateZ: '90deg'
                })
            }
        }
    </script>
</body>
</html>

点击动画按钮,同时改变透明度、宽度以及旋转角度。

velocity中旋转不是使用transform而必须使用rotateZ,少了Z也不行。

由于rotateZ这样的动画存在,所以不能单纯从值中提取出单位,还要根据属性名来获取,比如rotateZ是无法获取到开始值的,所以就额外处理一次,使用getUnitType返回预期的值。

function separateValue (property, value) {
    let unitType,
        numericValue
    // replace 是字符串的方法,如果是数值类型则没有 replace 方法,所以先将 value 转为字符串
    numericValue = value.toString().replace(/[%A-z]+$/, function(match) {
        // 将匹配到的字母作为单位
        unitType = match
        // 将属性值中的字母都去掉,保留数字
        return ""
    })

    // 如果没有获取到单位,就根据属性来获取
    function getUnitType (property) {
        if (/^(rotate|skew)/i.test(property)) {
            // 这两个属性值单位是 deg ,有点特殊
            return "deg"
        } else if (/(^(scale|scaleX|scaleY|scaleZ|opacity|alpha|fillOpacity|flexGrow|flexHeight|zIndex|fontWeight)$)|color/i.test(property)) {
            // 这些属性值都是没有单位的
            return ""
        } else {
            // 如果都没有匹配到,就默认是 px
            return "px"
        }
    }

    if (!unitType) {
        unitType = getUnitType(property)
    }

    return [ numericValue, unitType ]
}

多个属性值变化

之前使用全局变量保存propertystartValueendValueunitType,如果要改变多个属性,就必须使用到对象了,对象上每一个属性都有这些值。

let propertiesContainer = {}
for(let property in propertiesMap) {
    // 拿到开始值与开始单位
    const startSeparatedValue = separateValue(property, getPropertyValue(element, property))
    const startValue = parseFloat(startSeparatedValue[0])
    const startValueUnitType = startSeparatedValue[1]
    // 结束值与结束单位
    const endSeparatedValue = separateValue(property, propertiesMap[property])
    const endValue = parseFloat(endSeparatedValue[0]) || 0
    const endValueUnitType = endSeparatedValue[1]
    // 将结果保存到对象中
    propertiesContainer[property] = {
        startValue,
        endValue,
        unitType: endValueUnitType
    }
}

接下来就简单了,只要在tick函数内遍历propertiesContainer获取不同属性的开始值与结束值计算得到当前值即可。

// 核心动画函数
function tick () {
    // 当前时间
    let timeCurrent = (new Date).getTime()
    // 遍历要执行动画的 element 元素,这里暂时只支持一个元素
    // 当前值
    // 如果 timeStart 是 undefined ,表示这是动画的第一次执行
    if (!timeStart) {
        timeStart = timeCurrent - 16
    }
    // 检测动画是否执行完毕
    const percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1) 

    // 遍历要改变的属性值并一一改变
    for(let property in propertiesContainer) {
        // 拿到该属性当前值,一开始是 startValue
        const tween = propertiesContainer[property]
        // 如果动画执行完成
        if (percentComplete === 1) {
            currentValue = tween.endValue
        } else {
            currentValue = parseFloat(tween.startValue) + ((tween.endValue - tween.startValue) * Animation.easing['swing'](percentComplete))
            tween.currentValue = currentValue
        }
        // 改变 dom 的属性值
        setPropertyValue(element, property, currentValue + tween.unitType)
        // 终止调用 tick
        if (percentComplete === 1) {
            isTicking = false
        }

        if (isTicking) {
            requestAnimationFrame(tick)
        }
    }
}

透明度与宽度能够正确处理,但是角度却没有正确处理,因为并没有对rotateZ做特殊处理,实际并不能够直接给 DOM 设置rotateZ属性而需要设置transform属性。

改变角度

在调用setPropertyValue时,传入了(element, 'rotateZ', 'xxdeg'),为了职责分明,不在调用该函数前将rotateZ改变为transform,而是在setPropertyValue函数内部根据属性来判断究竟该怎么设置元素的属性值。

function setPropertyValue(element, property, value) {
    let propertyName = property
    if (normalization[property]) {
        // 如果在 normalization 这个对象内,就表示这个属性是需要经过处理的
        propertyName = 'transform'
    }
}

有哪些属性是使用transform设置的呢,在源码 499 行附近

  • rotate(X|Y|Z)
  • scale(X|Y|Z)
  • skew(X|Y)
  • translate(X|Y|Z)
function setPropertyValue (element, property, value) {
    let propertyName = property
    /********************
      声明需要额外处理的属性
    *********************/
    const transformProperties = [ "translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "scaleZ", "skewX", "skewY", "rotateX", "rotateY", "rotateZ" ]
    const Normalizations = {
        registered: {}
    }
    for(let i = 0, len = transformProperties.length; i < len; i++) {
        const transformName = transformProperties[i]
        Normalizations.registered[transformName] = function (propertyValue) {
            return transformName + '(' + propertyValue + ')'
        }
    }
    let propertyValue = value
    // 判断是否需要额外处理
    if (Normalizations.registered[property]) {
        propertyName = 'transform'
        propertyValue = Normalizations.registered[property](value)
    }
    console.log(propertyName, propertyValue)
    element.style[propertyName] = propertyValue
}

能够正确动画,但是却很卡。。不过只需要将判断是否终止动画tick的判断拿到for..in循环外即可。

最终代码

;(function (window) {
    /********************
      声明需要额外处理的属性
    *********************/
    const transformProperties = [ "translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "scaleZ", "skewX", "skewY", "rotateX", "rotateY", "rotateZ" ]
    const Normalizations = {
        registered: {}
    }
    // 如果这个属性是需要额外处理的
    for(let i = 0, len = transformProperties.length; i < len; i++) {
        const transformName = transformProperties[i]
        Normalizations.registered[transformName] = function (propertyValue) {
            return transformName + '(' + propertyValue + ')'
        }
    }
    // 获取指定 dom 的指定属性值
    function getPropertyValue (element, property) {
        return window.getComputedStyle(element, null).getPropertyValue(property)
    }
    // 给指定 dom 设置值
    function setPropertyValue (element, property, value) {
        let propertyName = property
        let propertyValue = value
        // 判断是否需要额外处理
        if (Normalizations.registered[property]) {
            propertyName = 'transform'
            propertyValue = Normalizations.registered[property](value)
        }
        element.style[propertyName] = propertyValue
    }
    // 分割值与单位
    function separateValue (property, value) {
        // 只处理两种简单的情况,没有单位和单位为 px
        let unitType,
            numericValue
        // replace 是字符串的方法,如果是数值类型则没有 replace 方法
        numericValue = value.toString().replace(/[%A-z]+$/, function(match) {
            unitType = match
            return ""
        })
        // 如果没有获取到单位,就根据属性来获取
        function getUnitType (property) {
            if (/^(rotate|skew)/i.test(property)) {
                // 这两个属性值单位是 deg ,有点特殊
                return "deg"
            } else if (/(^(scale|scaleX|scaleY|scaleZ|opacity|alpha|fillOpacity|flexGrow|flexHeight|zIndex|fontWeight)$)|color/i.test(property)) {
                // 这些属性值都是没有单位的
                return ""
            } else {
                // 如果都没有匹配到,就默认是 px
                return "px"
            }
        }
        if (!unitType) {
            unitType = getUnitType(property)
        }
        return [ numericValue, unitType ]
    }
    /* ========================
     * 构造函数
    =========================*/
    function Animation (element) {
        this.element = element
    }
    // easing 缓动函数
    Animation.easing = {
        swing: function (a) {
            return .5 - Math.cos(a * Math.PI) / 2
        }
    }
    // 暴露的动画接口
    Animation.prototype.animation = function (propertiesMap) {
        const element = this.element
        // 默认参数
        const opts = {
            duration: 400
        }
        // 保存要改变的属性集合
        let propertiesContainer = {}
        for(let property in propertiesMap) {
            // 拿到开始值
            const startSeparatedValue = separateValue(property, getPropertyValue(element, property))
            const startValue = parseFloat(startSeparatedValue[0]) || 0
            const startValueUnitType = startSeparatedValue[1]
            // 结束值
            const endSeparatedValue = separateValue(property, propertiesMap[property])
            const endValue = parseFloat(endSeparatedValue[0]) || 0
            const endValueUnitType = endSeparatedValue[1]

            propertiesContainer[property] = {
                startValue,
                endValue,
                unitType: endValueUnitType
            }
        }
        let timeStart
        // 终止动画标志
        let isTicking = true
        // 核心动画函数
        function tick () {
            // 当前时间
            let timeCurrent = (new Date).getTime()
            // 遍历要执行动画的 element 元素,这里暂时只支持一个元素
            // 当前值
            // 如果 timeStart 是 undefined ,表示这是动画的第一次执行
            if (!timeStart) {
                timeStart = timeCurrent - 16
            }
            // 检测动画是否执行完毕
            const percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1) 
            // 遍历要改变的属性值并一一改变
            for(let property in propertiesContainer) {
                // 拿到该属性当前值,一开始是 startValue
                const tween = propertiesContainer[property]
                // 如果动画执行完成
                if (percentComplete === 1) {
                    currentValue = tween.endValue
                } else {
                    currentValue = parseFloat(tween.startValue) + ((tween.endValue - tween.startValue) * Animation.easing['swing'](percentComplete))
                    tween.currentValue = currentValue
                }
                // 改变 dom 的属性值
                setPropertyValue(element, property, currentValue + tween.unitType)
            }
            // 终止调用 tick
            if (percentComplete === 1) {
                isTicking = false
            }
            if (isTicking) {
                requestAnimationFrame(tick)
            }
        }
        tick()
    }
    // 暴露至全局
    window.Animation = Animation
})(window)

总结

这次主要是实现了分割值与单位,同时简单的实现了同时改变多个属性的动画。仍存在很大缺陷,下篇笔记主要解决颜色值的改变与开始值结束值单位不一致这两个问题。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,569评论 18 139
  • 选择qi:是表达式 标签选择器 类选择器 属性选择器 继承属性: color,font,text-align,li...
    wzhiq896阅读 1,725评论 0 2
  • 选择qi:是表达式 标签选择器 类选择器 属性选择器 继承属性: color,font,text-align,li...
    love2013阅读 2,300评论 0 11
  • Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Laye...
    小猫仔阅读 3,675评论 1 4
  • 关于css3变形 CSS3变形是一些效果的集合,比如平移、旋转、缩放和倾斜效果,每个效果都被称作为变形函数(Tra...
    hopevow阅读 6,310评论 2 13