抽獎(jiǎng)動(dòng)畫 - 紅包雨抽獎(jiǎng)
本文介紹一個(gè)小型動(dòng)畫庫anime.js,anime.js 是一款功能強(qiáng)大的Javascript 動(dòng)畫庫插件。anime.js 可以和CSS3 屬性,SVG,DOM 元素和JS 對象一起工作,制作出各種高性能,平滑過渡的動(dòng)畫效果。
anime.js雖然沒有其他動(dòng)畫庫功能強(qiáng)大,但是它包含的功完全能夠滿足日常活動(dòng)類開發(fā),并且它體積很小,壓縮后的anime.min.js只有18kb。下面簡單介紹aminie.js提供了哪些動(dòng)畫方法,并舉例說明如何在項(xiàng)目中使用。
1. 基本概念
1.1 動(dòng)畫的目標(biāo)對象
- 可使用任意CSS選擇器作為動(dòng)畫目標(biāo),不能用偽元素。
anime({
targets: '.css-selector-demo .el',
translateX: 250
})
- 使用DOM節(jié)點(diǎn)或節(jié)點(diǎn)的集合作為動(dòng)畫目標(biāo)。
var elements = document.querySelectorAll('.dom-node-demo .el');
anime({
targets: elements,
translateX: 270
});
- 以JavaScript對象作為動(dòng)畫目標(biāo),這個(gè)對象必須含有至少一個(gè)數(shù)字屬性。這個(gè)在vue中非常有用,例如這個(gè)數(shù)據(jù)用在動(dòng)態(tài)樣式中,那隨著這個(gè)樣式變化,這樣就可以看到一個(gè)動(dòng)畫效果。
var battery = {
charged: '0%',
cycles: 120
}
anime({
targets: battery,
charged: '100%',
cycles: 130,
round: 1,
easing: 'linear',
update: function() {
logEl.innerHTML = JSON.stringify(battery);
}
});
- 以數(shù)組作為動(dòng)畫目標(biāo),以數(shù)組形式接受以上三種類型的對象。
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 可動(dòng)畫的目標(biāo)屬性
大多數(shù)CSS屬性都會導(dǎo)致布局更改或重新繪制,并會導(dǎo)致動(dòng)畫不穩(wěn)定。 因此盡可能優(yōu)先考慮opacity和CSS transforms,這兩個(gè)屬性不會觸發(fā)重繪和重排。
- 支持常見值是數(shù)值的css屬性,例如width,top,margin等。
- 支持相對數(shù)值,例如在原來基礎(chǔ)上增加,減少一個(gè)數(shù)字,乘以一個(gè)數(shù)字等,舉例如下
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'
});
- 支持顏色動(dòng)畫,單位可以是Haxadecimal,RGB,RGBA,HSL,HSLA
1.3 時(shí)間軸(Timeline)
時(shí)間軸可讓你將多個(gè)動(dòng)畫同步在一起。默認(rèn)情況下,添加到時(shí)間軸的每個(gè)動(dòng)畫都會在上一個(gè)動(dòng)畫結(jié)束時(shí)開始。這樣就可以連續(xù)播放多個(gè)動(dòng)畫,在實(shí)際開發(fā)中經(jīng)常會遇到多個(gè)動(dòng)畫先后播放的場合,用這個(gè)時(shí)間軸的功能就可以輕松解決。看下面的例子:
// 使用默認(rèn)參數(shù)創(chuàng)建時(shí)間軸
var tl = anime.timeline({
easing: 'easeOutExpo',
duration: 750
});
// 增加子項(xiàng)
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,
});
這里只介紹幾個(gè)重要的概念,anime.js提供了豐富的api,其他可以參考官方文檔。
2. 紅包雨動(dòng)畫
下面我們來介紹如何使用anime.js實(shí)現(xiàn)一個(gè)紅包雨動(dòng)畫,這里不僅使用到anime.js動(dòng)畫,還用到lottie動(dòng)畫。關(guān)于lottie動(dòng)畫這里不做詳細(xì)介紹,這個(gè)動(dòng)畫是點(diǎn)擊到紅包的時(shí)候顯示一個(gè)爆炸的效果,起到一個(gè)點(diǎn)綴(模擬煙花爆炸)的作用。我們先整體看看這個(gè)動(dòng)畫有哪些元素和交互組成。
2.1 需求分解
2.1.1 三二一倒計(jì)時(shí)
動(dòng)畫開始是一個(gè)倒計(jì)時(shí),從3倒數(shù)到1時(shí)顯示紅包降落動(dòng)畫,這個(gè)倒計(jì)時(shí)也是動(dòng)畫的一部分,UI給到的藍(lán)湖如下圖1
圖1
2.1.2 紅包降落
開始動(dòng)畫的時(shí)候要顯示另外一個(gè)倒計(jì)時(shí),這個(gè)倒計(jì)時(shí)是限制搶紅包的時(shí)間是8秒,在這個(gè)時(shí)間范圍內(nèi)用戶可以點(diǎn)擊降落的紅包,這里產(chǎn)品要求8秒內(nèi)
紅包持續(xù)降落,后端給到一個(gè)隨機(jī)數(shù),例如3,在用戶點(diǎn)到第3個(gè)紅包的時(shí)候請求抽獎(jiǎng)接口,獲取抽獎(jiǎng)結(jié)果。如果用戶在8秒結(jié)束時(shí)點(diǎn)擊次數(shù)小于這個(gè)隨機(jī)數(shù),或者用戶根本就沒有點(diǎn)也會請求,接口在這種情況下接口返回的結(jié)果是錯(cuò)過機(jī)會。UI給到的高保如下圖2:
圖2
從高保上看,這里涉及到的動(dòng)畫有:
-
倒計(jì)時(shí),從8變成0;
-
進(jìn)度條,從左到右填充滿;
-
紅包降落;
另外根據(jù)產(chǎn)品的口頭描述,還有個(gè)lottery動(dòng)畫
-
用戶點(diǎn)中紅包,紅包爆炸,變成煙花,紅包消失;
2.1.3 中獎(jiǎng)彈窗
根據(jù)請求接口的結(jié)果,顯示中獎(jiǎng)結(jié)果,這個(gè)就相對簡單,高保圖如下:
圖3
注意點(diǎn)擊繼續(xù)搶紅包的時(shí)候,重新開始第二次抽獎(jiǎng),直至沒有剩余抽獎(jiǎng)機(jī)會,底部按鈕會顯示查看獎(jiǎng)勵(lì)。如果開始第二次抽獎(jiǎng),要把上次播放的動(dòng)畫復(fù)原到初始狀態(tài),重新開始。
2.2 實(shí)現(xiàn)過程
下面我們把這個(gè)動(dòng)畫分解成幾個(gè)部分,逐步分解說明如何實(shí)現(xiàn)這個(gè)功能。
2.2.1 生成紅包
紅包
圖2中背景上的圖片是分開給的,UI給到6張圖片的圖片命名為raindrop-0.png,raindrop-1.png,等等,如下圖3
圖4
隨機(jī)傾斜
并且按照高保上看,圖片還是有寫傾斜的,可以使用css中的transform: rotateZ(90deg),所以還要給紅包圖片一個(gè)傾斜度,但是每個(gè)紅包的傾斜度不能相同,需要隨機(jī),這樣看起來才像“紅包雨”。這個(gè)用到了一個(gè)生成隨機(jī)數(shù)函數(shù)來生成傾斜度,如下:
//生成兩個(gè)整數(shù)中間的隨機(jī)數(shù)
export function getRandomIntInclusive(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值
}
傳入兩個(gè)整數(shù),第一個(gè)最小數(shù),第二個(gè)最大數(shù),返回大于等于最小數(shù),小于等于最大數(shù)的隨機(jī)數(shù)。
紅包傾斜的角度需要在一個(gè)范圍之間,并且有兩個(gè)范圍,10deg60deg和120deg160deg之間,這樣每個(gè)都有傾斜。這里忽略60deg~120deg之間的隨機(jī)角度,是應(yīng)為這個(gè)區(qū)間傾斜的話,看上去太正,例如,90deg是豎直的,如下圖示:
圖5
如何選擇上面10deg60deb和120deg160deg呢?還是使用隨機(jī)數(shù),不過這里簡單的使用Math.random()方法來控制。注意Math.random()返回值的返回是0到1,所以和0.5比較,要么左偏,要么右偏,不會你出現(xiàn)豎直的情況。如下:
Math.random() > 0.5 ? getRandomIntInclusive(10, 60) : getRandomIntInclusive(120, 170)
2.2.2 圖片尺寸
UI給到了6張紅包圖片raindrop-0.png~raindrop-5.png,紅包雨要降落的紅包肯定是大于5張的,不然看上去太少了,也不像“雨”,這就有個(gè)問題了,這5張紅包圖片的尺寸不一致,我們需要設(shè)置每個(gè)圖片的尺寸,這里要用到求余計(jì)算,“總紅包個(gè)數(shù) % 6”,這樣得到的結(jié)果永遠(yuǎn)都是[0~5],然后我們把圖片的尺寸記在一個(gè)有6個(gè)元素的數(shù)組中,如下:
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轉(zhuǎn)換成vw尺寸。
2.2.3 初始位
三二一倒計(jì)結(jié)束的時(shí)刻紅包是看不見的,這樣紅包初始位置要在屏幕之外,這里用到relative/absolute絕對定位,這里用到top: -96。還有個(gè)問題,left就不好用一個(gè)固定數(shù)值了,這里又也需要用到隨機(jī)數(shù),讓紅包在x軸隨機(jī)分布,這樣做也是為了讓動(dòng)畫看起來像“雨”。代碼如下:
getRandomIntInclusive(0, 100 - 170 / 7.5)
注意這里除以7.5使用來吧px轉(zhuǎn)換成vw尺寸。
2.2.4 紅包數(shù)組
最后的生成紅包數(shù)組的代碼如下:
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隨機(jī)傾斜
obj.left = getRandomIntInclusive(0, 100 - 170 / 7.5) //left
obj.imgSrc = require('./../assets/images/red-rain/raindrop-'+ index +'.png') //紅包圖片
return obj
})
2.2.5 倒計(jì)時(shí)
三二一倒計(jì)時(shí),這里使用setInterval方法,每秒start遞減直至為0,頁面上用這個(gè)start作為數(shù)字圖片的一部分,在倒計(jì)時(shí)結(jié)束后顯示紅包雨彈框并開始播放動(dòng)畫,代碼如下:
countDownTip() {
//321開始
this.intId = setInterval(() => {
this.countDown.start--
this.$nextTick(() => {
if (this.countDown.start <= 0) {
clearInterval(this.intId)
//3秒后顯示紅包雨動(dòng)畫
this.isShow.countDown = false
this.playAnime() //播放動(dòng)畫
}
})
}, 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 進(jìn)度條&倒計(jì)時(shí)&紅包降落&未點(diǎn)擊抽獎(jiǎng)
雖然進(jìn)度條動(dòng)畫,倒計(jì)時(shí)動(dòng)畫,紅包降落動(dòng)畫是同步進(jìn)行的,這里我們?yōu)榱舜a方便還是用到時(shí)間軸Timeline來組織代碼。進(jìn)度條動(dòng)畫是在8秒時(shí)間內(nèi)從左到右鋪滿,倒計(jì)時(shí)動(dòng)畫是數(shù)字從8逐步減少到0,紅包降落動(dòng)畫是修改元素的top屬性,從-96(隱藏)到整個(gè)屏幕的高度,就是落到屏幕最底部隱藏,注意紅包降落的過程中不能所有的一起降落,要有時(shí)間上的交錯(cuò),這里用到交錯(cuò)動(dòng)畫,來看下面的代碼。
playAnime() {
this.tl = anime.timeline({easing: 'linear', duration: 8000})
let height = window.screen.height
this.tl.add({ //倒計(jì)時(shí)動(dòng)畫
targets: this.countDown, //動(dòng)畫目標(biāo)countDown對象中的rob屬性,從8變成0
rob: 0,
duration: 8000, //持續(xù)8秒鐘
round: 1,
delay: 500,
easing: 'linear',
complete: () => {
this.tl.pause() //結(jié)束后動(dòng)畫結(jié)束
//8秒后未點(diǎn)擊或點(diǎn)擊數(shù)小于隨機(jī)數(shù),去抽獎(jiǎng)
if (this.btnClickCount < this.chance.random) {
this.lottery()
}
}
}).add({ //進(jìn)度條動(dòng)畫
targets: '#processImg', //動(dòng)畫目標(biāo)是標(biāo)簽,css選擇器
width: '100%', //修改標(biāo)簽的寬度
duration: 8000 //初始時(shí)間是8秒
}, 0).add({ //紅包降落動(dòng)畫
targets: '.envelop', //動(dòng)畫目標(biāo)是標(biāo)簽,一系列div標(biāo)簽
delay: anime.stagger(300, {start: 100}), //交錯(cuò)動(dòng)畫,延遲從100ms開始,然后每個(gè)元素增加300ms
easing: 'linear',
top: height, //修改高度
loop: true
}, 0)
}
來看看這個(gè)動(dòng)畫的效果,如下圖6
圖6
從界面效果上看符合需求的預(yù)期,右上角倒計(jì)時(shí),進(jìn)度條從左到右鋪滿,紅包持續(xù)降落,而不是一起降落。Math.random()和getRandomIntInclusive()方法配合讓紅包隨機(jī)左右傾斜并且在x軸隨機(jī)分布,這樣紅包看起來更像是一場“雨”。
2.2.7 紅包爆炸
在紅包降落的過程中,8秒時(shí)間內(nèi),如果用戶點(diǎn)擊了紅包,會有一個(gè)紅包爆炸的效果,這里用到Lottie動(dòng)畫。Lottie動(dòng)畫是由專門的動(dòng)畫設(shè)計(jì)師做好之后發(fā)個(gè)前端開發(fā)人員來接入的,這里我們不做詳細(xì)介紹,只說一個(gè)問題。
Lottery動(dòng)畫設(shè)計(jì)師輸出的產(chǎn)物是動(dòng)畫資源,包含一個(gè)img文件夾,里面是圖片文件,還有一個(gè)data.json數(shù)據(jù),引入Lottie插件之后,要額外再引入這個(gè)json數(shù)據(jù),注意這個(gè)json數(shù)據(jù)里會引入images文件夾下的圖片文件,在json對象的assets節(jié)點(diǎn)下面。如下圖7
圖7
我們看到assest目錄下有個(gè)圖片img_0.png,如下圖8
引入data.json之后要對assets節(jié)點(diǎn)下的圖片目錄特殊處理,使用require()方法引入,不然打包之后找不到圖片,如下處理
引入資源數(shù)據(jù)
import animeData from './../assets/boom/data.json'
處理數(shù)據(jù)
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'
點(diǎn)擊紅包之后要播放當(dāng)前點(diǎn)擊的紅包的爆炸動(dòng)畫,并且停止紅包雨,代碼如下:
//點(diǎn)擊紅包
btnRob(el, data) {
if (checkLogin()) {
//點(diǎn)擊次數(shù)加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)
})
//點(diǎn)擊次數(shù)大于等于隨機(jī)次數(shù)
if (this.btnClickCount >= this.chance.random) {
//停止飄落
this.tl.pause()
//去抽獎(jiǎng)
this.lottery()
}
}
}
下面來看看這個(gè)爆炸的效果,如下圖9
圖6
從圖中爆炸效果來看,Lottie動(dòng)畫是給這個(gè)煙花圖片做了一個(gè)從小變大的效果。
2.2.8 抽獎(jiǎng)
根據(jù)需求,在8秒內(nèi)用戶點(diǎn)擊紅包達(dá)到規(guī)定次數(shù)的時(shí)候,去抽獎(jiǎng),沒有點(diǎn)擊或者點(diǎn)擊次數(shù)小于規(guī)定次數(shù),也會去調(diào)抽獎(jiǎng)接口,接口會將抽獎(jiǎng)機(jī)會減1并告訴用戶錯(cuò)失機(jī)會。來看下面的代碼:
//抽獎(jiǎng)
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'
//業(yè)務(wù)推薦
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()
}
//卡券獎(jiǎng)勵(lì)
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 動(dòng)畫復(fù)原
上面代碼是調(diào)接口和接口處理邏輯,和動(dòng)畫關(guān)系不大,但是有一個(gè)要注意的地方,調(diào)接口之后彈出抽獎(jiǎng)結(jié)果彈框,可能用戶還有抽獎(jiǎng)機(jī)會,這時(shí)又可以抽,需要將動(dòng)畫復(fù)原。這里有個(gè)問題,如果是通過動(dòng)畫修改過的data值,需要重新賦值,并且使用anime.js賦值,直接使用vue中的this.xxx = yyy不起作用,這個(gè)估計(jì)是修改動(dòng)畫的值的時(shí)候沒有觸發(fā)set導(dǎo)致的,來看下面的代碼。
<!-- 卡券 -->
<win-prize ref="refWinPrize" :prize="prize" :chance="chance" @continueRob="continueRob"></win-prize>
<!-- 業(yè)務(wù)推薦 -->
<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 //用戶點(diǎn)擊次數(shù)初始化
this.countDown.start = 3
this.countDown.rob = 8
if (closeAll) {
this.isShow.pop = false //關(guān)閉整個(gè)紅包雨彈框
}
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
作者:Tyler Ning
出處:http://www.rzrgm.cn/tylerdonet/
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,如有問題,請微信聯(lián)系冬天里的一把火
浙公網(wǎng)安備 33010602011771號