抽奖动画 - 大转盘抽奖
1.需求
抽奖是各类营销活动中最常见的一种形式,本产品需求大致如下:转盘周围跑马灯交替闪烁,点击抽奖,大转盘旋转,调用接口获取抽奖结果,大转盘指针指向对应的奖品。高保如下图1
图1-高保
2.整体思路
本需求要求跑马灯交替闪烁,那四周的跑马灯就不能是死的图片了,要用动画来实现,并且第奇数,偶数交替变换,这个使用vue中的动态属性可以实现。
其次小灯泡分布在四周,首先想到的是 transform: rotate(); 然后奖品图片也是分布在圆形四周,这个也可以用 transform: rotate()。
最后点击立即抽奖,包含奖品图片的整个dom旋转,这个使用animation+transform: rotate();可以实现。
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,下次有时间再研究。