模仿 velocity.js 實現(xiàn) DOM 動畫類庫(二)

separateValue

這次我們先來討論如何正確分離屬性值與單位,假設要實現(xiàn)下面的動畫:

<!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">點擊執(zhí)行動畫</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>

點擊動畫按鈕,同時改變透明度、寬度以及旋轉(zhuǎn)角度。

velocity中旋轉(zhuǎn)不是使用transform而必須使用rotateZ,少了Z也不行。

由于rotateZ這樣的動畫存在,所以不能單純從值中提取出單位,還要根據(jù)屬性名來獲取,比如rotateZ是無法獲取到開始值的,所以就額外處理一次,使用getUnitType返回預期的值。

function separateValue (property, value) {
    let unitType,
        numericValue
    // replace 是字符串的方法,如果是數(shù)值類型則沒有 replace 方法,所以先將 value 轉(zhuǎn)為字符串
    numericValue = value.toString().replace(/[%A-z]+$/, function(match) {
        // 將匹配到的字母作為單位
        unitType = match
        // 將屬性值中的字母都去掉,保留數(shù)字
        return ""
    })

    // 如果沒有獲取到單位,就根據(jù)屬性來獲取
    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]
    // 結(jié)束值與結(jié)束單位
    const endSeparatedValue = separateValue(property, propertiesMap[property])
    const endValue = parseFloat(endSeparatedValue[0]) || 0
    const endValueUnitType = endSeparatedValue[1]
    // 將結(jié)果保存到對象中
    propertiesContainer[property] = {
        startValue,
        endValue,
        unitType: endValueUnitType
    }
}

接下來就簡單了,只要在tick函數(shù)內(nèi)遍歷propertiesContainer獲取不同屬性的開始值與結(jié)束值計算得到當前值即可。

// 核心動畫函數(shù)
function tick () {
    // 當前時間
    let timeCurrent = (new Date).getTime()
    // 遍歷要執(zhí)行動畫的 element 元素,這里暫時只支持一個元素
    // 當前值
    // 如果 timeStart 是 undefined ,表示這是動畫的第一次執(zhí)行
    if (!timeStart) {
        timeStart = timeCurrent - 16
    }
    // 檢測動畫是否執(zhí)行完畢
    const percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1) 

    // 遍歷要改變的屬性值并一一改變
    for(let property in propertiesContainer) {
        // 拿到該屬性當前值,一開始是 startValue
        const tween = propertiesContainer[property]
        // 如果動畫執(zhí)行完成
        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)
        // 終止調(diào)用 tick
        if (percentComplete === 1) {
            isTicking = false
        }

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

透明度與寬度能夠正確處理,但是角度卻沒有正確處理,因為并沒有對rotateZ做特殊處理,實際并不能夠直接給 DOM 設置rotateZ屬性而需要設置transform屬性。

改變角度

在調(diào)用setPropertyValue時,傳入了(element, 'rotateZ', 'xxdeg'),為了職責分明,不在調(diào)用該函數(shù)前將rotateZ改變?yōu)?code>transform,而是在setPropertyValue函數(shù)內(nèi)部根據(jù)屬性來判斷究竟該怎么設置元素的屬性值。

function setPropertyValue(element, property, value) {
    let propertyName = property
    if (normalization[property]) {
        // 如果在 normalization 這個對象內(nèi),就表示這個屬性是需要經(jīng)過處理的
        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循環(huán)外即可。

最終代碼

;(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 是字符串的方法,如果是數(shù)值類型則沒有 replace 方法
        numericValue = value.toString().replace(/[%A-z]+$/, function(match) {
            unitType = match
            return ""
        })
        // 如果沒有獲取到單位,就根據(jù)屬性來獲取
        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 ]
    }
    /* ========================
     * 構造函數(shù)
    =========================*/
    function Animation (element) {
        this.element = element
    }
    // easing 緩動函數(shù)
    Animation.easing = {
        swing: function (a) {
            return .5 - Math.cos(a * Math.PI) / 2
        }
    }
    // 暴露的動畫接口
    Animation.prototype.animation = function (propertiesMap) {
        const element = this.element
        // 默認參數(shù)
        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]
            // 結(jié)束值
            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
        // 核心動畫函數(shù)
        function tick () {
            // 當前時間
            let timeCurrent = (new Date).getTime()
            // 遍歷要執(zhí)行動畫的 element 元素,這里暫時只支持一個元素
            // 當前值
            // 如果 timeStart 是 undefined ,表示這是動畫的第一次執(zhí)行
            if (!timeStart) {
                timeStart = timeCurrent - 16
            }
            // 檢測動畫是否執(zhí)行完畢
            const percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1) 
            // 遍歷要改變的屬性值并一一改變
            for(let property in propertiesContainer) {
                // 拿到該屬性當前值,一開始是 startValue
                const tween = propertiesContainer[property]
                // 如果動畫執(zhí)行完成
                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)
            }
            // 終止調(diào)用 tick
            if (percentComplete === 1) {
                isTicking = false
            }
            if (isTicking) {
                requestAnimationFrame(tick)
            }
        }
        tick()
    }
    // 暴露至全局
    window.Animation = Animation
})(window)

總結(jié)

這次主要是實現(xiàn)了分割值與單位,同時簡單的實現(xiàn)了同時改變多個屬性的動畫。仍存在很大缺陷,下篇筆記主要解決顏色值的改變與開始值結(jié)束值單位不一致這兩個問題。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 選擇qi:是表達式 標簽選擇器 類選擇器 屬性選擇器 繼承屬性: color,font,text-align,li...
    wzhiq896閱讀 1,806評論 0 2
  • 選擇qi:是表達式 標簽選擇器 類選擇器 屬性選擇器 繼承屬性: color,font,text-align,li...
    love2013閱讀 2,339評論 0 11
  • Core Animation其實是一個令人誤解的命名。你可能認為它只是用來做動畫的,但實際上它是從一個叫做Laye...
    小貓仔閱讀 3,803評論 1 4
  • 關于css3變形 CSS3變形是一些效果的集合,比如平移、旋轉(zhuǎn)、縮放和傾斜效果,每個效果都被稱作為變形函數(shù)(Tra...
    hopevow閱讀 6,410評論 2 13