最近項目中遇到了需要做個h5的抽獎活動需求:要求支持九宮格模式和圓形抽獎盤模式。這邊基於vue和vantUI的loading組件造了這2個輪子。
九宮格抽獎
-
思路
動態構造幾次方的正方形抽鍵盤的4條邊;
requestAnimationFrame來控制動畫執行過程。 -
重點過程
- requestAnimationFrame初始化兼容性
// 兼容性raf初始化
window.requestAniFrame = (function() {
return window.requestAnimationFrame
// Older versions Chrome/Webkit
window.webkitRequestAnimationFrame ||
// Firefox < 23
window.mozRequestAnimationFrame ||
// opera
window.oRequestAnimationFrame ||
// ie
window.msRequestAnimationFrame ||
function(callback) {
return window.setTimeout(callback, 1000 / 60)
}
})()
// 兼容性取消初始化
window.cancelAnimation = (function() {
return (
window.cancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.cancelRequestAnimationFrame ||
function(id) {
clearTimeout(id)
}
)
})()
- 根據傳入的獎品項判斷當前是幾次方正方形(一個正方形有4條邊有4個頂點這個是死的)
DINGDIAN:4,
BIAN:4
// 獲取幾次冪方陣
this.firstTurn = (this.ds.length + this.DINGDIAN) / this.BIAN
// 獲取畫第二條邊 最多可畫幾個
this.secondNeedNumber = this.firstTurn - 1
- v-for下動態構造4條邊
// 獲取位置
getPosition(index, isSelect) {
// 選中樣式
let selectStyle = ``
if (isSelect) {
const selectWidth = this.tWidth - 4
const selectHeight = this.tHeight - 4
selectStyle = `width:${px2vw(selectWidth)};height:${px2vw(
selectHeight
)};border:${px2vw(2)} solid ${this.turnBackground}`
}
// 第一條邊 index 0 1 2
if (index < this.firstTurn) {
let i = index - 0 * (this.firstTurn - 1)
return `top:0;left:${px2vw(i * this.tWidth)};${
this.commonStyle
};${selectStyle}`
}
// 第二條邊 index值從 3 4
if (index - this.firstTurn < this.secondNeedNumber) {
let i = index - 1 * (this.firstTurn - 1)
return `top:${px2vw(i * this.tWidth)};left:${px2vw(this.abWidth)};${
this.commonStyle
};${selectStyle}`
}
// 第三條邊 5 6
if (index <= 3 * (this.firstTurn - 1)) {
let i = index - 2 * (this.firstTurn - 1)
return `top:${px2vw(this.abHeight)};left:${px2vw(
this.abWidth - i * this.tWidth
)};${this.commonStyle};${selectStyle}`
}
// 第四條邊 7
if (index < this.ds.length) {
let i = index - 3 * (this.firstTurn - 1)
return `top:${px2vw(this.abHeight - i * this.tWidth)};left:0;${
this.commonStyle
};${selectStyle}`
}
}
- 如何配合requestAnimationFrame
使用該api它表示瀏覽器在1秒內根據頻率指定刷新頁面,一般都是60HZ,也就是1000/60 = 16.7 (ms)具體可以去了解下這個api,跟setInterval區別。
這裏有個不太好的處理東西就是我們不能去設置這個刷新頻率,它是死的比如當前瀏覽器就是16.7ms執行一次。所以這個地方爲了方便我們來控制執行,需要兩個參數:當前執行動畫時間Date.now() 及上次執行動畫成功的時間lastSuccessTimer。這樣的話我們可以使用Date.now()減去lastSuccessTimer來跟我們制定的“時間間隔”作比較,在這裏時間間隔就相當於我們的頻率。更改這個時間間隔來控制我們的速度快慢也就是!
// runfunc
onRunFunc() {
let rafId = window.requestAniFrame(this.onRunFunc)
this.curRafID = rafId
let nowTimer = Date.now()
if (nowTimer - this.lastSuccessTimer >= this.spaceTimer) {
this.onRender(() => {
window.cancelAnimation(rafId)
// 恢復時間間隔
this.spaceTimer = this.whichStartSpaceTimer
this.isFinalCircle = false
this.onOff = true
this.isStart = false
})
}
}
- 關於滾動轉圈
這裏藉助vue的data數據綁定,我有個集合,集合對象中有個字段來判斷當前是否已滾動到次處。沒有滾動到此處就讓其執行加加++,整個獎品轉完後是爲執行了一圈,這裏我有個默認參數就是需要轉幾圈,到達指定轉圈數後,就是需要滾動到指定的中獎位置。
其實當時還有個疑惑就是我通過vue數據綁定來讓其字段變化,執行頻率這麼快vue那邊是否反映過來了,最開始也想直接操作dom,目前沒發現這樣使用有什麼弊端。
onRender(cb) {
if (this.isFinalCircle) {
// 固定圈數轉完後 在1位置
if (this.curSelect === this.bingoIndex && this.onOff) {
cb()
return
}
this.spaceTimer += this.eachTimer
this.curSelect++
if (this.curSelect < this.ds.length) {
this.ds[this.curSelect]['isSelect'] = true
this.ds[this.curSelect - 1]['isSelect'] = false
} else if (this.curSelect === this.ds.length) {
this.ds[this.curSelect - 1]['isSelect'] = false
this.ds[0]['isSelect'] = true
} else {
// 轉完一圈
this.curSelect = 0
this.onOff = true
}
this.$nextTick(() => {
this.lastSuccessTimer = Date.now()
})
} else {
this.curSelect++
if (this.curSelect < this.ds.length) {
this.ds[this.curSelect]['isSelect'] = true
this.ds[this.curSelect - 1]['isSelect'] = false
} else if (this.curSelect === this.ds.length) {
this.ds[this.curSelect - 1]['isSelect'] = false
this.ds[0]['isSelect'] = true
} else {
// 轉完一圈
this.curSelect = 1
this.ds[0]['isSelect'] = false
this.ds[1]['isSelect'] = true
this.countCircle++
if (this.countCircle === this.defaultCircle) {
this.countCircle = 0
this.spaceTimer = this.finalStartTimer
this.isFinalCircle = true
if (this.bingoIndex === 1) {
this.onOff = false
}
}
}
this.$nextTick(() => {
this.lastSuccessTimer = Date.now()
})
}
}
- 關於中獎
目前例子中是隨機中獎
this.bingoIndex = ~~(Math.random() * this.ds.length)
真實業務上,當我們點擊開始抽獎時回去調接口後端返回指定的中獎項,則直接根據返回值過濾出我們需要的中獎位置index值。
- 效果截圖
<nine-prize
:ds="dsNinePrize4"
prize-background="#BFEFFF"
prize-item-background="#F0F0F0"
pointer-text="GO!"
pointer-background="#EEB422"
turn-background="blue"
></nine-prize>
<nine-prize :ds="dsNinePrize"></nine-prize>
<nine-prize :ds="dsNinePrize12"></nine-prize>
圓形轉盤抽獎
-
思路
根據指定跑馬燈數量加css3旋轉動態構造燈泡外層(交替變化是使用keyframe);
分爲獎盤和獎品兩個div;
根據獎品數量加css3旋轉動態構造獎品項(360除以個數可得到圓心角);
第一種是隻支持偶數個的線型轉盤模式(旋轉計算);
第二種是可指定塊級轉盤樣式模式(三角函數算扇形);
旋轉方式通過transition來指定rotate的角度(固定角度+中獎商品角度);
通過監聽transitionend來得知動畫結束。 -
重點過程
- 如何畫跑馬燈
使用絕對水平垂直居中,定義每個跑馬燈外層div在最中心,在設置transition-origin來讓其變化是根據中心點來變化,每個旋轉角度是根據遍歷i來乘以固定的圓心角度。
// 跑馬燈
&-lamp {
position: absolute;
width: 100%;
height: 100%;
// 小燈泡
&__item {
// 絕對居中來旋轉
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 10px;
height: 100%;
margin: 0 auto;
transform-origin: center center;
@keyframes change-color {
0% {
background: #fff;
}
100% {
background: red;
}
}
// 畫小圓點
&::before {
content: "";
position: absolute;
top: 5px;
right: 0;
left: 0;
width: 10px;
height: 10px;
margin: 0 auto;
border-radius: 50%;
}
// 圓點顏色
&:nth-of-type(even):before {
background: #fff;
animation: change-color 1s linear infinite;
}
&:nth-of-type(odd):before {
background: red;
animation: change-color 1s linear reverse infinite;
}
}
}
- 如何畫線型獎盤
這裏也是使用絕對水平垂直居中加css3的旋轉來構造背景盤的分隔線,這裏我默認給了該線條爲1px(沒處理移動端1px顯示問題)
// 背景盤
&__background {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
// 畫線
.background-line-item {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 1px;
height: 50%;
margin: 0 auto;
transform-origin: center bottom;
}
}
- 如何畫獎品且中獎獎品居中
這裏需要注意的是我獎品默認都是寬度高度爲獎盤的一半的小正方形,且起始位置在左上角,旋轉中心是右下角來旋轉。還有個關鍵的地方就是如何去計算整體獎品div的偏移量,這個是根據獎品數量結合360度來計算的。這樣才能和獎盤對準。
這裏正方形一個角爲90度,居中取一半爲固定值45度: prizeRotateDegree
一圈爲360度: CIRCLEDEGREE
// 整個獎品盤旋轉量 (獎品起始於左上角 則旋轉量爲)
// 這裏目前該公式只適合銳角也就是獎品個數大於等於4
// 這裏公式是90 - 360/len - (45 - 360/len/2) 下面進行化簡
this.prizePlateRotate = this.prizeRotateDegree - this.CIRCLEDEGREE / 2 / len
- 如何畫扇形獎盤
這裏要解決扇形問題:其實扇形是三角形,那麼如何畫三角形即利用border來畫三角形再通過最外層overflow:hidden隱藏,因爲外層是個border-radius:50%的圓形。具體計算邏輯見下面函數註釋部分:
// 扇形背景渲染
renderSectorBackground() {
const len = this.ds.length
if (len === 1) {
// 此時扇形爲正方形
this.sectorWidth = this.wheelWidth - 2 * 20
this.sectorHeight = this.wheelWidth - 2 * 20
} else if (len === 2) {
// 此時扇形爲半圓的長方形
this.sectorWidth = this.wheelWidth - 2 * 20
this.sectorHeight = (this.wheelWidth - 2 * 20) / 2
} else {
const sectorR = (this.wheelWidth - 2 * 20) / 2 // 半徑
// 這裏看似畫的扇形其實是畫的三角形 利用overflow顯示出來就是扇形了
// 畫三角形 其實是利用Border來畫的 這裏的起始點在右上角開始畫
// 這裏保留的是上border 不要下border 而上border高度就是半徑 需要求一手border的寬度左右高度
// 而寬度是已半徑作高 作切線 公式:x/半徑 = tan(360/len/2)
// js的math三角函數的參數不是度數而是弧度參數 所以角度再轉弧度 360deg/2 等於 PI
const radian = Math.PI / len // 弧度
const bWidth = Math.tan(radian) * sectorR // 寬度
this.borderWidth = px2vw(Math.ceil(bWidth)) // border寬度(左右border)
this.borderHeight = px2vw(sectorR) // border高度(上border)
}
this.sectorRotateDegree = Math.ceil(this.CIRCLEDEGREE / len / 2)
this.dsSector = len
},
- 如何根據中獎項來計算出需要轉動的角度
這裏我有個默認轉動幾圈參數,一圈爲360,默認5圈。現在就需要計算出轉動到中獎項還有多少度。這裏還有個容易出錯的地方就是我的獎品項渲染是按順時針來渲染的,但是中獎項到中心卻是第一個,最後一個,倒數第二個依次類推,所以有個反的過程。
// 開始轉
onRun() {
this.isStart = true
const n = ~~(Math.random() * this.ds.length)
// 這裏有個區別 就是我渲染獎品是順時針渲染
// 但是旋轉是逆時針的旋轉 所以下標是反的
let realN
if (n === 0) {
realN = 0
} else {
realN = this.ds.length - n
}
console.log('中獎下標爲', realN)
// 需要旋轉度數
// 這裏公式是 i=0 -> 扇形圓心角 / 2 + 扇形圓心角 * 0
// i=1 -> 扇形圓心角 / 2 + 扇形圓心角 * 1
const degree = this.eachSectorDegree / 2 + this.eachSectorDegree * n
const fDegree = degree + this.turnTableCircle * this.CIRCLEDEGREE
this.$refs.myWheelPrize.setAttribute(
'style',
`transition: transform ${this.turnTableTimer}s ease-out 0s;transform: rotate(${fDegree}deg);${this.commonStyle}`
)
// 監聽transition事件完
this.$refs.myWheelPrize.addEventListener('transitionend', () =>
this.onTransitionEnd(degree)
)
},
// 動畫結束
onTransitionEnd(d) {
this.$refs.myWheelPrize.setAttribute(
'style',
`transition: transform 0s ease-out 0s;transform: rotate(${d}deg);${this.commonStyle}`
)
this.$refs.myWheelPrize.removeEventListener(
'transitionend',
this.onTransitionEnd
)
this.isStart = false
}
- 效果截圖
<wheel-prize
:ds="dsLinePrize"
wheel-background-type="line"
wheel-background="#0887f2"
turn-table-background="#c6c7ca"
turn-table-line-color="#333"
pointer-background="yellow"
pointer-text-color="#c6c7ca"
pointer-text="GO!"
></wheel-prize>
<wheel-prize :ds="dsSectorPrize" wheel-background-type="sector"></wheel-prize>
其中代碼中使用了js的加減乘除;使用了px2vw,css中是本地的mixin也就是Flex佈局。若需使用則拿下來根據自己項目要求更改,其中配置項可在組件內查看,就沒描述的那麼清楚了。具體的業務邏輯可看源碼!