手把手教你开发一个九宫格抽奖动画

背景

有一句是这么说的,“当你手里有一把锤子,眼中满世界都是钉子” 。笔者继上一篇 开发自己的第一个npm包 开发了一个列表项固定高度的虚拟列表组件之后,开发组件上瘾了。 最近项目中要实现一个九宫格转盘抽奖动画。感觉这个功能可以复用。也打算把它做成组件。有了上次开发组件探索出来的开发范式,这次开发组件少了许多开发组件之外的问题的羁绊。现在我们进入今天的正题。

效果演示

先看看最终的效果,点击抽奖按钮后,开始转动抽奖,抽奖动画由慢变快,接着保持匀速,快停下来的时候减速,比较贴合现实世界物体的转动效果。顺便说一下,文中的抽奖功能是采用vue3 + vite + ts开发的,如果你使用的是别的技术栈的话,需要改造一下才能使用。

channel.gif

实现思路

九宫格抽奖跑马灯动画是九宫格抽奖功能的核心。如下图所示: 通常九宫格跑马灯动画都是顺时针转动的,转动的顺序是[1, 2, 3, 6, 9, 8, 7, 4]。转动到哪个格子的时候,给这个格子加一个高亮样式,看起来就有闪烁的效果。另外,转动的速度是会变化的。刚开始是从初始速度加速到最快速度,接着保持匀速转动,快到中奖的格子时,要将速度降下来。怎么控制跑马灯转动速度呢?可以根据当前到达位置的步数和总共需要转动的步数来判断。比如说在前三分之一的路程,让定时器加速,最后六分之一的路程,让定时器加速,中间的路程,保持匀速,设置定时器的定时值为固定值。

image.png

动手实现

画静态页面

从网上找了三幅图,分别是10元立减金图片,谢谢参与图片,抽奖按钮图片,用这三幅图,生成9宫格素材数据列表。为什么要用9幅图而不是用一整张图呢,主要原因是如果用一整张图,设置跑马灯经过的格子时,高亮效果不太好实现。


  import { reactive } from 'vue';
  import { NineGridLottery } from '@lib/core';
  import RMBImg from '@/assets/10rmb.png';
  import LotteryBtnImg from '@/assets/lottery-btn.png';
  import ThankImg from '@/assets/thank.png';

  // 奖品列表
  const lotteryList = reactive([
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '抽奖', pic: LotteryBtnImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
  ]);

网页结构如下: index=4 时,这个位置是抽奖按钮,抽奖按钮一般交互比较多,所以用插槽的方式暴露给父组件,让父组件去定制。因为slot上不能绑定事件,所以需要在外层加个div,把点击事件绑定在外层的div上。考虑到不同的抽奖活动外观展示都不一样,所以抽奖的样式要允许父组件自定义。这里把抽奖组件的样式类名前缀交给父组件去配置。

<template>
  <div :class="`${props.classPrefix}-box`">
    <div
      v-for="(item, index) in props.lotteryList"
      :class="[`${props.classPrefix}-item`, { active: state.curIndex === index }]"
    >
      <template v-if="index !== 4">
        <img v-if="item.pic" :src="item.pic" class="pic" alt="" />
        <!-- <p class='text'>{{ item.name }}</p> -->
      </template>
      <div v-else @click="start()">
        <slot name="lotteryBtn" :itemData="item"> </slot>
      </div>
    </div>
  </div>
</template>

九宫格抽奖组件的样式是在父组件定义的,九宫格抽奖容器采用flex布局,需要注意的是 flex-wrap默认属性是不换行的,要将属性值设置成wrap。另外下载的png图片不太标准,有色彩的部分并不在png图片背景的正中央,需要向左偏移1px, 看起来左右才对称。转动高亮的效果使用区别于静止状态的背景图和阴影效果实现。

<style lang="less">
  .lottery-box {
    width: 375px;
    height: 375px;
    background: #ea0019;
    margin: 100px auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-items: center;
    .lottery-item {
      width: 125px;
      height: 125px;
      position: relative;

      &.active {
        box-shadow: 2px 2px 30px #ffe4c0;
        background-color: #ffe4c0;
      }

      &:nth-of-type(5) {
        cursor: pointer;
      }

      .pic {
        width: 100%;
        height: 100%;
        position: absolute;
        left: -1px;
      }

      .text {
        width: 100%;
        height: 20px;
        background: rgba(0, 0, 0, 0.5);
        color: #fff;
        font-size: 12px;
        text-align: center;
        line-height: 20px;
        position: absolute;
        left: 0;
        bottom: 0;
      }
    }
  }
</style>

让抽奖转盘转起来

