前端搞一个扭蛋抽奖动效?

请叫我海龟先生
• 阅读 1172

最近新增一个抽奖小模块,就是扭蛋机的形式,产品给了参考网页,奈何不好扒下来用,只得自己动手干了,不多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>
点赞
收藏
评论区
推荐文章
VUE 实现一个简易老虎机
今天突然要做一个竖直滚动老虎机,可以设置中奖位置,以及中奖回调,然后再带点常规的滚动动画,还是有点意思,和之前的转盘抽奖有点类似,有兴趣可以看下。 简单分析下UI,ui的话,就简单点,三个列表从下往上滚动,搞个框罩住 css的活,应该简单。动画,老规矩,我们采用之前的方案,动态设置 css,可以搞定。设置中奖位置,我们可以想传递
刚刚好 刚刚好
1星期前
css问题
1、 在IOS中图片不显示(给图片加了圆角或者img没有父级) <div<img src""/</div div {width: 20px; height: 20px; borderradius: 20px; overflow: h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录 问题 用navicat导入数据时,报错: 原因这是因为当前的MySQL不支持datetime为0的情况。 解决修改sql\mode: sql\mode:SQL Mode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。 全局s
vue 简单实现 营销 转盘抽奖
1.0 思路整理 转盘抽奖很常见,之前也没写过,现在有空来写写,分析如下: 1.1 转盘旋转 ? 可以用 transform 的 rotate 来解决 1.2 旋转动画 ? transition 过渡来处理 1.3 停留目标位置及中奖提示 ? 通过控制旋转角度控制停留位置,中奖提示,考虑添加回调 1.1 开始行
晴空闲云 晴空闲云
1星期前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。 盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
Wesley13 Wesley13
11个月前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。 **前端数据转化与请求** var contracts = [ {id: '1', name: 'yanggb合同1'}, {id: '2', name: 'yanggb合同2'}, {id: '3', name: 'yang
Wesley13 Wesley13
11个月前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序 select * from table_name order id desc; 2.按照指定(多个)字段排序 select * from table_name order id desc,status desc; 3.按照指定字段和规则排序 selec
Wesley13 Wesley13
11个月前
MySQL分割一行为多行的思路
最近数据分析有需求,分析运营活动短信用户,但是发送短信的用户是通过 JSON 字符串数组存储在一个 text 字段的。内容类似于: ["user1", "user2", "user3"....] 数据分析想分析这些用户,那么就需要 in 这些用户查询。自己手动拼 SQL 太蛋疼,而且好几万几十万的用户,拼成SQL,复制粘贴也够蛋疼的。那
常用知识整理
# Javascript ## 判断对象是否为空 ```js Object.keys(myObject).length === 0 ``` ## 经常使用的三元运算 > 我们经常遇到处理表格列状态字段如 `status` 的时候可以用到 ``` vue
helloworld_34035044 helloworld_34035044
2个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。 uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid() 或 uuid(sep)参数说明:sep 布尔值,生成的uuid中是否包含分隔符'',缺省为