1.需求
抽獎是各類營銷活動中最常見的一種形式,本產品需求大致如下:轉盤周圍跑馬燈交替閃爍,點擊抽獎,大轉盤旋轉,調用接口獲取抽獎結果,大轉盤指針指向對應的獎品。高保如下圖1
圖1-高保
2.整體思路
2.1跑馬燈
本需求要求跑馬燈交替閃爍,那四周的跑馬燈就不能是死的圖片了,要用動畫來實現,並且第奇數,偶數交替變換,這個使用vue中的動態屬性可以實現。
其次小燈泡分布在四周,首先想到的是 transform: rotate(); 然后獎品圖片也是分布在圓形四周,這個也可以用 transform: rotate()。
2.2轉盤旋轉
點擊立即抽獎,包含獎品圖片的整個dom旋轉,這個使用animation(css動畫)+transform: rotate()(css 2D變換)可以實現。
3.實現過程
各位看官請注意,這里只介紹了關鍵實現過程,中間涉及到的布局是大多使用absolute定位來實現的,中獎彈框是另外一個組件,因為不是關鍵,所以沒有具體介紹,也沒有貼出代碼。
3.1背景
這個背景一般是UI給個圖片出來,雖然使用css可以實現復雜的圖形,但是要花很長時間得不償失,一般給個背景圖片就可以了。注意這里高保給的有問題,后面獎品的dom會用另外一張背景覆蓋。如下圖2
圖2-轉盤背景
3.2轉盤跑馬燈
跑馬燈是一個一個的燈泡,放在轉盤周圍,這個要根據高保尺寸來寫,不然距離有差,沒法落在四周邊緣的位置。還有小燈泡明暗交替放置,不然也沒有效果。html代碼如下:
<!-- 轉盤周圍跑馬燈 --> <div class="lightWrap"> <div v-for="i in 20" :key="i" :style="setLightRotate(i)" class="lightItem"> <img v-if="i % 2 == 0" :class="{active: !lightChange}" :src="lightChange ? lightGrey : lightActive" alt=""/> <img v-else :class="{active: lightChange}" :src="!lightChange ? lightGrey : lightActive" alt=""/> </div> </div>
注意周圍有20個燈泡,所以使用v-for="i in 20",然后setLightToatate(i)是一個方法,用來設置每個燈泡的傾斜度,如下:
//20個燈泡設置傾斜 setLightRotate(index) { let lightRotate = (360 / 20) * index return { transform: 'rotate(' + lightRotate + 'deg)' } }
這個很容易理解了,整個圓是360度,除以20,是每兩個圓之間的間隔角度,再乘以數組20的下標,就是每個燈泡的偏移角度。
小燈泡的顯示就需要交替顯示了,計算奇偶使用表達式i%2 == 0,然后使用一個變量lightChange來判斷當前這個燈泡是否是點亮,來區分顯示不同的圖片。這個變量是通過questAnimationFrame遞歸調用來修改。方法如下:
//瀏覽器播放跑馬燈動畫 setTimeLine() { this.lightCount += 1 // 瀏覽器渲染頻率 60幀/s 約等於 16.66ms 一次,取20的倍數,就是約300ms切換跑馬燈一次 if ((this.lightCount % 20) == 0) { this.lightChange = !this.lightChange } requestAnimationFrame(this.setTimeLine) }
在mounted鈎子里調用一次setTimeLine()方法,然后在方法里調用requestAnimationFrame(),但是在requestAnimation()的回調函數里又調用了自己。注意這種方式類似遞歸調用,但是不是遞歸調用,瀏覽器的渲染評率是60幀每秒,也就是requestAnimationFrame()的回調函數在1000毫秒/60=16.66毫秒,也就是每16.66毫秒就執行一次setTimeLine()方法,在方法里lightChange自增1,判斷lightCount是20的倍數切換lightChange變量,然后16.66毫秒*20=33.33毫秒切換一次。
lightChange變量還切換了當前燈泡的樣式,點亮后還會設置燈泡變大一點。css如下:
.lightWrap { width: $turntableWrap_size; height: $turntableWrap_size; position: absolute; left: 50%; top: 50%; margin-left: calc(#{$turntableWrap_size} / -2); margin-top: calc(#{$turntableWrap_size} / -2); .lightItem { width: 22px; height: calc(#{$turntableWrap_size} / 2); position: absolute; left: 50%; top: 0%; transform-origin: 0 calc(#{$turntableWrap_size} / 2); img { width: 22px; height: 22px; position: absolute; top: 10px; left: 0; } img.active { width: 40px; height: 40px; position: absolute; top: 1px; left: -9px; } } }
最終效果如下圖3
圖3
3.3獎品圖片
接下來是要把獎品圖片放在轉盤上,並且分布在轉盤四周,原理還是使用transform: rotate();方法來設置傾斜。html代碼如下:
<!-- 獎品圖片 --> <div class="circleMax" :class="{ani: runningLock}"> <div v-for="(item, i) in actPrizeList" :key="i" :style="setRotate(i)" class="spin"> <div :style="setSpinInner(i)" class="spinInner"> <div :style="spinCntDocObj" class="spinCntDoc"> <div class="spinImg"> <img :src="item.imgUrl" alt="" srcset=""> </div> </div> </div> </div> </div>
actPrizeList就是獎品信息了,這個是從接口獲取,里面有配置好的獎品圖片連接。這里還是使用了一個方法setTotate(i)來動態設置樣式,方法如下:
/* 獎品圖片傾斜 */ setRotate(index) { let spinRotate = this.jiaodu * index return { transform: 'rotate(' + spinRotate + 'deg)' } }
setSpinInner()方法的功能類似,如下:
setSpinInner(index) { return { transform: 'rotate(-' + this.jiaodu + 'deg)', borderLeft: 0 } }
這里的變量jiaodu是45,是根據360度/8個獎品的規則來的,用來調整獎品傾斜度。這里還有一個spinCntDocObj對象,用來設置獎品圖片容器的尺寸,這個是為了在某些需求不是顯示獎品圖片,而是獎品名稱的時候,或者即顯示獎品名稱,又顯示獎品圖片的時候布局方便,當然在這里只顯示了一個獎品圖片。如下圖4
圖4
具體設置方法根據內圈直徑計算容器寬度,代碼如下:
/* 圖片旋轉 */ setSpinCntDoc() { let spinCntDocWidth = (Math.sin((this.jiaodu / 2) * (Math.PI / 180)) * this.tableInnerSize) / 70 this.spinCntDocObj.width = spinCntDocWidth + 'rem' this.spinCntDocObj.textAlign = 'center' this.spinCntDocObj.transform = 'rotate(' + (this.jiaodu / 2) + 'deg)' }
3.4背景整體旋轉
跑馬燈有了,獎品也有了,剩下就是要背景整體旋轉起來了。細心的話,你會發現在所有獎品容器上有一個動態樣式:class="{ani: runningLock}",變量runningLock這個變量是用來控制大轉盤旋轉的,大轉盤中所有獎品容器如下圖5:
圖5
css類anti中包含一個animation動畫,如下:
.ani { animation: circle 3s ease forwards; }
因為最后要根據中獎的獎品來計算大轉盤具體傾斜的角度,所以這個關鍵幀circle需要在請求接口之后,通過js代碼動態加載到頁面上,下面講抽獎按鈕的時候會具體的說明。
3.5抽獎按鈕
接下來需要把抽獎按鈕放在大轉盤正中間,還是使用相對定位absolute來實現,html代碼如下:
<!-- 抽獎按鈕 --> <div class="arrowBtn" :class="{btnShake: btnShakeShow}" @click="startClick"> <img src="../assets/images/summer/btn-draw.png" alt=""/> </div>
css代碼如下
.arrowBtn { width: 216px; height: 260px; border-radius: 95px; text-align: center; position: absolute; left: 50%; top: 50%; margin-left: -108px; margin-top: -152px; img { width: 100%; } }
在點擊按鈕的時候也有一個動畫,就是按鈕會變大然后變小,看起開是彈了一下,這個就簡單了,使用animation動畫就好,這里通過btnShakeShow變量來控制,css代碼如下:
.btnShake { animation: btnShakeAni 0.5s ease-out forwards; } @keyframes btnShakeAni { 0% { transform: scale(1); } 10% { transform: scale(1.1); } 30% { transform: scale(0.9); } 50% { transform: scale(1.1); } 70% { transform: scale(0.9); } 90% { transform: scale(1.1); } 100% { transform: scale(1); } }
最后效果如下圖6
圖6
3.6點擊抽獎
上面3.4講到在css類ani中使用animation動畫circle來控制整個大轉盤旋轉,並且要根據接口返回的抽獎結果計算旋轉角度動態設置rotate角度。代碼如下
/* 點擊抽獎播放動畫 */ startClick() { //大轉盤旋轉 this.runningLock = true //抽獎按鈕彈一下 this.btnShakeShow = true //調接口 let data = {actCode: actCode} coc2.drawLottery(data).then(res => { if (res.code == 0) { if (res.data) { this.prizeNum = this.actPrizeList.findIndex((item, index) => item.pid == res.data.pid) this.prizeName = res.data.prizeName this.prizeImgSrc = res.data.litimgUrl } } //動態加載動畫關鍵幀 let targetDeg = 360 * this.defaultRunTimes + (this.spinNum - this.prizeNum) * this.jiaodu + this.jiaodu * 0.5 let runkeyframes = `@keyframes circle{ 0% {transform: rotate(0deg);} 100% {transform: rotate(${targetDeg}deg); }` document.getElementById('mystyle').innerHTML = runkeyframes setTimeout(() => { this.$refs.refAlert.show('getPrize') }, 3500) }) }
注意spinNum是轉盤中所有獎品個數,prizeNum是中獎獎品在整個獎品中數組中的下標,二者做減法,然后頭部加上一個轉盤默認要轉圈數,尾部加上一個偏移(360度/8=45度)就可以定位到相應的位置的角度。隨后就是用這個角度拼接關鍵幀,最后動態設置這個css關鍵幀。注意要在index.html中加上一個id為mystyle的style元素,html如下:
圖7
整個動畫部分已經完成,來看看整體效果是怎么樣的,如下圖7
圖7
3.7動畫復原&中獎彈框
最后還有一個問題,抽獎之后需要無論是否中獎都需要將轉盤復原到初始狀態,這個動作的觸發時機在中獎彈框彈出之后,這樣方便下一次抽獎。實現這個功能需要在點擊獎品彈框的時候使用回調的方式。最后中獎的獎品圖片和獎品名稱也需要通過屬性賦值傳遞給獎品彈框。上面代碼中有的this.prizeName = res.data.prizeName;this.prizeImgSrc = res.data.litimgUrl就是在做這個事情。下面的html代碼。
<!-- 中獎彈框 --> <dialog-alert ref="refAlert" :prize-img-src="prizeImgSrc" :prize-name="prizeName"></dialog-alert>
在dialog-alert組件中,會有一個事件回調,這里使用的是eventbus,原因是這個組件在多個地方調用,這個和本文的主題關系不大 ,只簡單提一下。組件中回調方法如下:
EventBus.$emit("turntableReset")
當前抽獎組件中監聽方法如下:
mounted() { // 彈窗關閉 重置大轉盤 EventBus.$on("turntableReset", () => this.turnTableReset()) }, methods{ //轉盤數據重置 turnTableReset() { this.startLock = false this.runningLock = false this.btnShakeShow = false }
}
最后看看整體效果,如下圖8
圖8
4.總結
本功能還涉及到其他的功能,本功能實現的有些倉促,還有很多可以改進的地方。例如在播放動畫之前先請求了接口,等后端有了響應才開始播放動畫,這個不太合理,應該是先播放一個動畫,等有結果之后,再播放第二個動畫,讓指針指向中獎獎品。可以使用jquery動畫,或者tween.js,下次有時間再研究。