Vue3实现转盘抽奖效果的示例详解
作者:Liben
前言
之前的文章有写过九宫格抽奖功能【 Vue3实现九宫格抽奖功能 】, 有兴趣的可以看一看给个赞啥的,今天我们看看如何实现一个转盘抽奖功能。
之前公司也写过转盘抽奖功能,实现流程大概是设计师出个转盘抽奖设计图,礼品固定,后台请求接口返回中奖数据,然后前端去实现动效,这么做的缺点就比较固定,比如奖品无法配置,中奖概率也无法配置。我在各大网站都逛了一圈,基本上都上这种固定的抽奖配置,很少有动态配置奖品和中奖概率的转盘抽奖,基于此我今天就抽空来实现一个可动态配置奖品的转盘抽象功能。
功能与效果图
功能:
1、礼物可后台动态配置
2、转盘奖品布局和背景动态生成(重点)
3、转动速度和时间可配置,速度先快后慢
4、记录上一次停止位置,开始转动不重置,转动位置最终停止在转盘指针中间
5、动画停止可触发中奖提示或实现其它功能
6、中奖概率可配置(这里先不管,后面有机会总结一些抽奖算法)
效果图如下:
实现
第一步:转盘布局
思路:首先需要一个转盘,然后需要一个抽奖按钮定位在中间,按钮定位在中间很简单,难的是扇形背景颜色如何实现,奖品如何均匀分布在每个扇形中间,因为奖品和可配置的,所以扇形大小可变化的。
<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>
第一个难点:如何绘制扇形背景,而且扇形数量动态?扇形背景我们可以使用css的裁剪属性clip-path 或者css中的锥形渐变函数conic-gradient()来实现,这里我们使用conic-gradient()
函数实现扇形背景更为简单。由于奖品可配置数量不定,所以背景的扇形我们可以动态计算生成,这里我们用到了vue3的计算属性computed实现。
bgColor 计算属性实现如下:
// 计算绘制转盘背景 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)}); ` })
根据奖品数组长度计算出每个扇形的角度,然后颜色可配置,rotateAngle计算属性为所有奖品平均分布在圆上的角度,这样一个转盘的背景就动态绘制成功了。
第二个难点:奖品信息如何平均分布在背景扇形里,并且居中显示呢?这里我们可以利用CSS transform
属性中的旋转函数rotate()来实现,这里我们要均匀的分布,也需要使用计算属性来实现。
prizeStyle(index) 计算属性实现如下:
// 每个奖品布局 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%; ` } })
动态计算每个奖品的布局,通过rotate()
函数和奖品索引计算每个div的旋转角度,这里注意旋转的源点,我们所有奖品的div布局是定位再圆心靠上,所以这里要改变旋转原点为底部居中,值为transform-origin: 50% 100%
,这里才会均匀分布在背景圆上。
**第三个难点:**因为扇形大小是动态的,每个奖品div盒子长宽不能太大,要大概限制在扇形里面,这样礼物图片和礼物名字的布局才好限制不会超出扇形,我们怎么计算奖品布局的容器div大小呢?其实这里我们只需要计算出div的宽即可,因为高为圆的半径,那宽度如何确定?
如上图,我们要计算大概宽度,即计算BC的长度,A点为圆心,∠CAB和边AC、AB我们已知,实际上这个问题就变得简单了,知道角度和两条邻边长度,我们可以利用我们高中所学的正弦余弦函数来求BC的值了,不会还有人不会吧?然后利用Math对象中的Math.sin(x)
函数求出BC长度即可,需要注意的是JavaScript中的正弦函数参数x是一个数值(以弧度为单位),而数据中是角度值,这里我们只要稍加转化求出∠CAB所对应的弧度长即可,这里不会有人不会计算弧长吧?不会吧?
宽度计算如下:
width: ${2 * 270 * Math.sin(_degree / 2 * Math.PI / 180)}px;
_degree为扇形角度,因为正弦函数只能在直角三角形中使用,所以我们作辅助线构造一个直角三角形来计算即可,不清楚的可以重温下高中知识,正弦余弦函数,高考必考点哦。
这样布局就基本完成了。
第二步:转动效果
思路:点击抽奖按钮请求中奖数据,然后开始转动转盘,这里抽奖后台实现,前端请求接口即可,暂不讨论。
第一个问题:转动效果实现方式有很多,我们可以使用js实现,也可以使用css方式实现,众所周知 ,能使用css实现的绝不使用js,优势不用我多说吧,懂得都懂。那动效如何实现呢?这里我们利用过渡属性transition和旋转函数rotate()即可轻松的实现转动特效,而且动画时长和速度可以很方便的控制。
第二个问题:css实现的话动效停止提示和后续操作如何实现?我们使用了过渡属性transition
,而恰好在js中有个监听过渡属性结束的事件transitionend,当转动停止时就会触发改事件,我们就可以做一些后续操作包括提示中奖信息等。
部分代码如下:
const startRun = () => { console.log(state.isRunning, totalRunAngle.value) // 设置动效 prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value}deg); transition: all 4s ease; ` // 监听transition动效停止事件 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); ` }
点击开始转动后给转动的div设置旋转度数和过渡效果,停止时移除过渡效果,然后旋转角度重置到上一次中奖的角度初始值。
完整Demo代码
有不理解的地方可以copy代码到自己本地调试,完整代码如下:
<!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: '手机', 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: '苹果', 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' } ], // 后台配置的奖品数据 isRunning: false, // 是否正在抽奖 baseRunAngle: 360 * 5, // 总共转动角度 至少5圈 prizeId: 0, // 中奖id }) const prizeWrap = ref(null) // 平均每个奖品角度 const rotateAngle = computed(() => { const _degree = 360 / state.prizeList.length return _degree }) // 要执行总角度数 const totalRunAngle = computed(() => { return state.baseRunAngle + 360 - state.prizeId * rotateAngle.value - rotateAngle.value / 2 }) // 计算绘制转盘背景 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)}); ` }) // 每个奖品布局 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) }) // 获取随机数 const getRandomNum = () => { const num = Math.floor(Math.random() * state.prizeList.length) return num } const start = () => { if (!state.isRunning) { state.isRunning = true console.log('开始抽奖,后台请求中奖奖品') // 请求返回的奖品编号 这里使用随机数 const prizeId = getRandomNum() console.log('中奖ID>>>', prizeId, state.prizeList[prizeId]) state.prizeId = prizeId startRun() } } const startRun = () => { console.log(state.isRunning, totalRunAngle.value) // 设置动效 prizeWrap.value.style = ` ${bgColor.value} transform: rotate(${totalRunAngle.value}deg); transition: all 4s ease; ` // 监听transition动效停止事件 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实现转盘抽奖效果的示例详解的详细内容,更多关于Vue3转盘抽奖的资料请关注脚本之家其它相关文章!