最近新增一个抽奖小模块,就是扭蛋机的形式,产品给了参考网页,奈何不好扒下来用,只得自己动手干了,不多bb,先看效果吧!
效果图:
动画分析
由上面gif可看出,整个动画分为四个部分
- 扭蛋随机(也不算随机吧)在固定盒子内跳动
- 中奖扭蛋下落
- 中奖扭蛋移动到中心,并且逐渐放大
- 中奖扭蛋做出扭开姿势,缓慢打开
整个过程分析好了,貌似还不难,那就一步一步来实现
实现步骤一,盒子内随机跳动
在实现跳动前,先要做的一步是,尽可能把蛋摆放的随机,自然一点,怎么做?当然是定位啦。 我比较懒,于是计算了大概边界位置(我将整个球的摆放,分为三层,第一层,当然是贴近盒子边缘,第二层就再其上方了,第三层类推,同时再找好左右边界位置)
初始位置计算:
// 这里用的是vue框架,扭蛋是通过v-for渲染出来的
computed: {
//动态绑定style
calcStyle() {
return function (index) {
let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') :
( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`);
return {
width: '18%',
transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`,
top
}
}
}
},
// 生成随机数
export const getRandomArbitrary = (min = 0, max)=> {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值
}
随机跳动,其实就是写好的动画,需要时只需添加上即可
// 其中一个动画
@keyframes move1 {
0% {
transform: rotate(-30deg);
left: 12.7%;
top: 57.9%;
}
26% {
transform: rotate(60deg);
left: 41.2%;
top: 8.9%;
}
44% {
transform: rotate(110deg);
left: 52.2%;
top: 21.8%;
}
64% {
transform: rotate(56deg);
left: 72%;
top: 38%;
}
100% {
transform: rotate(-30deg);
left: 12.7%;
top: 57.9%;
}
}
添加动画
itemBoxStyle.animation = `move$1 0.75s 6 linear`
实现步骤二,扭蛋下落
下落动画不难,定义好初始位置,和结束位置,同样添加合适动画就可以了 tips: 要注意一个问题,开始扭蛋是看不见的(可能需要定位层级改变),然后下落一定高度扭蛋可以看见了(我用 overflow: hidden; 去解决) css:
/* 下降动画 */
@keyframes upInDown {
0% {
opacity: 0;
}
100% {
opacity: 1;
top: 43%;
}
}
js添加:
resNode.classList.add('resulteDown')
实现步骤三,扭蛋移动到中心
要实现扭蛋移动到中心,并且逐渐放大,整个动画看似复杂,其实看你的思路,由于接触了之前的 flip思想,不懂的可以看之前的文章,只需知道中心位置和起始位置就可以计算出平移量,其他的就是细节处理 中心位置计算:
// 这里我采取先将其定位到中心位置,然后在获取位置,建议在加载时,就计算好
getEggEndLocation() {
const eggEnd = this.$refs.hitEgg
const style = {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(1.8)',
'z-index': '-1',
opacity: 0
}
for (const key in style) {
if (Object.hasOwnProperty.call(style, key)) {
eggEnd.style[key] = style[key]
}
}
this.lastSite = this.getEleLocation(eggEnd)
// 清除设置
for (const key in style) {
if (Object.hasOwnProperty.call(style, key)) {
eggEnd.style[key] = ''
}
}
},
// 获取元素位置
getEleLocation(ele) {
const { top,left } = ele.getBoundingClientRect()
return { top,left }
},
初始位置,直接用 getEleLocation 就可以了,有了起始和结束位置,就可以计算动画过程了
resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)`
resNode.classList.add('active2')
.active2{
transition: all 1.4s linear;
}
实现步骤四,扭一扭,然后打开
这一步就全是动画了,就不过多叙说
@keyframes upOpen {
0% {
transform: translate(0px,0px);
}
25% {
transform: translate(5px,0px);
}
50% {
transform: translate(-5px,0px);
}
70% {
transform-origin: -10% 85%;
transform: translate(0px,0px) rotate(0deg);
}
100% {
transform: rotate(-30deg);
transform-origin: -10% 85%;
}
}
@keyframes bottomOpen {
0% {
transform: translate(0px,0px);
}
25% {
transform: translate(-5px,0px);
}
50% {
transform: translate(5px,0px);
}
70% {
transform-origin: 6% 16%;
transform: translate(0px,0px) rotate(0deg);
}
100% {
transform: rotate(30deg);
transform-origin: 6% 16%;
}
}
最后就动画效果的复位,删除添加的class就可以了
全部代码:
<template>
<div>
<div class="gashapon" >
<button @click="toStart" >点击抽奖</button>
<!-- 蛋区 -->
<div class="egg_area" >
<div ref="eggBody" >
<div class="egg_box" v-for="(item,index) in imgIndex"
:style="calcStyle(index)"
:class="`egg_box${index+1}`"
:key="index">
<img :src="require(`../../../assets/egg/egg${item}.png`)" alt=""/>
</div>
</div>
</div>
<!-- 出口 -->
<div class="hit_box" ref="hitEggBox">
<!-- 出口蛋 -->
<div class="hit_egg" ref="hitEgg" >
<!-- 光 -->
<div class="light_box" v-show="lightShow" >
<img class="light_img" src="../../../assets/egg/e_sun.jpg" alt="">
</div>
<img src="../../../assets/egg/egg_top.png" alt="">
<img src="../../../assets/egg/egg_foot.png" alt="">
</div>
</div>
</div>
<van-overlay :show="show" :lock-scroll="true" />
</div>
</template>
<script>
import { getRandomArbitrary } from '../../../utils/lib'
import {
Overlay,
} from "vant"
export default {
name: 'EggMachine',
components: {
[Overlay.name]: Overlay,
},
data() {
return {
imgIndex: [1, 2, 3, 2, 2, 3, 1, 1, 2, 1],
moveIng: false,
lastSite: {},
show: false,
lightShow: false
}
},
created() {
},
async mounted() {
this.$nextTick(() => {
// 获取中心位置
setTimeout(() => {
this.getEggEndLocation()
},400)
})
},
computed: {
calcStyle() {
return function (index) {
let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') :
( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`);
return {
width: '18%',
transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`,
top
}
}
}
},
methods: {
async toStart() {
this.setNodeClass(true)
await this.delay(1500)
this.setNodeClass(false)
// 页面滚动到顶部,保证动画在中
window.scroll(0,0)
// 下降
this.eggDown()
},
// 节点class处理
async setNodeClass(add = true) {
const eggChild = this.$refs.eggBody.childNodes
for (let i = 0; i < 10; i++) {
const itemBoxStyle = eggChild[i].style
add ? itemBoxStyle.animation = `move${i+1} 0.75s 6 linear` : itemBoxStyle.animation = ''
}
this.moveIng = add
},
// 下降
async eggDown() {
const resNode = this.$refs.hitEgg
this.show = true
resNode.style.zIndex = '2'
resNode.classList.add('resulteDown')
await this.delay(1000)
// 记录当前位置
const { top,left } = resNode.getBoundingClientRect()
// 设置转变
this.$refs.hitEggBox.style.overflow = 'visible'
if(!Object.keys(this.lastSite).length) {
this.getEggEndLocation()
}
resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)`
resNode.classList.add('active2')
await this.delay(1800)
this.openEgg()
},
// 获取扭蛋结束中心位置
getEggEndLocation() {
const eggEnd = this.$refs.hitEgg
const style = {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(1.8)',
'z-index': '-1',
opacity: 0
}
for (const key in style) {
if (Object.hasOwnProperty.call(style, key)) {
eggEnd.style[key] = style[key]
}
}
this.lastSite = this.getEleLocation(eggEnd)
// 清除设置
for (const key in style) {
if (Object.hasOwnProperty.call(style, key)) {
eggEnd.style[key] = ''
}
}
},
// 打开动画
async openEgg() {
// 测试
const resNode = this.$refs.hitEgg
const resNodeImg = resNode.childNodes
// 添加打开动画
resNodeImg[1].classList.add('eggOpenTop')
resNodeImg[2].classList.add('eggOpenBottom')
await this.delay(900)
this.lightShow = true
await this.delay(900)
// console.log('抽奖结束')
this.$emit('darw-succes')
// 复位
this.$refs.hitEggBox.style.overflow = 'hidden'
resNodeImg[1].classList.remove('eggOpenTop')
resNodeImg[2].classList.remove('eggOpenBottom')
resNode.classList.remove('resulteDown')
resNode.classList.remove('active2')
resNode.style.transform = ''
resNode.style.zIndex = ''
this.show = false
this.lightShow = false
},
// 获取元素位置
getEleLocation(ele) {
const { top,left } = ele.getBoundingClientRect()
return { top,left }
},
// 延迟函数
async delay(time = 2000) {
return new Promise((res) => {
setTimeout(() => {
res()
},time)
})
},
}
}
</script>
<style lang="less" scoped>
.active2{
transition: all 1.4s linear;
}
.eggOpenTop {
animation: upOpen 1.2s linear;
animation-fill-mode: forwards;
}
.eggOpenBottom {
animation: bottomOpen 1.2s linear;
animation-fill-mode: forwards;
}
img{
width: 100%;
}
.gashapon{
min-height: 8rem;
background: url('../../../assets/egg/gashapon.png') no-repeat center;
background-size: 100% 100%;
position: relative;
}
.egg_area{
position: absolute;
left: 54.5%;
transform: translateX(-50%);
width: 5.2rem;
height: 4.5rem;
background-color: transparent;
border-radius: 50%;
top: 0.1rem;
z-index: 1;
}
.egg_box {
position: absolute;
}
.egg_box img {
width: 100%;
}
.hit_egg{
position: absolute;
width: 0.8rem;
top: -80%;
left: 49%;
transform: rotate(-45deg) translateX(-50%);
transform-origin:50% 50%;
img{
width: 100%;
&:nth-child(3){
margin-top: -0.1rem;
}
}
.light_box{
position: absolute;
width: 1rem;
overflow: hidden;
top: -0.1rem;
.light_img{
animation: rotateAni 0.8s infinite linear;
}
}
}
.hit_box{
position: absolute;
width: 1.6rem;
height: 2rem;
top: 71%;
left: 29%;
overflow: hidden;
}
.resulteDown {
animation: upInDown 0.6s cubic-bezier(0.390, 0.575, 0.565, 1.000);
animation-fill-mode: forwards;
}
/* ------- 10个蛋 ------- */
/* 前4个 */
.egg_box1 {
left: 16%;
}
.egg_box2 {
left: 32%;
}
.egg_box3 {
left: 48%;
}
.egg_box4 {
left: 64%;
}
/* 后四个 */
.egg_box5 {
left: 21%;
}
.egg_box6 {
left: 34%;
}
.egg_box7 {
left: 48%;
}
.egg_box8 {
left: 60%;
}
/* 后两个 */
.egg_box9 {
left: 48%;
}
.egg_box10 {
left: 37%;
}
// 放大动画
@keyframes rotateAni {
0%{
transform: scale(0.9);
}
100% {
transform: scale(1.1);
}
}
</style>
<style>
/* 打开动画 */
@keyframes upOpen {
0% {
transform: translate(0px,0px);
}
25% {
transform: translate(5px,0px);
}
50% {
transform: translate(-5px,0px);
}
70% {
transform-origin: -10% 85%;
transform: translate(0px,0px) rotate(0deg);
}
100% {
transform: rotate(-30deg);
transform-origin: -10% 85%;
}
}
@keyframes bottomOpen {
0% {
transform: translate(0px,0px);
}
25% {
transform: translate(-5px,0px);
}
50% {
transform: translate(5px,0px);
}
70% {
transform-origin: 6% 16%;
transform: translate(0px,0px) rotate(0deg);
}
100% {
transform: rotate(30deg);
transform-origin: 6% 16%;
}
}
/* 下降动画 */
@keyframes upInDown {
0% {
opacity: 0;
}
100% {
opacity: 1;
top: 43%;
}
}
/* 蛋滚动 */
@keyframes move1 {
0% {
transform: rotate(-30deg);
left: 12.7%;
top: 57.9%;
}
26% {
transform: rotate(60deg);
left: 41.2%;
top: 8.9%;
}
44% {
transform: rotate(110deg);
left: 52.2%;
top: 21.8%;
}
64% {
transform: rotate(56deg);
left: 72%;
top: 38%;
}
100% {
transform: rotate(-30deg);
left: 12.7%;
top: 57.9%;
}
}
@keyframes move2 {
0% {
transform: rotate(85deg);
left: 31.2%;
top: 57.9%;
}
23% {
transform: rotate(210deg);
left: 70%;
top: 36%;
}
45% {
transform: rotate(120deg);
left: 45%;
top: 8%;
}
72% {
transform: rotate(30deg);
left: 8%;
top: 34%;
}
100% {
transform: rotate(85deg);
left: 31.2%;
top: 57.9%;
}
}
@keyframes move3 {
0% {
transform: rotate(-10deg);
left: 50%;
top: 57.9%;
}
38% {
transform: rotate(-30deg);
left: 38%;
top: 11.4%;
}
65% {
transform: rotate(-50deg);
left: 7%;
top: 38.7%;
}
100% {
transform: rotate(-10deg);
left: 50%;
top: 57.9%;
}
}
@keyframes move4 {
0% {
transform: rotate(20deg);
left: 65%;
top: 59.9%;
}
35% {
transform: rotate(-30deg);
left: 53.4%;
top: 11.3%;
}
64% {
transform: rotate(-53deg);
left: 24.3%;
top: 56%;
}
100% {
transform: rotate(20deg);
left: 65%;
top: 59.9%;
}
}
@keyframes move5 {
0% {
transform: rotate(-65deg);
left: 61.4%;
top: 38%;
}
29% {
transform: rotate(-180deg);
left: 40%;
top: 11.5%;
}
53% {
transform: rotate(-222deg);
left: 9%;
top: 41.3%;
}
76% {
transform: rotate(-160deg);
left: 21.8%;
top: 57.9%;
}
100% {
transform: rotate(-65deg);
left: 61.4%;
top: 38%;
}
}
@keyframes move6 {
0% {
transform: rotate(16deg);
left: 44.2%;
top: 42%;
}
28% {
transform: rotate(-60deg);
left: 18%;
top: 57%;
}
40% {
transform: rotate(-45deg);
left: 8%;
top: 41.3%;
}
80% {
transform: rotate(70deg);
left: 52.7%;
top: 9.9%;
}
100% {
transform: rotate(16deg);
left: 44.2%;
top: 42%;
}
}
@keyframes move7 {
0% {
transform: rotate(-13deg);
left: 27.5%;
top: 39.9%;
}
17% {
transform: rotate(50deg);
left: 37.5%;
top: 57.9%;
}
44% {
transform: rotate(75deg);
left: 75%;
top: 41.3%;
}
67% {
transform: rotate(42deg);
left: 50.18%;
top: 8%;
}
100% {
transform: rotate(-13deg);
left: 27.5%;
top: 39.9%;
}
}
@keyframes move8 {
0% {
transform: rotate(46deg);
left: 14.4%;
top: 33.9%;
}
20% {
transform: rotate(97deg);
left: 45.6%;
top: 7.8%;
}
45% {
transform: rotate(143deg);
left: 76.8%;
top: 41.6%;
}
65% {
transform: rotate(85deg);
left: 64.6%;
top: 57%;
}
100% {
transform: rotate(46deg);
left: 14.4%;
top: 33.9%;
}
}
@keyframes move9 {
0% {
transform: rotate(65deg);
left: 36.4%;
top: 20%;
}
41% {
transform: rotate(130deg);
left: 74.3%;
top: 42.9%;
}
76% {
transform: rotate(94deg);
left: 46.5%;
top: 57.9%;
}
100% {
transform: rotate(65deg);
left: 36.4%;
top: 20%;
}
}
@keyframes move10 {
0% {
transform: rotate(-92deg);
left: 53.6%;
top: 22.11%;
}
20% {
transform: rotate(-142deg);
left: 37%;
top: 58.5%;
}
47% {
transform: rotate(-198deg);
left: 6.7%;
top: 37.3%;
}
67% {
transform: rotate(-135deg);
left: 23%;
top: 10.7%;
}
100% {
transform: rotate(-92deg);
left: 53.6%;
top: 22.11%;
}
}
</style>