九宫格抽奖,大转盘抽奖,滚动抽奖,刮刮卡抽奖(三)

做了各种各样的营销抽奖活动,有九宫格抽奖、大转盘抽奖、滚动抽奖、刮刮卡抽奖,来总结一波~

九宫格抽奖
大转盘抽奖
滚动抽奖
刮刮卡抽奖

滚动抽奖

我们先来看下最终实现的效果,点击抽奖时三列奖品依次滚动,抽中一样的则代表中奖:

思路

通过改变每一列的位移来形成滚动效果,要想形成循环滚动效果有很多种方式,本文介绍以下三种(假如奖品有10个):

  • 将10个奖品竖着按顺序排列,想要滚动n圈,则将这10个奖品循环n次。
  • 将10个奖品竖着按顺序排列,最后再添加一次第一个奖品,滚动到该奖品时,瞬间将奖品列表拉回到位置0再继续滚动,形成循环。
  • 使用css的3D效果,将10个奖品摆成一个环形,环心是垂直于我们的,然后让其沿着X轴旋转形成滚动效果。

第一种实现

先来写静态页面:

ini 复制代码
<div id="app">
    <div class="lottery">
        <div 
            class="lottery__list" 
            v-for="item in lotteryList"
            :style="{transform:`translateY(${item.topVal}px)`,transition: `${transitionTime}s ease`}">
            <div class="lottery__item" v-for="item_1 in lotteryData">{{ item_1.name }}</div>
            <div class="lottery__item" v-for="item_1 in lotteryData">{{ item_1.name }}</div>
            <div class="lottery__item" v-for="item_1 in lotteryData">{{ item_1.name }}</div>
        </div>
    </div>
    <div class="btn" @click="handleLottery">抽奖</div>
</div>

可以看到有动态设置translateY的值,动画的时间也是动态的,这个是为什么呢?因为每次抽奖,我们都是从偏移位置为0的地方开始,所以开启下一次抽奖时,我们需要将动画时间设置为0,瞬间将列表滚至初始位置。然后再将动画时间改为合适的时间,让其开始滚动。

样式:

xml 复制代码
<style>
    .lottery {
        display: flex;
        width: 300px;
        height: 50px;
        overflow: hidden;
        border: 1px solid #ccc;
        margin: 200px auto 30px;
    }

    .lottery__list {
        flex: 1;
        border-right: 1px solid #ccc;
        text-align: center;
    }

    .lottery__list:nth-last-child(1) {
        border: 0;
    }

    .lottery__item {
        width: 100%;
        line-height: 50px;
    }

    .lottery__item:nth-child(2n) {
        background-color: red;
    }

    .lottery__item:nth-child(2n+1) {
        background-color: pink;
    }

    .btn {
        width: 100px;
        line-height: 30px;
        background-color: rgb(62, 11, 245);
        margin: 0 auto;
        border-radius: 20px;
        text-align: center;
        color: #fff;
    }
</style>

vue实现:

根据上面的静态页,我们首先需要渲染出我们的奖品列表,需要计算出滚动到每个奖品时需要往y轴方向偏移多少:

xml 复制代码
<script>
    const { createApp, reactive, toRefs, onMounted, nextTick } = Vue
    createApp({
        setup() {
            const state = reactive({
                lottery: [], // 用户获取到的奖品
                lotteryData: [], // 全部奖品
                lotteryList: [], // 每一列的滚动参数
                transitionTime: 3 // 动画时间
                loading: false
            })

            // 获取所有奖品及初始化每一列的滚动参数
            init()

            function init() {
                let data = []
                for (let i = 1; i <= 10; i++) {
                    data.push({
                        name: "奖品" + i,
                        value: i,
                        // 滚动到该奖品时需要偏移的距离,让上滚动,所以该距离为负值
                        // translateY = -(该奖品索引 - 1) * 每个奖品的高度50 - 默认先滚动两圈再滚动至奖品处,该两圈的偏移距离
                        topVal: -(i - 1) * 50 - 10 * 50 * 2 
                    })
                }
                state.lotteryData = data

                let list = []
                for (let i = 1; i <= 3; i++) {
                    list.push({
                        index: i,
                        topVal: 0,
                    })
                }
                state.lotteryList = list
            }

            // 抽奖函数
            function handleLottery(){}

            return {
                ...toRefs(state),
                handleLottery
            }
        }
    })

然后来实现我们的抽奖函数,点击抽奖时向后台请求接口获取奖品,根据该奖品对应的偏移距离给奖品列表设置translateY的值:

ini 复制代码
function getLottery() {
    // 实际业务中应该通过接口获取,这里采用随机数1-10模拟
    return Math.floor(Math.random() * 10 + 1)
}

function handleLottery() {
    if(state.loading) return
    state.loading = true
    state.lottery = []
    // 获取奖品
    const curLottery = {
        lottery1: getLottery(),
        lottery2: getLottery(),
        lottery3: getLottery(),
    }
    // 获取每一列的奖品信息(主要是拿偏移距离)
    for (let i = 0; i < state.lotteryData.length; i++) {
        if (state.lottery[0] && state.lottery[1] && state.lottery[2]) break
        if (state.lotteryData[i].value === curLottery.lottery1) {
            state.lottery[0] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
        if (state.lotteryData[i].value === curLottery.lottery2) {
            state.lottery[1] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
        if (state.lotteryData[i].value === curLottery.lottery3) {
            state.lottery[2] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
    }
    // 设置每列的偏移距离,形成滚动效果
    for (let i = 0; i < 3; i++) {
        // 初始化,先将奖品列表拉回初始位置
        state.transitionTime = 0
        state.lotteryList[i].topVal = 0
        // 通过定时器分别给每一列奖品列表设置位移和动画时间,使得每列奖品依次滚动
        setTimeout(() => {
            state.lotteryList[i].topVal = state.lottery[i].topVal
            state.transitionTime = 3
        }, i * 300)
    }
}

最后我们需要监听动画完成事件,弹出抽奖结果:

scss 复制代码
// 动画完成监听函数
function playFn(index) {
    // 因为最后一列是最后滚动的,所以最后一列结束滚动时才能弹出抽奖结果
    if(index === 2){
        if (state.lottery[0].value === state.lottery[1].value && state.lottery[0].value === state.lottery[2].value){
            alert("恭喜获得" + state.lottery[0].value)
        }else{
            alert("很遗憾,您未中奖")
        }
        state.loading = false
    }
}

onMounted(() => {
    nextTick(() => {
        for (let i = 0; i < 3; i++) {
            // 监听动画完成
            document.querySelectorAll('.lottery__list')[i].addEventListener('transitionend', () => {
                playFn(i)
            })
        }
    })

})

第二种实现

先来写静态页面:

xml 复制代码
<div id="app">
    <div class="lottery">
        <div class="lottery__list" v-for="item in lotteryList"
            :style="{transform:`translateY(${item.topVal}px)`, transition:`${item.transitionTime}s ${item.model}`}">
            <div class="lottery__item" v-for="item_1 in lotteryData">{{ item_1.name }}</div>
            <!-- 最后再添加一次第一个奖品,滚动到该奖品时,瞬间将奖品列表拉回到位置0再继续滚动,形成循环 -->
            <div class="lottery__item">{{ lotteryData[0].name }}</div>
        </div>
    </div>
    <div class="btn" @click="handleLottery">抽奖</div>
</div>

可以看到跟第一种实现除了dom结构不一样了之外,transition多加了一个值item.model,这个是用来做什么的呢?这个是用来设置过渡动画的类型的,在第一种实现中,我们只需要设置一次动画,过渡动画没有设置,默认是ease,是先快后慢的效果。但是在第二种方法中,我们需要先滚动到最后一个元素,再瞬间将奖品列表拉回到位置0处,然后再进行第二圈滚动,以此来形成循环滚动效果。所以第一圈和第二圈时我们需要将过渡动画的类型设置为匀速,最后一圈滚动时再将其设置为先快后慢。

样式跟第一种方法的样式是完全一样的,这里就不再赘述了。

vue实现:

跟第一种方法一样,我们先来初始化我们的奖品列表,并计算出每个奖品的偏移位置:

xml 复制代码
<script>
    const { createApp, reactive, toRefs, onMounted, nextTick } = Vue
    createApp({
        setup() {
            const state = reactive({
                lottery: [], // 用户获取到的奖品
                lotteryData: [], // 全部奖品
                lotteryList: [], // 每一列的滚动参数
                loading: false
            })

            // 获取所有奖品及初始化每一列的滚动参数
            init()

            function init() {
                let data = []
                for (let i = 1; i <= 10; i++) {
                    data.push({
                        name: "奖品" + i,
                        value: i,
                        topVal: -(i - 1) * 50, 
                    })
                }
                state.lotteryData = data

                let list = []
                for (let i = 1; i <= 3; i++) {
                    list.push({
                        index: i,
                        topVal: 0, // 每列奖品的偏移距离
                        transitionTime: 0, // 动画过渡时间
                        model: 'linear', // 动画过渡类型
                        loopTimes: 0, // 动画执行次数
                    })
                }
                state.lotteryList = list
            }

            // 抽奖函数
            function handleLottery(){}
            return {
                ...toRefs(state),
                handleLottery
            }
        }
    })

然后来实现我们的抽奖函数,点击抽奖时向后台请求接口获取奖品,根据该奖品对应的偏移距离给奖品列表设置translateY的值:

ini 复制代码
function getLottery() {
    return Math.floor(Math.random() * 10 + 1)
}

function handleLottery() {
    if(state.loading) return
    state.loading = true
    state.lottery = []
    // 获取奖品
    const curLottery = {
        lottery1: getLottery(),
        lottery2: getLottery(),
        lottery3: getLottery(),
    }
    // 获取每一列的奖品信息(主要是拿偏移距离)
    for (let i = 0; i < state.lotteryData.length; i++) {
        if (state.lottery[0] && state.lottery[1] && state.lottery[2]) break
        if (state.lotteryData[i].value === curLottery.lottery1) {
            state.lottery[0] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
        if (state.lotteryData[i].value === curLottery.lottery2) {
            state.lottery[1] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
        if (state.lotteryData[i].value === curLottery.lottery3) {
            state.lottery[2] = JSON.parse(JSON.stringify(state.lotteryData[i]))
        }
    }
    // 设置每列的偏移距离,形成滚动效果
    for (let i = 0; i < 3; i++) {
        // 每次抽奖前先初始化每一列的抽奖数据
        state.lotteryList[i].loopTimes = 0
        // 第一圈动画过渡类型设置为匀速
        state.lotteryList[i].model = 'linear'
        state.lotteryList[i].topVal = 0
        state.lotteryList[i].transitionTime = 0
        // 滚动第一圈,滚动完成后会触发动画完成监听函数,每一列错开300ms滚动,也可以同时滚动
        setTimeout(() => {
            // 将最后一个元素的偏移位置赋值给奖品列表
            state.lotteryList[i].topVal = state.lotteryData[state.lotteryData.length - 1].topVal - 50
            state.lotteryList[i].transitionTime = 1
        }, i * 300)
    }
}

与第一种方式主要不同的就是动画完成监听函数,当我们监听到第一次动画结束时,我们首先需要将奖品列表瞬间拉回位置0处,然后再重新设置偏移位置和动画过渡时间,使其进行第二次滚动。当我们监听到第二次动画结束时,同样的需要将奖品列表瞬间拉回位置0处,然后再重新设置偏移位置和动画过渡时间,另外我们还需要将动画过渡效果设置为先快后慢,因为最后一圈需要减速滚动至奖品处。

scss 复制代码
// 动画完成监听函数
function playFn(index) {
    const curList = state.lotteryList[index]
    const curLottery = state.lottery[index]

    curList.loopTimes++
    // 第一圈滚动结束
    if (curList.loopTimes <= 1) {
        // y的位移值设为0,动画时间设为0,瞬间将列表拉回位置0,形成循环滚动效果
        curList.topVal = 0
        curList.transitionTime = 0
        // 继续滚动第二圈
        setTimeout(() => {
            curList.topVal = state.lotteryData[state.lotteryData.length - 1].topVal - 50
            curList.transitionTime = 1
        }, 0)

    }
    // 第二圈滚动结束
    if (curList.loopTimes === 2) {
        curList.topVal = 0
        curList.transitionTime = 0
        // 速度慢慢变慢滚动到相应奖品处
        setTimeout(() => {
            curList.topVal = curLottery.topVal
            curList.transitionTime = 1
            curList.model = 'ease-out'
        }, 0)
    }

    // 判断是否中奖函数
    resultFn()
}

onMounted(() => {
    nextTick(() => {
        for (let i = 0; i < 3; i++) {
            // 监听动画完成
            document.querySelectorAll('.lottery__list')[i].addEventListener('transitionend', () => {
                playFn(i)
            })
        }
    })
})

最后我们需要来判断下是否中奖,这里的情况比较复杂,因为动画监听函数会被执行多次,我们要在最后一次执行动画监听函数时判断是否中奖,怎么判断是最后一次执行动画监听函数呢?回看我们之前的代码,发现每一列奖品列表中都有一个变量loopTimes,该变量记录了每一列动画的执行次数。原本以为所有的列都会执行三次动画,测试后才发现,如果抽中的是第一个奖品的话,第三次动画就不会执行了!!!所以我们需要分情况讨论:

ini 复制代码
fcuntion resultFn(){
    const firstLottery = state.lotteryData[0].value
    // 三列奖品相同才代表中奖
    if (state.lottery[0].value === state.lottery[1].value && state.lottery[0].value === state.lottery[2].value) {
        // 肯定是第三列最后执行完动画,因为它是延迟了2*300ms
        if (index === 2) {
            // 如果中奖奖品为第一个奖品,动画完成监听函数只会执行两次(因为第三次滚动是从0到0,不会触发动画完成监听函数),如果不是第一个奖品,则动画监听函数会执行三次
            if ((state.lottery[0].value === firstLottery && curList.loopTimes === 2) || curList.loopTimes === 3) {
                alert("恭喜获得" + state.lottery[0].value)
                state.loading = false
            }
        }
    } else {
        if(curList.loopTimes === 3){
            // 如果第三列的奖品不是第一个奖品,那么肯定是第三列最后执行完动画,因为它是延迟了2*300ms
            if (state.lottery[2].value !== firstLottery && index === 2) {
                alert("很遗憾,您未中奖")
                state.loading = false
            }
            // 如果第三列的奖品是第一个奖品,但是第二列的奖品不是第一个奖品,那么肯定是第二列最后执行完动画,因为它是延迟了1*300ms
            if (state.lottery[2].value === firstLottery && state.lottery[1].value !== firstLottery && index === 1) {
                alert("很遗憾,您未中奖")
                state.loading = false
            }
            // 如果第三列的奖品是第一个奖品,但是第二列的奖品也是第一个奖品,那么肯定是第一列最后执行完动画
            if (state.lottery[2].value === firstLottery && state.lottery[1].value === firstLottery && index === 0) {
                alert("很遗憾,您未中奖")
                state.loading = false
            }
        }
    }
}

这里真的好绕啊,但是暂时没有想到更好的处理方式 T^T......

第三种实现

先来了解下 CSS 3D 的基本概念,具体可参考文章CSS 3D 转换

要利用 CSS3 实现 3D 的效果,最主要的就是借助 transform-style 属性,当我们指定一个容器的 transform-style 的属性值为 preserve-3d 时,容器的子元素变会具有 3D 效果。也就是子元素可以相对于父元素的平面进行 3D 变换操作。另外我们还需要使用 perspective 属性定义 3D 元素距视图的距离。当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。总结就是父元素设置transform-style属性,子元素设置perspective属性,孙元素进行 3D 转换。

第三种实现方法是要使用到旋转的 3D 转换方法,旋转的 3D 转换方法有三种:

  • rotateX(angle):定义沿 X 轴的 3D 旋转。
  • rotateY(angle):定义沿 Y 轴的 3D 旋转。
  • rotateZ(angle):定义沿 Z 轴的 3D 旋转。

我们先来看下 2D 坐标轴和 3D 坐标轴区别:

可以看到 2D 坐标轴是平面的,3D 坐标轴是立体的。

看起来有点懵懵的,那就动手实践下吧~

静态页:

ini 复制代码
<div class="father">
    <div class="son">
        <div
        class="grandson"
        :style="`transform: rotateX(${currentDeg}deg)`"
        >翻转</div>
    </div>
</div>

样式:

css 复制代码
.father {
    position: relative;
    transform-style: preserve-3d;
}
.son {
    perspective: 3000;
    -webkit-perspective: 3000;
}
.grandson {
    position: absolute;
    width: 200px;
    height: 100px;
    border: 1px solid black;
}

vue实现:

scss 复制代码
<script>
    const { createApp, reactive, onMounted, toRefs } = Vue;
    createApp({
        setup() {
            const state = reactive({
                currentDeg: 0,
            });
            function add() {
                if (state.currentDeg < 180) {
                    state.currentDeg++;
                    requestAnimationFrame(add);
                }
            }
            onMounted(() => {
                requestAnimationFrame(add);
            });
            return {
                ...toRefs(state),
            };
        },
    }).mount('#app');
</script>

展示:

沿 Y 轴的 3D 旋转,将 rotateX 改为 rotateY

沿 Z 轴的 3D 旋转,将 rotateY 改为 rotateZ

了解了CSS 3D的基本概念,那就可以来开发抽奖了,具体实现可参考这位小伙伴的Vue<幸运抽奖-老虎机🎰>

下一篇来实现:刮刮卡抽奖

参考文章

1、一次性弄懂CSS3 3D

相关推荐
程序媛小果2 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长6 分钟前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
吕彬-前端38 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱40 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js