做了各种各样的营销抽奖活动,有九宫格抽奖、大转盘抽奖、滚动抽奖、刮刮卡抽奖,来总结一波~
滚动抽奖
我们先来看下最终实现的效果,点击抽奖时三列奖品依次滚动,抽中一样的则代表中奖:
思路
通过改变每一列的位移来形成滚动效果,要想形成循环滚动效果有很多种方式,本文介绍以下三种(假如奖品有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<幸运抽奖-老虎机🎰>。
下一篇来实现:刮刮卡抽奖