模仿 velocity.js 實(shí)現(xiàn) DOM 動(dòng)畫類庫(一)

動(dòng)畫的原理就是每隔一段時(shí)間改變畫面,這個(gè)時(shí)間小到眼睛無法識(shí)別,所以看起來就像是畫面在動(dòng)。

DOM 動(dòng)畫也是一樣的,每隔一定時(shí)間就改變 DOM 的某個(gè) CSS 屬性值,比如寬度、高度透明度等等,從而實(shí)現(xiàn)了我們所看到的 DOM 動(dòng)畫。

當(dāng)然實(shí)現(xiàn)一個(gè) DOM 動(dòng)畫類庫并不是很困難,但一開始就要很完善很完美就很困難了,所以我準(zhǔn)備從最簡單的入手,先實(shí)現(xiàn)透明度動(dòng)畫,再實(shí)現(xiàn)透明度與寬度同時(shí)動(dòng)畫。

為什么先實(shí)現(xiàn)透明度呢?因?yàn)?code>opacity的值沒有單位,不像widthpx單位,要改變width就要先分割值與單位,將值做改變后加上單位;也不像background-color一樣可能是十六位進(jìn)制#fff也可能是rgb(0, 0, 0)需要額外處理的。

流程圖

當(dāng)然上面說的原理太虛無縹緲了,如果用流程圖說明,大概就是這樣的:

偽代碼流程圖

實(shí)際代碼

落實(shí)到代碼,總共有 5 個(gè)核心函數(shù)

  • 獲取開始值 getPropertyValue => 步驟2
  • 分割屬性值與單位 separateValue => 步驟2
  • 動(dòng)畫函數(shù) tick => 步驟 4
  • 緩動(dòng)函數(shù)計(jì)算當(dāng)前值 easing => 步驟 4.1
  • 改變 DOM 屬性 setPropertyValue => 步驟 4.2

之后的代碼都以實(shí)現(xiàn)下面的div透明度變化為目標(biāo)。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>測試</title>
    <script src="./src/fakeVelocity.js"></script>
    <style>
        #div {
            width: 300px;
            height: 180px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="div"></div>
    <button id="run">點(diǎn)擊執(zhí)行動(dòng)畫</button>
    <script>
        window.onload = function () {
            document.querySelector('#run').onclick = function () {
                Animation(document.querySelector('#div'), {
                    opacity: 0.5
                })
            }
        }
    </script>
</body>
</html>

getPropertyValue

首先要獲取到#div的初始透明度值,div.style.opacity?結(jié)果是空,這里需要用到getComputedStylegetPropertyValue。

function getPropertyValue (element, property) {
    return window.getComputedStyle(element, null).getPropertyValue(property)
}

getPropertyValuegetComputedStyle返回對(duì)象的方法,所以不用擔(dān)心會(huì)和我們自己定義的getPropertyValue沖突。

function Animation (element, propertiesMap) {
    let property,
         startValue,
         endValue,
         unitType
    for(property in propertiesMap) {
        startValue = getPropertyValue(element, propertiesMap)
        endValue = propertiesMap[property]
        console.log(startValue, endValue)
    }
}

此時(shí)能夠正確獲取到動(dòng)畫開始值與動(dòng)畫結(jié)束值分別為1、0.5

separateValue

當(dāng)然這個(gè)函數(shù)現(xiàn)在并沒有用,因?yàn)?code>opacity沒有單位,但是我們知道后續(xù)需要增加有單位的值,所以先將這個(gè)函數(shù)聲明好。

function separateValue (property, value) {
    return [value, '']
}

調(diào)用該函數(shù)后,將返回?cái)?shù)組,第一個(gè)元素為值,第二個(gè)元素為單位。

function Animation(element, propertiesMap) {
    let property,
        startValue,
        endValue,
        unitType
    for(property in propertiesMap) {
        startValue = getPropertyValue(element, property)
        // 分割值與單位
        const separatedValue = separateValue(property, startValue)
        // 2、真正的開始值
        startValue = separatedValue[0]
        // 2、單位
        unitType = separatedValue[1]
        // 2、結(jié)束值
        endValue = propertiesMap[property]
    }
}

OK,回過頭看看流程圖,現(xiàn)在到了第三步,準(zhǔn)備執(zhí)行動(dòng)畫函數(shù)tick了。

tick

先來聲明好這個(gè)動(dòng)畫函數(shù),前面也提到了,在這個(gè)函數(shù)內(nèi)部,每次調(diào)用都會(huì)獲取到當(dāng)前時(shí)間,并與調(diào)用前聲明的startTime進(jìn)行比對(duì),如果currentTime - startTime >= duration就結(jié)束動(dòng)畫,否則就再調(diào)用一次tick。

duration就是動(dòng)畫持續(xù)時(shí)間,通過配置項(xiàng)傳入,這里暫時(shí)寫死在代碼中。

const opts = {
    duration: 400
}
3、準(zhǔn)備調(diào)用動(dòng)畫函數(shù)
const startTime = new Date().getTime()
function tick () {
    // 這次調(diào)用的當(dāng)前時(shí)間
    let currentTime = new Date().getTime()
    // 5、計(jì)算動(dòng)畫時(shí)間是否結(jié)束 (>= duration)
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 當(dāng)前透明度的值,準(zhǔn)備用來修改 DOM 對(duì)應(yīng)屬性
    let currentValue
    // 如果已經(jīng)執(zhí)行的動(dòng)畫時(shí)間大于動(dòng)畫應(yīng)該執(zhí)行的時(shí)間,就將值直接賦為結(jié)束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        // 4.1 計(jì)算當(dāng)前值應(yīng)該是多少
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
}
// 4、調(diào)用動(dòng)畫函數(shù)
tick()

