Vue3實(shí)現(xiàn)轉(zhuǎn)盤(pán)抽獎(jiǎng)效果的示例詳解
前言
之前的文章有寫(xiě)過(guò)九宮格抽獎(jiǎng)功能【 Vue3實(shí)現(xiàn)九宮格抽獎(jiǎng)功能 】, 有興趣的可以看一看給個(gè)贊啥的,今天我們看看如何實(shí)現(xiàn)一個(gè)轉(zhuǎn)盤(pán)抽獎(jiǎng)功能。
之前公司也寫(xiě)過(guò)轉(zhuǎn)盤(pán)抽獎(jiǎng)功能,實(shí)現(xiàn)流程大概是設(shè)計(jì)師出個(gè)轉(zhuǎn)盤(pán)抽獎(jiǎng)設(shè)計(jì)圖,禮品固定,后臺(tái)請(qǐng)求接口返回中獎(jiǎng)數(shù)據(jù),然后前端去實(shí)現(xiàn)動(dòng)效,這么做的缺點(diǎn)就比較固定,比如獎(jiǎng)品無(wú)法配置,中獎(jiǎng)概率也無(wú)法配置。我在各大網(wǎng)站都逛了一圈,基本上都上這種固定的抽獎(jiǎng)配置,很少有動(dòng)態(tài)配置獎(jiǎng)品和中獎(jiǎng)概率的轉(zhuǎn)盤(pán)抽獎(jiǎng),基于此我今天就抽空來(lái)實(shí)現(xiàn)一個(gè)可動(dòng)態(tài)配置獎(jiǎng)品的轉(zhuǎn)盤(pán)抽象功能。
功能與效果圖
功能:
1、禮物可后臺(tái)動(dòng)態(tài)配置
2、轉(zhuǎn)盤(pán)獎(jiǎng)品布局和背景動(dòng)態(tài)生成(重點(diǎn))
3、轉(zhuǎn)動(dòng)速度和時(shí)間可配置,速度先快后慢
4、記錄上一次停止位置,開(kāi)始轉(zhuǎn)動(dòng)不重置,轉(zhuǎn)動(dòng)位置最終停止在轉(zhuǎn)盤(pán)指針中間
5、動(dòng)畫(huà)停止可觸發(fā)中獎(jiǎng)提示或?qū)崿F(xiàn)其它功能
6、中獎(jiǎng)概率可配置(這里先不管,后面有機(jī)會(huì)總結(jié)一些抽獎(jiǎng)算法)
效果圖如下:
實(shí)現(xiàn)
第一步:轉(zhuǎn)盤(pán)布局
思路:首先需要一個(gè)轉(zhuǎn)盤(pán),然后需要一個(gè)抽獎(jiǎng)按鈕定位在中間,按鈕定位在中間很簡(jiǎn)單,難的是扇形背景顏色如何實(shí)現(xiàn),獎(jiǎng)品如何均勻分布在每個(gè)扇形中間,因?yàn)楠?jiǎng)品和可配置的,所以扇形大小可變化的。
<div id="app" v-cloak> <div class="container"> <!-- 背景 --> <div class="prize-list" ref="prizeWrap" :style="bgColor"> <!-- 獎(jiǎng)品列表 --> <div class="prize-item" v-for="(item, index) in prizeList" :style="prizeStyle(index)"> <img :src="item.pic" alt=""> <p>{{ item.name }}</p> </div> </div> <div class="btn" @click="start"></div> </div> </div>
第一個(gè)難點(diǎn):如何繪制扇形背景,而且扇形數(shù)量動(dòng)態(tài)?扇形背景我們可以使用css的裁剪屬性clip-path 或者css中的錐形漸變函數(shù)conic-gradient()來(lái)實(shí)現(xiàn),這里我們使用conic-gradient()
函數(shù)實(shí)現(xiàn)扇形背景更為簡(jiǎn)單。由于獎(jiǎng)品可配置數(shù)量不定,所以背景的扇形我們可以動(dòng)態(tài)計(jì)算生成,這里我們用到了vue3的計(jì)算屬性computed實(shí)現(xiàn)。
bgColor 計(jì)算屬性實(shí)現(xiàn)如下:
// 計(jì)算繪制轉(zhuǎn)盤(pán)背景 const bgColor = computed(() => { const _len = state.prizeList.length const colorList = ['#5352b3', '#363589'] let colorVal = '' for (let i = 0; i < _len; i++) { colorVal += `${colorList[i % 2]} ${rotateAngle.value * i}deg ${rotateAngle.value * (i + 1)}deg,` } return ` background: conic-gradient(${colorVal.slice(0, -1)}); ` })
根據(jù)獎(jiǎng)品數(shù)組長(zhǎng)度計(jì)算出每個(gè)扇形的角度,然后顏色可配置,rotateAngle計(jì)算屬性為所有獎(jiǎng)品平均分布在圓上的角度,這樣一個(gè)轉(zhuǎn)盤(pán)的背景就動(dòng)態(tài)繪制成功了。
第二個(gè)難點(diǎn):獎(jiǎng)品信息如何平均分布在背景扇形里,并且居中顯示呢?這里我們可以利用CSS transform
屬性中的旋轉(zhuǎn)函數(shù)rotate()來(lái)實(shí)現(xiàn),這里我們要均勻的分布,也需要使用計(jì)算屬性來(lái)實(shí)現(xiàn)。
prizeStyle(index) 計(jì)算屬性實(shí)現(xiàn)如下:
// 每個(gè)獎(jiǎng)品布局 const prizeStyle = computed(() => { const _degree = rotateAngle.value return (i) => { return ` width: ${2 * 270 * Math.sin(_degree / 2 * Math.PI / 180)}px; height: 270px; transform: rotate(${_degree * i + _degree / 2}deg); transform-origin: 50% 100%; ` } })
動(dòng)態(tài)計(jì)算每個(gè)獎(jiǎng)品的布局,通過(guò)rotate()
函數(shù)和獎(jiǎng)品索引計(jì)算每個(gè)div的旋轉(zhuǎn)角度,這里注意旋轉(zhuǎn)的源點(diǎn),我們所有獎(jiǎng)品的div布局是定位再圓心靠上,所以這里要改變旋轉(zhuǎn)原點(diǎn)為底部居中,值為transform-origin: 50% 100%
,這里才會(huì)均勻分布在背景圓上。
**第三個(gè)難點(diǎn):**因?yàn)樯刃未笮∈莿?dòng)態(tài)的,每個(gè)獎(jiǎng)品div盒子長(zhǎng)寬不能太大,要大概限制在扇形里面,這樣禮物圖片和禮物名字的布局才好限制不會(huì)超出扇形,我們?cè)趺从?jì)算獎(jiǎng)品布局的容器div大小呢?其實(shí)這里我們只需要計(jì)算出div的寬即可,因?yàn)楦邽閳A的半徑,那寬度如何確定?
如上圖,我們要計(jì)算大概寬度,即計(jì)算BC的長(zhǎng)度,A點(diǎn)為圓心,∠CAB和邊AC、AB我們已知,實(shí)際上這個(gè)問(wèn)題就變得簡(jiǎn)單了,知道角度和兩條鄰邊長(zhǎng)度,我們可以利用我們高中所學(xué)的正弦余弦函數(shù)來(lái)求BC的值了,不會(huì)還有人不會(huì)吧?然后利用Math對(duì)象中的Math.sin(x)
函數(shù)求出BC長(zhǎng)度即可,需要注意的是JavaScript中的正弦函數(shù)參數(shù)x是一個(gè)數(shù)值(以弧度為單位),而數(shù)據(jù)中是角度值,這里我們只要稍加轉(zhuǎn)化求出∠CAB所對(duì)應(yīng)的弧度長(zhǎng)即可,這里不會(huì)有人不會(huì)計(jì)算弧長(zhǎng)吧?不會(huì)吧?
寬度計(jì)算如下:
width: ${2 * 270 * Math.sin(_degree / 2 * Math.PI / 180)}px;
_degree為扇形角度,因?yàn)檎液瘮?shù)只能在直角三角形中使用,所以我們作輔助線(xiàn)構(gòu)造一個(gè)直角三角形來(lái)計(jì)算即可,不清楚的可以重溫下高中知識(shí),正弦余弦函數(shù),高考必考點(diǎn)哦。
這樣布局就基本完成了。
第二步:轉(zhuǎn)動(dòng)效果
思路:點(diǎn)擊抽獎(jiǎng)按鈕請(qǐng)求中獎(jiǎng)數(shù)據(jù),然后開(kāi)始轉(zhuǎn)動(dòng)轉(zhuǎn)盤(pán),這里抽獎(jiǎng)后臺(tái)實(shí)現(xiàn),前端請(qǐng)求接口即可,暫不討論。
第一個(gè)問(wèn)題:轉(zhuǎn)動(dòng)效果實(shí)現(xiàn)方式有很多,我們可以使用js實(shí)現(xiàn),也可以使用css方式實(shí)現(xiàn),眾所周知 ,能使用css實(shí)現(xiàn)的絕不使用js,優(yōu)勢(shì)不用我多說(shuō)吧,懂得都懂。那動(dòng)效如何實(shí)現(xiàn)呢?這里我們利用過(guò)渡屬性transition和旋轉(zhuǎn)函數(shù)rotate()即可輕松的實(shí)現(xiàn)轉(zhuǎn)動(dòng)特效,而且動(dòng)畫(huà)時(shí)長(zhǎng)和速度可以很方便的控制。
第二個(gè)問(wèn)題:css實(shí)現(xiàn)的話(huà)動(dòng)效停止提示和后續(xù)操作如何實(shí)現(xiàn)?我們使用了過(guò)渡屬性transition
,而恰好在js中有個(gè)監(jiān)聽(tīng)過(guò)渡屬性結(jié)束的事件transitionend,當(dāng)轉(zhuǎn)動(dòng)停止時(shí)就會(huì)觸發(fā)改事件,我們就可以做一些后續(xù)操作包括提示中獎(jiǎng)信息等。
部分代碼如下:
const startRun = () => { console.log(state.isRunning, totalRunAngle.value) // 設(shè)置動(dòng)效 prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value}deg); transition: all 4s ease; ` // 監(jiān)聽(tīng)transition動(dòng)效停止事件 prizeWrap.value.addEventListener('transitionend', stopRun) } const stopRun = (e) => { console.log(e) state.isRunning = false prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value - state.baseRunAngle}deg); ` }
點(diǎn)擊開(kāi)始轉(zhuǎn)動(dòng)后給轉(zhuǎn)動(dòng)的div設(shè)置旋轉(zhuǎn)度數(shù)和過(guò)渡效果,停止時(shí)移除過(guò)渡效果,然后旋轉(zhuǎn)角度重置到上一次中獎(jiǎng)的角度初始值。
完整Demo代碼
有不理解的地方可以copy代碼到自己本地調(diào)試,完整代碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } [v-cloak] { display: none; } .container { width: 540px; height: 540px; /*background: #98d3fc url('https://www.jq22.com/demo/vue-luck-draw-pdmm202010260015/img/bg.a4b976d5.png') no-repeat center / 100% 100%;*/ /*background: conic-gradient( red 6deg, orange 6deg 18deg, yellow 18deg 45deg, green 45deg 110deg, blue 110deg 200deg, purple 200deg);*/ margin: 100px auto; position: relative; } .prize-list { width: 100%; height: 100%; border-radius: 50%; border: 10px solid #98d3fc; overflow: hidden; } .prize-item { /*border: 2px solid red;*/ position: absolute; left: 0; right: 0; top: -10px; margin: auto; } .prize-item img { width: 30%; height: 20%; margin: 40px auto 10px; display: block; } .prize-item p { color: #fff; font-size: 12px; text-align: center; line-height: 20px; } .btn { width: 160px; height: 160px; background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/btn_lottery.png') no-repeat center / 100% 100%; position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; cursor: pointer; } .btn::before { content: ""; width: 41px; height: 39px; background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/icon_point.png') no-repeat center / 100% 100%; position: absolute; left: 0; right: 0; top: -33px; margin: auto; } </style> </head> <body> <div id="app" v-cloak> <div class="container"> <div class="prize-list" ref="prizeWrap" :style="bgColor"> <div class="prize-item" v-for="(item, index) in prizeList" :style="prizeStyle(index)"> <img :src="item.pic" alt=""> <p>{{ item.name }}</p> </div> </div> <div class="btn" @click="start"></div> </div> </div> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script> const { createApp, onMounted, onUnmounted, ref, reactive, toRefs, computed, nextTick } = Vue createApp({ setup () { const state = reactive({ prizeList: [ { name: '手機(jī)', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' }, { name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' }, { name: '蘋(píng)果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' }, { name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' }, { name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' }, { name: '木馬', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' }, { name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' }, { name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' } ], // 后臺(tái)配置的獎(jiǎng)品數(shù)據(jù) isRunning: false, // 是否正在抽獎(jiǎng) baseRunAngle: 360 * 5, // 總共轉(zhuǎn)動(dòng)角度 至少5圈 prizeId: 0, // 中獎(jiǎng)id }) const prizeWrap = ref(null) // 平均每個(gè)獎(jiǎng)品角度 const rotateAngle = computed(() => { const _degree = 360 / state.prizeList.length return _degree }) // 要執(zhí)行總角度數(shù) const totalRunAngle = computed(() => { return state.baseRunAngle + 360 - state.prizeId * rotateAngle.value - rotateAngle.value / 2 }) // 計(jì)算繪制轉(zhuǎn)盤(pán)背景 const bgColor = computed(() => { const _len = state.prizeList.length const colorList = ['#5352b3', '#363589'] let colorVal = '' for (let i = 0; i < _len; i++) { colorVal += `${colorList[i % 2]} ${rotateAngle.value * i}deg ${rotateAngle.value * (i + 1)}deg,` } return ` background: conic-gradient(${colorVal.slice(0, -1)}); ` }) // 每個(gè)獎(jiǎng)品布局 const prizeStyle = computed(() => { const _degree = rotateAngle.value return (i) => { return ` width: ${2 * 270 * Math.sin(_degree / 2 * Math.PI / 180)}px; height: 270px; transform: rotate(${_degree * i + _degree / 2}deg); transform-origin: 50% 100%; ` } }) onMounted(() => { prizeWrap.value.style = `${bgColor.value} transform: rotate(-${rotateAngle.value / 2}deg)` }) onUnmounted(() => { prizeWrap.value.removeEventListener('transitionend', stopRun) }) // 獲取隨機(jī)數(shù) const getRandomNum = () => { const num = Math.floor(Math.random() * state.prizeList.length) return num } const start = () => { if (!state.isRunning) { state.isRunning = true console.log('開(kāi)始抽獎(jiǎng),后臺(tái)請(qǐng)求中獎(jiǎng)獎(jiǎng)品') // 請(qǐng)求返回的獎(jiǎng)品編號(hào) 這里使用隨機(jī)數(shù) const prizeId = getRandomNum() console.log('中獎(jiǎng)ID>>>', prizeId, state.prizeList[prizeId]) state.prizeId = prizeId startRun() } } const startRun = () => { console.log(state.isRunning, totalRunAngle.value) // 設(shè)置動(dòng)效 prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value}deg); transition: all 4s ease; ` // 監(jiān)聽(tīng)transition動(dòng)效停止事件 prizeWrap.value.addEventListener('transitionend', stopRun) } const stopRun = (e) => { console.log(e) state.isRunning = false prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value - state.baseRunAngle}deg); ` } return { ...toRefs(state), bgColor, prizeStyle, prizeWrap, start } } }).mount('#app') </script> </body> </html>
以上就是Vue3實(shí)現(xiàn)轉(zhuǎn)盤(pán)抽獎(jiǎng)效果的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3轉(zhuǎn)盤(pán)抽獎(jiǎng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Vue-cli3 項(xiàng)目在安卓低版本系統(tǒng)和IE上白屏問(wèn)題解決
這篇文章主要介紹了Vue-cli3 項(xiàng)目在安卓低版本系統(tǒng)和 IE 上白屏問(wèn)題解決,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04vue中關(guān)于@media媒體查詢(xún)的使用
這篇文章主要介紹了vue中關(guān)于@media媒體查詢(xún)的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08vue2.0+webpack環(huán)境的構(gòu)造過(guò)程
本文分步驟給大家介紹了vue2.0+webpack環(huán)境的構(gòu)造過(guò)程的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11Element-ui中元素滾動(dòng)時(shí)el-option超出元素區(qū)域的問(wèn)題
這篇文章主要介紹了Element-ui中元素滾動(dòng)時(shí)el-option超出元素區(qū)域的問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05