最近项目中遇到了需要做个h5的抽奖活动需求:要求支持九宫格模式和圆形抽奖盘模式。这边基于vue和vantUI的loading组件造了这2个轮子。

九宫格抽奖
  • 思路
    动态构造几次方的正方形抽键盘的4条边;
    requestAnimationFrame来控制动画执行过程。

  • 重点过程

  1. requestAnimationFrame初始化兼容性
// 兼容性raf初始化
window.requestAniFrame = (function() {
  return window.requestAnimationFrame

  // Older versions Chrome/Webkit
  window.webkitRequestAnimationFrame ||
    // Firefox < 23
    window.mozRequestAnimationFrame ||
    // opera
    window.oRequestAnimationFrame ||
    // ie
    window.msRequestAnimationFrame ||
    function(callback) {
      return window.setTimeout(callback, 1000 / 60)
    }
})()

// 兼容性取消初始化
window.cancelAnimation = (function() {
  return (
    window.cancelAnimationFrame ||
    window.mozCancelAnimationFrame ||
    window.cancelRequestAnimationFrame ||
    function(id) {
      clearTimeout(id)
    }
  )
})()
  1. 根据传入的奖品项判断当前是几次方正方形(一个正方形有4条边有4个顶点这个是死的)
DINGDIAN:4,
BIAN:4
// 获取几次幂方阵
this.firstTurn = (this.ds.length + this.DINGDIAN) / this.BIAN

// 获取画第二条边 最多可画几个
this.secondNeedNumber = this.firstTurn - 1
  1. v-for下动态构造4条边
// 获取位置
getPosition(index, isSelect) {
  // 选中样式
  let selectStyle = ``
  if (isSelect) {
    const selectWidth = this.tWidth - 4
    const selectHeight = this.tHeight - 4
    selectStyle = `width:${px2vw(selectWidth)};height:${px2vw(
      selectHeight
    )};border:${px2vw(2)} solid ${this.turnBackground}`
  }

  // 第一条边 index 0 1 2
  if (index < this.firstTurn) {
    let i = index - 0 * (this.firstTurn - 1)
    return `top:0;left:${px2vw(i * this.tWidth)};${
      this.commonStyle
    };${selectStyle}`
  }
  // 第二条边 index值从 3 4
  if (index - this.firstTurn < this.secondNeedNumber) {
    let i = index - 1 * (this.firstTurn - 1)
    return `top:${px2vw(i * this.tWidth)};left:${px2vw(this.abWidth)};${
      this.commonStyle
    };${selectStyle}`
  }
  // 第三条边 5 6
  if (index <= 3 * (this.firstTurn - 1)) {
    let i = index - 2 * (this.firstTurn - 1)
    return `top:${px2vw(this.abHeight)};left:${px2vw(
      this.abWidth - i * this.tWidth
    )};${this.commonStyle};${selectStyle}`
  }
  // 第四条边 7
  if (index < this.ds.length) {
    let i = index - 3 * (this.firstTurn - 1)
    return `top:${px2vw(this.abHeight - i * this.tWidth)};left:0;${
      this.commonStyle
    };${selectStyle}`
  }
}
  1. 如何配合requestAnimationFrame
    使用该api它表示浏览器在1秒内根据频率指定刷新页面,一般都是60HZ,也就是1000/60 = 16.7 (ms)具体可以去了解下这个api,跟setInterval区别。
    这里有个不太好的处理东西就是我们不能去设置这个刷新频率,它是死的比如当前浏览器就是16.7ms执行一次。所以这个地方为了方便我们来控制执行,需要两个参数:当前执行动画时间Date.now()上次执行动画成功的时间lastSuccessTimer。这样的话我们可以使用Date.now()减去lastSuccessTimer来跟我们制定的“时间间隔”作比较,在这里时间间隔就相当于我们的频率。更改这个时间间隔来控制我们的速度快慢也就是!
// runfunc
onRunFunc() {
  let rafId = window.requestAniFrame(this.onRunFunc)
  this.curRafID = rafId
  let nowTimer = Date.now()
  if (nowTimer - this.lastSuccessTimer >= this.spaceTimer) {
    this.onRender(() => {
      window.cancelAnimation(rafId)
      // 恢复时间间隔
      this.spaceTimer = this.whichStartSpaceTimer
      this.isFinalCircle = false
      this.onOff = true
      this.isStart = false
    })
  }
}
  1. 关于滚动转圈
    这里借助vue的data数据绑定,我有个集合,集合对象中有个字段来判断当前是否已滚动到次处。没有滚动到此处就让其执行加加++,整个奖品转完后是为执行了一圈,这里我有个默认参数就是需要转几圈,到达指定转圈数后,就是需要滚动到指定的中奖位置。
    其实当时还有个疑惑就是我通过vue数据绑定来让其字段变化,执行频率这么快vue那边是否反映过来了,最开始也想直接操作dom,目前没发现这样使用有什么弊端。
