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

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

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

滚动抽奖

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

思路

通过改变每一列的位移来形成滚动效果,要想形成循环滚动效果有很多种方式,本文介绍以下三种(假如奖品有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

相关推荐
崔庆才丨静觅39 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax