前端搞一个扭蛋抽奖小动画?

最近新增一个抽奖小模块,就是扭蛋机的形式,产品给了参考网页,奈何不好扒下来用,只得自己动手干了,不多bb,先看效果吧!

效果图:
在这里插入图片描述

动画分析

由上面gif可看出,整个动画分为个部分

  1. 扭蛋随机(也不算随机吧)在固定盒子内跳动
  2. 中奖扭蛋下落
  3. 中奖扭蛋移动到中心,并且逐渐放大
  4. 中奖扭蛋做出扭开姿势,缓慢打开

整个过程分析好了,貌似还不难,那就一步一步来实现

实现步骤一,盒子内随机跳动

在实现跳动前,先要做的一步是,尽可能把蛋摆放的随机自然一点,怎么做?当然是定位啦。 我比较懒,于是计算了大概边界位置(我将整个球的摆放,分为三层,第一层,当然是贴近盒子边缘,第二层就再其上方了,第三层类推,同时再找好左右边界位置)

初始位置计算:

// 这里用的是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>

ps: 7/22 修复一个bug

由于笔者在小球 style 处采用了绑定 calcStyle 方法,vue在更新dom时会调用这个方法,导致这个方法会运行多次,小球位置也随之发生变化,暂时采取如下方式

    data() {
            return {
            // data 处新增缓存变量
            styleCacheMap: {}
        }
    },
    computed: {
            calcStyle() {
                return function (index) {
                    // 作一次缓存处理
                    if(Object.keys(this.styleCacheMap).length && this.styleCacheMap[index]) {
                        return this.styleCacheMap[index]
                    }
                    let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%')  : 
                        (  index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`);
                    this.styleCacheMap[index] = {
                        width: '18%',
                        transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`,
                        top
                    }
                    return this.styleCacheMap[index]
                }
            }
        },
  • 4
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值