先看5、計(jì)算動(dòng)畫時(shí)間是否結(jié)束,這里并沒有按照之前說的計(jì)算方法currentTime - startTime >= duration計(jì)算動(dòng)畫是否結(jié)束,而是使用了比較

  • (currentTime - startTime) / duration
  • 1

這兩個(gè)值的大小,取更小的那個(gè)值。當(dāng)然currentTime - startTime大于等于duration時(shí),才會(huì)是1更小,道理是相同的,不過因?yàn)?/p>

(currentTime - startTime) / duration

這個(gè)值需要用在緩動(dòng)函數(shù)內(nèi),所以就不做兩次處理了,當(dāng)然這樣也是可以的,但是沒必要不是嗎?

if ((currentTime - startTime) >= duration) {
    percentComplete = 1
} else {
    percentComplete = (currentTime - startTime) / duration
}

緩動(dòng)函數(shù)

這個(gè)就是直接拿現(xiàn)成的算法來用了,上面代碼是這樣使用的:

currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)

重點(diǎn)在后面的easing['swing'](percentComplete),很容易理解,easing是一個(gè)對(duì)象,有swing屬性,并且對(duì)應(yīng)的值是一個(gè)函數(shù)。

// easing 緩動(dòng)函數(shù)
easing = {
    swing: function (a) {
        return .5 - Math.cos(a * Math.PI) / 2
    },
    Sine: function (p) {
        return 1 - Math.cos(p * Math.PI / 2)
    },
    Circ: function (p) {
        return 1 - Math.sqrt(1 - p * p)
    }
}

這么做的好處很明顯,如果我不想用swing這個(gè)緩動(dòng)函數(shù)而想換一個(gè),這樣就可以:

easing[opts.easing](percentComplete)

opts.easing只要傳不同的字符串,就能夠直接調(diào)用對(duì)應(yīng)的函數(shù),而且還可以讓用戶自己拓展easing這個(gè)對(duì)象,只要opts.easing能夠?qū)?yīng)上就可以了。

這其實(shí)就是策略模式。

結(jié)束動(dòng)畫調(diào)用

上面的tick函數(shù)只會(huì)執(zhí)行一次,因?yàn)檫€沒有用到setInterval或者requestAnimationFrame來重復(fù)調(diào)用tick函數(shù)。

只需要在tick函數(shù)最后面,調(diào)用requestAnimationFrame(tick)即可,不過要加一個(gè)結(jié)束條件,就是percentComplete !== 1

function tick () {
    let currentTime = new Date().getTime()
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 當(dāng)前透明度的值,準(zhǔn)備用來修改 DOM 對(duì)應(yīng)屬性
    let currentValue
    // 如果已經(jīng)執(zhí)行的動(dòng)畫時(shí)間大于動(dòng)畫應(yīng)該執(zhí)行的時(shí)間,就將值直接賦為結(jié)束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
    // 6、結(jié)束動(dòng)畫條件
    if (percentComplete !== 1) {
        requestAnimationFrame(tick)
    }
}

現(xiàn)在打開控制臺(tái),點(diǎn)擊按鈕執(zhí)行動(dòng)畫,就能看到打印1 ~ 0.5逐漸變化的過程,表示成功?,F(xiàn)在就差最后一步,將這個(gè)值賦給 DOM 對(duì)應(yīng)屬性。

setPropertyValue

這個(gè)就簡單了,

function setPropertyValue (element, property, value) {
    element.style[property] = value
}

所以在tick函數(shù)內(nèi)這樣調(diào)用該函數(shù):

function tick () {
    let currentTime = new Date().getTime()
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 當(dāng)前透明度的值,準(zhǔn)備用來修改 DOM 對(duì)應(yīng)屬性
    let currentValue
    // 如果已經(jīng)執(zhí)行的動(dòng)畫時(shí)間大于動(dòng)畫應(yīng)該執(zhí)行的時(shí)間,就將值直接賦為結(jié)束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
    // 4.2、改變 dom 的屬性值
    setPropertyValue(element, property, currentValue + unitType)
    // 6、終止調(diào)用 tick
    if (percentComplete !== 1) {
        requestAnimationFrame(tick)
    }
}

再重新刷新頁面,點(diǎn)擊按鈕,看看能否正確改變透明度?

總結(jié)

真正的velocity.js源碼有 4000+ 行,即使是最初的版本也有 2000+,而我們自己實(shí)現(xiàn)的僅僅有 60+,所以可想而知有多簡陋,不過千里之行,始于足下,能夠開始,就是進(jìn)步。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 什么時(shí)候適合用jQuery DOM操作較多、簡單的AJAX、需要兼容多款瀏覽器 什么時(shí)候不用jQuery 頁面交互...
    yuhuan121閱讀 415評(píng)論 0 1
  • JQuery是什么? jQuery就是javascript的一個(gè)庫,把我們常用的一些功能進(jìn)行了封裝,方便我們來調(diào)用...
    阿魯提爾閱讀 396評(píng)論 0 3
  • 隱藏元素的hide方法 讓頁面上的元素不可見,一般可以通過設(shè)置css的display為none屬性。但是通過css...
    老夫撩發(fā)少年狂閱讀 1,194評(píng)論 0 2
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計(jì)者無需花費(fèi)時(shí)間糾纏JS復(fù)雜的高級(jí)特性。 1....
    LaBaby_閱讀 1,506評(píng)論 0 2
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計(jì)者無需花費(fèi)時(shí)間糾纏JS復(fù)雜的高級(jí)特性。 1....
    LaBaby_閱讀 1,274評(píng)論 0 1

友情鏈接更多精彩內(nèi)容