餓了么下單時(shí),點(diǎn)擊?會有一個(gè)小球跳躍進(jìn)入購物車的動畫,公司有個(gè)商城頁面要求模仿這個(gè)效果,在網(wǎng)上搜了下.都需要借助框架,而且不是很靈活,位置必須相對固定.使用也不是很方便.因此決定借鑒思路,實(shí)現(xiàn)一個(gè)小球類.
完整代碼
期望
使用new的方式獲得實(shí)例:let ball = new Ball(ball)
調(diào)用實(shí)例方法: ball.drop({x: point.clientX, y: point.clientY, z: 0})
小球從傳入的坐標(biāo)處開始運(yùn)行運(yùn)動,執(zhí)行拋物線落入購物車的動畫
思路
仔細(xì)觀察動畫,不難發(fā)現(xiàn),小球有個(gè)固定的落點(diǎn),即購物車,那么我們可以將其作為小球真實(shí)dom的容器,也就是運(yùn)動的終點(diǎn).運(yùn)動的起點(diǎn)則是鼠標(biāo)點(diǎn)擊的位置.則我們只需要在點(diǎn)擊時(shí),使用transform
將小球移動到鼠標(biāo)點(diǎn)擊位置,然后再回到原點(diǎn)即可,期間加上恰當(dāng)?shù)?code>transition即可形成移動動畫(將拋物線分解為水平和豎直方向的移動,水平方向勻速平移,水平方向使用貝賽爾曲線過渡).
代碼
我們來實(shí)現(xiàn)ball類,這個(gè)類應(yīng)接受一個(gè)dom作為參數(shù)(這個(gè)dom應(yīng)包含有一個(gè)子元素),這個(gè)類操作dom實(shí)現(xiàn)我們的動畫.它應(yīng)提供一個(gè)方法,這個(gè)方法接受一個(gè)坐標(biāo)對象,該對象指示了小球的起點(diǎn).當(dāng)小球動畫執(zhí)行完畢,應(yīng)通知外部.
class Ball {
constructor(el) {
this.el = el
// 事件回調(diào)容器
this.taskMap = {}
// 獲得元素位置
this.position = el.getBoundingClientRect()
// 綁定事件回調(diào)this
this.dropOver = this.dropOver.bind(this)
}
// 將小球移動到指定位置
beforeDrop(pos) {
let {x, y, z} = pos
let {top, left} = this.position
let offsetY = top - y
let offsetX = left - x
this.translate({x: -offsetX, y: -offsetY, z})
this.el.style.visibility = 'visible'
// 等待瀏覽器重繪完成
setTimeout(() => {
this.startDrop()
}, 20)
}
// 添加transition,回到原點(diǎn)
startDrop() {
let styleKey = prefixStyle('transition')
// 內(nèi)外層添加不同的transition
this.el.style[styleKey] = 'all .3s cubic-bezier(0.49, -0.29, 0.75, 0.41)'
this.el.children[0].style[styleKey] = 'all .3s linear'
this.translate()
this.el.addEventListener(prefixStyle('transitionEnd'), this.dropOver)
}
// 清除事件監(jiān)聽,通知外部
dropOver() {
let styleKey = prefixStyle('transition')
this.el.removeEventListener(prefixStyle('transitionEnd'), this.dropOver)
this.el.style[styleKey] = ''
this.el.children[0].style[styleKey] = ''
setTimeout(() => {
this.el.style.visibility = 'hidden'
this.fire('drop-over')
})
}
// 移動小球
translate(pos = {}) {
let {x = 0, y = 0, z = 0} = pos
let el = this.el
let styleKey = 'transform'
let styleVal = `translate3d(0px,${y}px,${z}px)`
el.style[styleKey] = styleVal
el.children[0].style[styleKey] = `translate3d(${x}px,0px,${z}px)`
}
// 簡單的事件回調(diào)
on(event, callback) {
this.taskMap[event] = callback
}
fire(event) {
this.taskMap[event] && this.taskMap[event]()
}
}
至此,我們完成了一個(gè)可以呈拋物線跳躍的小球,這個(gè)小球可以從任意高于其真實(shí)dom所在的位置拋物線回落
但不難發(fā)現(xiàn),我們一次只能執(zhí)行一個(gè)小球動畫.我們還需要一個(gè)隊(duì)列用于管理小球,進(jìn)行動畫.
同樣的,我們借助一個(gè)類來實(shí)現(xiàn),這個(gè)類接受一個(gè)元素作為參數(shù),該元素應(yīng)包含有復(fù)數(shù)個(gè)符合小球類要求的元素.調(diào)用該類的實(shí)例方法,可以同時(shí)執(zhí)行多個(gè)動畫.
代碼
小球隊(duì)列類
class DropBall {
constructor(el) {
this.el = el
this.createBallList()
}
createBallList() {
let el = this.el
let children = Array.from(el.children)
this.ballList = children.map(child =>new Ball(child))
}
drop(pos) {
let ball = this.ballList.shift()
if (!ball)
ball.beforeDrop(pos)
ball.on('drop-over', () => {
this.ballList.push(ball)
})
}
}
這個(gè)類非常簡單,將傳入的元素的子元素挨個(gè)生成
Ball
實(shí)例存入隊(duì)列,提供一個(gè)drop
方法,該方法每次從隊(duì)列中取出一個(gè)實(shí)例執(zhí)行動畫,當(dāng)實(shí)例動畫執(zhí)行完畢,將其推回隊(duì)列中,供下次調(diào)用.小球動畫執(zhí)行時(shí)間約為320ms,一般ball實(shí)例只需要5個(gè),就能保證夠用(點(diǎn)擊間隔不小于60ms,正常人抽不到這風(fēng))