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 ]
}
多個屬性值變化
之前使用全局變量保存property
、startValue
、endValue
和unitType
,如果要改變多個屬性,就必須使用到對象了,對象上每一個屬性都有這些值。
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é)束值單位不一致這兩個問題。