首先确定一下九宫格转盘的跑动顺序。顺时针转动时,每个格子的下标为:

 // 跑动顺序
  const lotterySort = [0, 1, 2, 5, 8, 7, 6, 3];

每跑一圈,需要跑八步。到达中奖格子位置跑马灯动画需要跑动的总步数=基本圈数*8+中奖格子在跑动数组中的位置。中奖的id在未跑动前,要先调用后端接口,确定下来。

  // 总执行步数
  const totalSteps = computed(() => {
    return props.baseCircles * 8 + lotterySort.indexOf(props.winId);
  });

跑动时,需要动态设置跑动速率。每次跑动时,清除上一次的速率。每前进一步,重新创建定时器,调整速率。并对到达位置的格子进行高亮显示。当跑动的步数大于等于总步数时,给父组件发消息。

 const timer: Ref<ReturnType<typeof setTimeout> | null> = ref(null);
 const startRun = () => {
    // 延时器的速度要动态调节
    timer.value && clearTimeout(timer.value);

    // console.log(`已走步数=${state.curStep}, 执行总步数=${totalSteps.value}`);

    // 已走步数超过要执行总步数, 则停止
    if (state.curStep >= totalSteps.value) {
      state.isRunning = false;
      return emit('end');
    }

    // 高亮抽奖格子序号
    state.curIndex = lotterySort[state.curStep % 8];
    // 速度调整
    state.speed = calcSpeed(state.speed);

    timer.value = setTimeout(() => {
      state.curStep++;
      startRun();
    }, state.speed);
  };

跑动速率的计算方法,前三分之一的路程,从初始速率加速到最快速率,然后保持最快速率,匀速跑动,到了总路程最后的六分之一时,开始减速,犹如刹车一样,慢慢停下来。

  // 需要加速的前段步数
  const frontSteps = Math.floor(props.baseCircles * 8 * (1 / 3));
  // 需要减速的后段步数
  const midSteps = Math.floor(totalSteps.value * (5 / 6));

  // 计算速度
  const calcSpeed = (speed: number) => {
    // 最快最慢速度
    const { fastSpeed, slowSpeed } = props;
    // 前段加速,中段匀速,后段减速
    if (state.curStep < frontSteps && speed > fastSpeed) {
      speed = speed - Math.floor((props.initSpeed - fastSpeed) / frontSteps);
    } else if (state.curStep > midSteps && speed < slowSpeed) {
      speed = speed + Math.floor((slowSpeed - fastSpeed) / frontSteps / 5);
    }
    return speed;
  };

使用方法

组件有 7 个配置参数:

组件属性名 含义
lotteryList 抽奖列表
winId 中奖 id
classPrefix 九宫格容器和奖项的样式类名前缀
initSpeed 初始转动速度 单位 ms (可选)
baseCircles 基本转动圈数 (可选)
fastSpeed 最快转动速度 单位 ms (可选)
slowSpeed 最慢转动速度 单位 ms (可选)
@end 转动结束事件
<template>
  <NineGridLottery
    :lotteryList="lotteryList"
    :winId="options.winId"
    :initSpeed="options.initSpeed"
    :baseCircles="options.baseCircles"
    @end="handleEnd"
    classPrefix="lottery"
  >
    <template #lotteryBtn="{ itemData }">
      <img :src="itemData.pic" class="pic" alt="" />
    </template>
  </NineGridLottery>
</template>

<script setup lang="ts">
  import { reactive } from 'vue';
  import { NineGridLottery } from '@lib/core';
  import RMBImg from '@/assets/10rmb.png';
  import LotteryBtnImg from '@/assets/lottery-btn.png';
  import ThankImg from '@/assets/thank.png';

  // 奖品列表
  const lotteryList = reactive([
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '抽奖', pic: LotteryBtnImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
    { name: '谢谢参与', pic: ThankImg },
    { name: '10元立减金', pic: RMBImg },
  ]);

  // 后台配置的奖品数据
  const options = reactive({
    // 中奖id
    winId: 6,
    // 基本圈数 
    baseCircles: 4,
    // 抽奖转动速度
    initSpeed: 300,
    fastSpeed: 100,
    slowSpeed: 600,
  });

  const handleEnd = () => {
    alert('恭喜你中奖了');
  };
</script>

// 样式参照上文
<style lang="less"></style>

结语

vue3+vite4+ts版的九宫格转动抽奖主要功能点的实现到此就讲完了。如果你想获得本文示例的完整代码,请点击这里下载。另外,这个组件也上传到了npm官方网站,你可以点此查看,安装使用。

猜你喜欢

转载自juejin.im/post/7248168880572694588