onRender(cb) {
  if (this.isFinalCircle) {
    // 固定圈数转完后 在1位置
    if (this.curSelect === this.bingoIndex && this.onOff) {
      cb()
      return
    }
    this.spaceTimer += this.eachTimer
    this.curSelect++
    if (this.curSelect < this.ds.length) {
      this.ds[this.curSelect]['isSelect'] = true
      this.ds[this.curSelect - 1]['isSelect'] = false
    } else if (this.curSelect === this.ds.length) {
      this.ds[this.curSelect - 1]['isSelect'] = false
      this.ds[0]['isSelect'] = true
    } else {
      // 转完一圈
      this.curSelect = 0
      this.onOff = true
    }
    this.$nextTick(() => {
      this.lastSuccessTimer = Date.now()
    })
  } else {
    this.curSelect++
    if (this.curSelect < this.ds.length) {
      this.ds[this.curSelect]['isSelect'] = true
      this.ds[this.curSelect - 1]['isSelect'] = false
    } else if (this.curSelect === this.ds.length) {
      this.ds[this.curSelect - 1]['isSelect'] = false
      this.ds[0]['isSelect'] = true
    } else {
      // 转完一圈
      this.curSelect = 1
      this.ds[0]['isSelect'] = false
      this.ds[1]['isSelect'] = true
      this.countCircle++
      if (this.countCircle === this.defaultCircle) {
        this.countCircle = 0
        this.spaceTimer = this.finalStartTimer
        this.isFinalCircle = true
        if (this.bingoIndex === 1) {
          this.onOff = false
        }
      }
    }
    this.$nextTick(() => {
      this.lastSuccessTimer = Date.now()
    })
  }
}
  1. 关于中奖
    目前例子中是随机中奖
    this.bingoIndex = ~~(Math.random() * this.ds.length)
    真实业务上,当我们点击开始抽奖时回去调接口后端返回指定的中奖项,则直接根据返回值过滤出我们需要的中奖位置index值。
  • 效果截图
<nine-prize
      :ds="dsNinePrize4"
      prize-background="#BFEFFF"
      prize-item-background="#F0F0F0"
      pointer-text="GO!"
      pointer-background="#EEB422"
      turn-background="blue"
    ></nine-prize>

4奖品-抽奖盘

<nine-prize :ds="dsNinePrize"></nine-prize>

8奖品-抽奖盘

<nine-prize :ds="dsNinePrize12"></nine-prize>

12奖品-抽奖盘

圆形转盘抽奖
  • 思路
    根据指定跑马灯数量加css3旋转动态构造灯泡外层(交替变化是使用keyframe);
    分为奖盘和奖品两个div;
    根据奖品数量加css3旋转动态构造奖品项(360除以个数可得到圆心角);
    第一种是只支持偶数个的线型转盘模式(旋转计算);
    第二种是可指定块级转盘样式模式(三角函数算扇形);
    旋转方式通过transition来指定rotate的角度(固定角度+中奖商品角度);
    通过监听transitionend来得知动画结束。

  • 重点过程

  1. 如何画跑马灯
    使用绝对水平垂直居中,定义每个跑马灯外层div在最中心,在设置transition-origin来让其变化是根据中心点来变化,每个旋转角度是根据遍历i来乘以固定的圆心角度。
// 跑马灯
  &-lamp {
    position: absolute;
    width: 100%;
    height: 100%;
    // 小灯泡
    &__item {
      // 绝对居中来旋转
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      width: 10px;
      height: 100%;
      margin: 0 auto;
      transform-origin: center center;
      @keyframes change-color {
        0% {
          background: #fff;
        }
        100% {
          background: red;
        }
      }
      // 画小圆点
      &::before {
        content: "";
        position: absolute;
        top: 5px;
        right: 0;
        left: 0;
        width: 10px;
        height: 10px;
        margin: 0 auto;
        border-radius: 50%;
      }
      // 圆点颜色
      &:nth-of-type(even):before {
        background: #fff;
        animation: change-color 1s linear infinite;
      }
      &:nth-of-type(odd):before {
        background: red;
        animation: change-color 1s linear reverse infinite;
      }
    }
  }
  1. 如何画线型奖盘
    这里也是使用绝对水平垂直居中加css3的旋转来构造背景盘的分隔线,这里我默认给了该线条为1px(没处理移动端1px显示问题)
    线型
// 背景盘
    &__background {
      position: absolute;
      overflow: hidden;
      width: 100%;
      height: 100%;
      // 画线
      .background-line-item {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        width: 1px;
        height: 50%;
        margin: 0 auto;
        transform-origin: center bottom;
      }
    }
  1. 如何画奖品且中奖奖品居中
    这里需要注意的是我奖品默认都是宽度高度为奖盘的一半的小正方形,且起始位置在左上角,旋转中心是右下角来旋转。还有个关键的地方就是如何去计算整体奖品div的偏移量,这个是根据奖品数量结合360度来计算的。这样才能和奖盘对准。
