本文介紹一個小型動畫庫anime.js,anime.js 是一款功能強大的Javascript 動畫庫外掛。anime.js 可以和CSS3 屬性,SVG,DOM 元素和JS 物件一起工作,製作出各種高效能,平滑過渡的動畫效果。
anime.js雖然沒有其他動畫庫功能強大,但是它包含的功完全能夠滿足日常活動類開發,並且它體積很小,壓縮後的anime.min.js只有18kb。下面簡單介紹aminie.js提供了哪些動畫方法,並舉例說明如何在專案中使用。
1. 基本概念
1.1 動畫的目標物件
- 可使用任意CSS選擇器作為動畫目標,不能用偽元素。
anime({
targets: '.css-selector-demo .el',
translateX: 250
})
- 使用DOM節點或節點的集合作為動畫目標。
var elements = document.querySelectorAll('.dom-node-demo .el');
anime({
targets: elements,
translateX: 270
});
- 以JavaScript物件作為動畫目標,這個物件必須含有至少一個數字屬性。這個在vue中非常有用,例如這個資料用在動態樣式中,那隨著這個樣式變化,這樣就可以看到一個動畫效果。
var battery = {
charged: '0%',
cycles: 120
}
anime({
targets: battery,
charged: '100%',
cycles: 130,
round: 1,
easing: 'linear',
update: function() {
logEl.innerHTML = JSON.stringify(battery);
}
});
- 以陣列作為動畫目標,以陣列形式接受以上三種型別的物件。
var el = document.querySelector('.mixed-array-demo .el-01');
anime({
targets: [el, '.mixed-array-demo .el-02', '.mixed-array-demo .el-03'],
translateX: 250
});
1.2 可動畫的目標屬性
大多數CSS屬性都會導致佈局更改或重新繪製,並會導致動畫不穩定。 因此儘可能優先考慮opacity和CSS transforms,這兩個屬性不會觸發重繪和重排。
- 支援常見值是數值的css屬性,例如width,top,margin等。
- 支援相對數值,例如在原來基礎上增加,減少一個數字,乘以一個數字等,舉例如下
var relativeEl = document.querySelector('.el.relative-values');
relativeEl.style.transform = 'translateX(100px)';
anime({
targets: '.el.relative-values',
translateX: {
value: '*=2.5', // 100px * 2.5 = '250px'
duration: 1000
},
width: {
value: '-=20px', // 28 - 20 = '8px'
duration: 1800,
easing: 'easeInOutSine'
},
rotate: {
value: '+=2turn', // 0 * 2 = '2turn'
duration: 1800,
easing: 'easeInOutSine'
},
direction: 'alternate'
});
- 支援顏色動畫,單位可以是Haxadecimal,RGB,RGBA,HSL,HSLA
1.3 時間軸(Timeline)
時間軸可讓你將多個動畫同步在一起。預設情況下,新增到時間軸的每個動畫都會在上一個動畫結束時開始。這樣就可以連續播放多個動畫,在實際開發中經常會遇到多個動畫先後播放的場合,用這個時間軸的功能就可以輕鬆解決。看下面的例子:
// 使用預設引數建立時間軸
var tl = anime.timeline({
easing: 'easeOutExpo',
duration: 750
});
// 增加子項
tl
.add({
targets: '.basic-timeline-demo .el.square',
translateX: 250,
})
.add({
targets: '.basic-timeline-demo .el.circle',
translateX: 250,
})
.add({
targets: '.basic-timeline-demo .el.triangle',
translateX: 250,
});
這裡只介紹幾個重要的概念,anime.js提供了豐富的api,其他可以參考官方文件。
2. 紅包雨動畫
下面我們來介紹如何使用anime.js實現一個紅包雨動畫,這裡不僅使用到anime.js動畫,還用到lottie動畫。關於lottie動畫這裡不做詳細介紹,這個動畫是點選到紅包的時候顯示一個爆炸的效果,起到一個點綴(模擬煙花爆炸)的作用。我們先整體看看這個動畫有哪些元素和互動組成。
2.1 需求分解
2.1.1 三二一倒數計時
動畫開始是一個倒數計時,從3倒數到1時顯示紅包降落動畫,這個倒數計時也是動畫的一部分,UI給到的藍湖如下圖1
圖1
2.1.2 紅包降落
開始動畫的時候要顯示另外一個倒數計時,這個倒數計時是限制搶紅包的時間是8秒,在這個時間範圍內使用者可以點選降落的紅包,這裡產品要求8秒內
紅包持續降落,後端給到一個隨機數,例如3,在使用者點到第3個紅包的時候請求抽獎介面,獲取抽獎結果。如果使用者在8秒結束時點選次數小於這個隨機數,或者使用者根本就沒有點也會請求,介面在這種情況下介面返回的結果是錯過機會。UI給到的高保如下圖2:
圖2
從高保上看,這裡涉及到的動畫有:
-
倒數計時,從8變成0;
-
進度條,從左到右填充滿;
-
紅包降落;
另外根據產品的口頭描述,還有個lottery動畫
-
使用者點中紅包,紅包爆炸,變成煙花,紅包消失;
2.1.3 中獎彈窗
根據請求介面的結果,顯示中獎結果,這個就相對簡單,高保圖如下:
圖3
注意點選繼續搶紅包的時候,重新開始第二次抽獎,直至沒有剩餘抽獎機會,底部按鈕會顯示檢視獎勵。如果開始第二次抽獎,要把上次播放的動畫復原到初始狀態,重新開始。
2.2 實現過程
下面我們把這個動畫分解成幾個部分,逐步分解說明如何實現這個功能。
2.2.1 生成紅包
紅包
圖2中背景上的圖片是分開給的,UI給到6張圖片的圖片命名為raindrop-0.png,raindrop-1.png,等等,如下圖3
圖4
隨機傾斜
並且按照高保上看,圖片還是有寫傾斜的,可以使用css中的transform: rotateZ(90deg),所以還要給紅包圖片一個傾斜度,但是每個紅包的傾斜度不能相同,需要隨機,這樣看起來才像“紅包雨”。這個用到了一個生成隨機數函式來生成傾斜度,如下:
//生成兩個整數中間的隨機數
export function getRandomIntInclusive(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值
}
傳入兩個整數,第一個最小數,第二個最大數,返回大於等於最小數,小於等於最大數的隨機數。
紅包傾斜的角度需要在一個範圍之間,並且有兩個範圍,10deg60deg和120deg160deg之間,這樣每個都有傾斜。這裡忽略60deg~120deg之間的隨機角度,是應為這個區間傾斜的話,看上去太正,例如,90deg是豎直的,如下圖示:
圖5
如何選擇上面10deg60deb和120deg160deg呢?還是使用隨機數,不過這裡簡單的使用Math.random()方法來控制。注意Math.random()返回值的返回是0到1,所以和0.5比較,要麼左偏,要麼右偏,不會你出現豎直的情況。如下:
Math.random() > 0.5 ? getRandomIntInclusive(10, 60) : getRandomIntInclusive(120, 170)
2.2.2 圖片尺寸
UI給到了6張紅包圖片raindrop-0.png~raindrop-5.png,紅包雨要降落的紅包肯定是大於5張的,不然看上去太少了,也不像“雨”,這就有個問題了,這5張紅包圖片的尺寸不一致,我們需要設定每個圖片的尺寸,這裡要用到求餘計算,“總紅包個數 % 6”,這樣得到的結果永遠都是[0~5],然後我們把圖片的尺寸記在一個有6個元素的陣列中,如下:
export const pSize = [
{w: 136/7.5, h: 134/7.5},
{w: 170/7.5, h: 202/7.5},
{w: 170/7.5, h: 202/7.5},
{w: 152/7.5, h: 180/7.5},
{w: 152/7.5, h: 180/7.5},
{w: 106/7.5, h: 144/7.5}
]
注意這裡除以7.5使用來吧px轉換成vw尺寸。
2.2.3 初始位
三二一倒計結束的時刻紅包是看不見的,這樣紅包初始位置要在螢幕之外,這裡用到relative/absolute絕對定位,這裡用到top: -96。還有個問題,left就不好用一個固定數值了,這裡又也需要用到隨機數,讓紅包在x軸隨機分佈,這樣做也是為了讓動畫看起來像“雨”。程式碼如下:
getRandomIntInclusive(0, 100 - 170 / 7.5)
注意這裡除以7.5使用來吧px轉換成vw尺寸。
2.2.4 紅包陣列
最後的生成紅包陣列的程式碼如下:
this.envelop = Array(20).fill({}).map((a, i) => {
let index = i % 6, {w, h} = pSize[index] //尺寸
let obj = {left: 0, top: -96, rotateZ: 0, imgSrc: '', w, h} //top: -96 初始隱藏
obj.rotateZ = Math.random() > 0.5 ? getRandomIntInclusive(10, 60) : getRandomIntInclusive(120, 170) //sui隨機傾斜
obj.left = getRandomIntInclusive(0, 100 - 170 / 7.5) //left
obj.imgSrc = require('./../assets/images/red-rain/raindrop-'+ index +'.png') //紅包圖片
return obj
})
2.2.5 倒數計時
三二一倒數計時,這裡使用setInterval方法,每秒start遞減直至為0,頁面上用這個start作為數字圖片的一部分,在倒數計時結束後顯示紅包雨彈框並開始播放動畫,程式碼如下:
countDownTip() {
//321開始
this.intId = setInterval(() => {
this.countDown.start--
this.$nextTick(() => {
if (this.countDown.start <= 0) {
clearInterval(this.intId)
//3秒後顯示紅包雨動畫
this.isShow.countDown = false
this.playAnime() //播放動畫
}
})
}, 1000)
}
<mask-slot :is-show="isShow.countDown">
<div class="content tip">
<img
style="margin-top: 30%"
:src="require('../assets/images/red-rain/count-'+ countDown.start +'.png')"
class="number"
alt="" />
</div>
</mask-slot>
2.2.6 進度條&倒數計時&紅包降落&未點選抽獎
雖然進度條動畫,倒數計時動畫,紅包降落動畫是同步進行的,這裡我們為了程式碼方便還是用到時間軸Timeline來組織程式碼。進度條動畫是在8秒時間內從左到右鋪滿,倒數計時動畫是數字從8逐步減少到0,紅包降落動畫是修改元素的top屬性,從-96(隱藏)到整個螢幕的高度,就是落到螢幕最底部隱藏,注意紅包降落的過程中不能所有的一起降落,要有時間上的交錯,這裡用到交錯動畫,來看下面的程式碼。
playAnime() {
this.tl = anime.timeline({easing: 'linear', duration: 8000})
let height = window.screen.height
this.tl.add({ //倒數計時動畫
targets: this.countDown, //動畫目標countDown物件中的rob屬性,從8變成0
rob: 0,
duration: 8000, //持續8秒鐘
round: 1,
delay: 500,
easing: 'linear',
complete: () => {
this.tl.pause() //結束後動畫結束
//8秒後未點選或點選數小於隨機數,去抽獎
if (this.btnClickCount < this.chance.random) {
this.lottery()
}
}
}).add({ //進度條動畫
targets: '#processImg', //動畫目標是標籤,css選擇器
width: '100%', //修改標籤的寬度
duration: 8000 //初始時間是8秒
}, 0).add({ //紅包降落動畫
targets: '.envelop', //動畫目標是標籤,一系列div標籤
delay: anime.stagger(300, {start: 100}), //交錯動畫,延遲從100ms開始,然後每個元素增加300ms
easing: 'linear',
top: height, //修改高度
loop: true
}, 0)
}
來看看這個動畫的效果,如下圖6
圖6
從介面效果上看符合需求的預期,右上角倒數計時,進度條從左到右鋪滿,紅包持續降落,而不是一起降落。Math.random()和getRandomIntInclusive()方法配合讓紅包隨機左右傾斜並且在x軸隨機分佈,這樣紅包看起來更像是一場“雨”。
2.2.7 紅包爆炸
在紅包降落的過程中,8秒時間內,如果使用者點選了紅包,會有一個紅包爆炸的效果,這裡用到Lottie動畫。Lottie動畫是由專門的動畫設計師做好之後發個前端開發人員來接入的,這裡我們不做詳細介紹,只說一個問題。
Lottery動畫設計師輸出的產物是動畫資源,包含一個img資料夾,裡面是圖片檔案,還有一個data.json資料,引入Lottie外掛之後,要額外再引入這個json資料,注意這個json資料裡會引入images資料夾下的圖片檔案,在json物件的assets節點下面。如下圖7
圖7
我們看到assest目錄下有個圖片img_0.png,如下圖8
引入data.json之後要對assets節點下的圖片目錄特殊處理,使用require()方法引入,不然打包之後找不到圖片,如下處理
引入資源資料
import animeData from './../assets/boom/data.json'
處理資料
mounted() {
this.processData()
}
//處理json圖片路徑
processData() {
shuffle(this.envelop)
animeData.assets.forEach(item => {
item.u = ''
if (item.w && item.h) {
item.p = require(`@/assets/boom/images/${item.p}`) //require處理圖片路徑
}
})
}
還要安裝並引入Lottie外掛,如下:
import lottie from 'lottie-web'
點選紅包之後要播放當前點選的紅包的爆炸動畫,並且停止紅包雨,程式碼如下:
//點選紅包
btnRob(el, data) {
if (checkLogin()) {
//點選次數加1
this.btnClickCount++
el.target.style.background = 'none' //隱藏紅包
let lott = lottie.loadAnimation({
container: el.target,
animType: 'html',
renderer: 'svg',
loop: false,
autoplay: true,
animationData: animeData,
})
lott.setSpeed(3.5)//修改爆炸煙花速度
lott.addEventListener('complete', e => {
setTimeout(() => {
el.target.innerText = '' //隱藏紅包
}, 500)
})
//點選次數大於等於隨機次數
if (this.btnClickCount >= this.chance.random) {
//停止飄落
this.tl.pause()
//去抽獎
this.lottery()
}
}
}
下面來看看這個爆炸的效果,如下圖9
圖6
從圖中爆炸效果來看,Lottie動畫是給這個煙花圖片做了一個從小變大的效果。
2.2.8 抽獎
根據需求,在8秒內使用者點選紅包達到規定次數的時候,去抽獎,沒有點選或者點選次數小於規定次數,也會去調抽獎介面,介面會將抽獎機會減1並告訴使用者錯失機會。來看下面的程式碼:
//抽獎
lottery() {
this.$toast.loading({message: '載入中...', duration: 0, forbidClick: true, loadingType: 'spinner'})
let {auth} = getLocalStorage()
let data = {
actId: configData.actId,
clickNum: this.btnClickCount,
provinceId: auth.provinceCode,
channelId: configData.channelId
}
api.coc2.redEnvelope.raffle(data).then(res => {
this.$toast.clear()
this.prize = {}
this.$nextTick(() => {
if ([0, 9300001, 8000007, 9300003].includes(res.hRet)) {
if (res.hRet == 0) {
this.prize = res.data
}
this.prize.hRet = res.hRet
this.prize.page = 'red-envelope'
//業務推薦
if (4 === this.prize.prizeType) {
this.$refs.refService && this.$refs.refService.popUp()
}
//福卡
else if (6 === this.prize.prizeType) {
this.$refs.refAlipayCard && this.$refs.refAlipayCard.popUp()
}
//卡券獎勵
else {
this.$refs.refWinPrize && this.$refs.refWinPrize.popUp(this.prize)
}
} else if (res.hRet === 303) {
pullLogin()
} else {
this.$toast(res.retMsg)
this.close(true)
EventBus.$emit(EventKey.checkPrize)
}
})
}).catch(e => {
this.$toast.clear()
this.prize.hRet = 8000007
this.$nextTick(() => {
this.$refs.refWinPrize && this.$refs.refWinPrize.popUp(this.prize)
})
})
}
2.2.9 動畫復原
上面程式碼是調介面和介面處理邏輯,和動畫關係不大,但是有一個要注意的地方,調介面之後彈出抽獎結果彈框,可能使用者還有抽獎機會,這時又可以抽,需要將動畫復原。這裡有個問題,如果是通過動畫修改過的data值,需要重新賦值,並且使用anime.js賦值,直接使用vue中的this.xxx = yyy不起作用,這個估計是修改動畫的值的時候沒有觸發set導致的,來看下面的程式碼。
<!-- 卡券 -->
<win-prize ref="refWinPrize" :prize="prize" :chance="chance" @continueRob="continueRob"></win-prize>
<!-- 業務推薦 -->
<handle-service ref="refService" :prize="prize" @close="close"></handle-service>
<!-- 福卡 -->
<alipay-card ref="refAlipayCard" :prize="prize" :chance="chance" @continueRob="continueRob"></alipay-card>
close(closeAll) {
this.btnClickCount = 0 //使用者點選次數初始化
this.countDown.start = 3
this.countDown.rob = 8
if (closeAll) {
this.isShow.pop = false //關閉整個紅包雨彈框
}
this.isShow.countDown = true
clearInterval(this.intId)
this.tl = anime.timeline()
this.tl.add({
targets: '.envelop',
top: -96,
duration: 100,
easing: 'linear'
}).add({
targets: '#processImg',
width: '0%',
duration: 100
})
}
3 最終效果
圖7
5.參考
- animejs https://www.animejs.cn/
- Lottie https://airbnb.design/lottie/#get-started