動(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的值沒有單位,不像width有px單位,要改變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é)果是空,這里需要用到getComputedStyle和getPropertyValue。
function getPropertyValue (element, property) {
return window.getComputedStyle(element, null).getPropertyValue(property)
}
getPropertyValue是getComputedStyle返回對(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)步。