这里正方形一个角为90度,居中取一半为固定值45: prizeRotateDegree 
一圈为360: CIRCLEDEGREE 
// 整个奖品盘旋转量 (奖品起始于左上角 则旋转量为)
// 这里目前该公式只适合锐角也就是奖品个数大于等于4
// 这里公式是90 - 360/len - (45 - 360/len/2) 下面进行化简
this.prizePlateRotate = this.prizeRotateDegree - this.CIRCLEDEGREE / 2 / len
  1. 如何画扇形奖盘
    这里要解决扇形问题:其实扇形是三角形,那么如何画三角形即利用border来画三角形再通过最外层overflow:hidden隐藏,因为外层是个border-radius:50%的圆形。具体计算逻辑见下面函数注释部分:
// 扇形背景渲染
renderSectorBackground() {
  const len = this.ds.length
  if (len === 1) {
    // 此时扇形为正方形
    this.sectorWidth = this.wheelWidth - 2 * 20
    this.sectorHeight = this.wheelWidth - 2 * 20
  } else if (len === 2) {
    // 此时扇形为半圆的长方形
    this.sectorWidth = this.wheelWidth - 2 * 20
    this.sectorHeight = (this.wheelWidth - 2 * 20) / 2
  } else {
    const sectorR = (this.wheelWidth - 2 * 20) / 2 // 半径
    // 这里看似画的扇形其实是画的三角形 利用overflow显示出来就是扇形了
    // 画三角形 其实是利用Border来画的 这里的起始点在右上角开始画
    // 这里保留的是上border 不要下border 而上border高度就是半径 需要求一手border的宽度左右高度
    // 而宽度是已半径作高 作切线 公式:x/半径 = tan(360/len/2)
    // js的math三角函数的参数不是度数而是弧度参数 所以角度再转弧度 360deg/2 等于 PI
    const radian = Math.PI / len // 弧度
    const bWidth = Math.tan(radian) * sectorR // 宽度
    this.borderWidth = px2vw(Math.ceil(bWidth)) // border宽度(左右border)
    this.borderHeight = px2vw(sectorR) // border高度(上border)
  }
  this.sectorRotateDegree = Math.ceil(this.CIRCLEDEGREE / len / 2)
  this.dsSector = len
},
  1. 如何根据中奖项来计算出需要转动的角度
    这里我有个默认转动几圈参数,一圈为360,默认5圈。现在就需要计算出转动到中奖项还有多少度。这里还有个容易出错的地方就是我的奖品项渲染是按顺时针来渲染的,但是中奖项到中心却是第一个,最后一个,倒数第二个依次类推,所以有个反的过程。
    三角形画扇形
// 开始转
onRun() {
  this.isStart = true
  const n = ~~(Math.random() * this.ds.length)
  // 这里有个区别 就是我渲染奖品是顺时针渲染
  // 但是旋转是逆时针的旋转 所以下标是反的
  let realN
  if (n === 0) {
    realN = 0
  } else {
    realN = this.ds.length - n
  }
  console.log('中奖下标为', realN)
  // 需要旋转度数
  // 这里公式是 i=0 -> 扇形圆心角 / 2 + 扇形圆心角 * 0
  //           i=1 -> 扇形圆心角 / 2 + 扇形圆心角 * 1
  const degree = this.eachSectorDegree / 2 + this.eachSectorDegree * n

  const fDegree = degree + this.turnTableCircle * this.CIRCLEDEGREE

  this.$refs.myWheelPrize.setAttribute(
    'style',
    `transition: transform ${this.turnTableTimer}s ease-out 0s;transform: rotate(${fDegree}deg);${this.commonStyle}`
  )
  // 监听transition事件完
  this.$refs.myWheelPrize.addEventListener('transitionend', () =>
    this.onTransitionEnd(degree)
  )
},
// 动画结束
onTransitionEnd(d) {
  this.$refs.myWheelPrize.setAttribute(
    'style',
    `transition: transform 0s ease-out 0s;transform: rotate(${d}deg);${this.commonStyle}`
  )
  this.$refs.myWheelPrize.removeEventListener(
    'transitionend',
    this.onTransitionEnd
  )
  this.isStart = false
}
  • 效果截图
<wheel-prize
      :ds="dsLinePrize"
      wheel-background-type="line"
      wheel-background="#0887f2"
      turn-table-background="#c6c7ca"
      turn-table-line-color="#333"
      pointer-background="yellow"
      pointer-text-color="#c6c7ca"
      pointer-text="GO!"
    ></wheel-prize>

在这里插入图片描述

<wheel-prize :ds="dsSectorPrize" wheel-background-type="sector"></wheel-prize>

在这里插入图片描述
其中代码中使用了js的加减乘除;使用了px2vw,css中是本地的mixin也就是Flex布局。若需使用则拿下来根据自己项目要求更改,其中配置项可在组件内查看,就没描述的那么清楚了。具体的业务逻辑可看源码!

九宫格及圆形源码

Logo

前往低代码交流专区

更多推荐