微信小程序九宫格抽奖和转盘抽奖的实现

描述:

最近在工作中遇到了小程序抽奖的一些页面制作,以前制作网页抽奖的时候,在jQuery插件库谁便找一个Demo就可以满足我们的需求了,自己动手写的话不仅会拖慢项目进度,而且写出来的也未必有别人的那么流畅自然。而在小程序中我们就没有这么幸福了(ಥ﹏ಥ),并没有丰富的插件库供给我们使用,所以该写的还得自己动手写。好了,下面是常见的两种抽奖方式:九宫格和转盘,在实际项目中用的还不错,分享给大家。


一、九宫格抽奖:

效果:
在这里插入图片描述

思路:
   首先是结构上,用弹性布局就可以了,每一个奖品的宽度设置成3分之1,超出3个后让它换行;然后需要定义一个数组用来存放界面的奖品, 因为第五个内容是按钮,所以需要设置一个类型字段用来区分奖品和按钮。又因为需要一个旋转动画的效果,所以需要在奖品数组中设置一个是否已选中的状态字段。
   接着是逻辑上,因为旋转的轨迹是顺时针的,所以得出轨迹的执行顺序数组为:[0, 1, 2, 5, 8, 7, 6, 3],接着我们只需要循环这个数组,在里面设置一个定时器,让它能够在n秒后把第n个奖品的状态改为已选中,这样经过一个循环后,就可以实现动画旋转一圈的视觉效果了;可以旋转一圈后我们可以把这段代码先封装起来,接着我们只需要去循环这段代码,并且再套一个计时器,这样就可以实现旋转多圈的效果了。
   想法是完美的,但是实际项目中还存在很多的因素,比如说旋转每一圈中的衔接时间,调教旋转中的速度,让动画在最后一圈停留在对应的奖品上,这都是开发时要攻破的难关,具体还得认真观看下面代码。

wxml:

<view class="body">
  <view class="box">
    <block wx:for="{{prize_arr}}" wx:key="id">
      <!-- 判断是奖品还是抽奖按钮 -->
      <view wx:if="{{item.type=='btn'}}" bindtap="clickPrize" class="prize btn {{isTurnOver?'':'grayscale'}}">
        {{item.name}}
      </view>
      <view wx:else class="prize {{item.isSelected?'selected':''}}">{{item.name}}</view>
    </block>
  </view>
</view>

wxss:

.body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100vh;
}

.box {
  display: flex;
  flex-wrap: wrap;
  width: 600rpx;
}

.box .prize {
  width: 200rpx;
  height: 200rpx;
  line-height: 200rpx;
  text-align: center;
  font-size: 40rpx;
  font-weight: 700;
  color: white;
  background-color: orange;
  border: 2px solid snow;
  box-sizing: border-box;
}

.box .btn {
  color: tomato;
  background-color: gold;
}

.box .selected {
  background-color: orangered;
}

.grayscale {
  filter: grayscale(50%);
}

js:

Page({

  /**
   * 页面的初始数据
   */
  data: {
    prize_arr: [
      {id: '1',type: 'prize',name: '奖品1',isSelected: false},
      {id: '2',type: 'prize',name: '奖品2',isSelected: false},
      {id: '3',type: 'prize',name: '奖品3',isSelected: false},
      {id: '4',type: 'prize',name: '奖品8',isSelected: false},
      {id: '5',type: 'btn',name: '按钮',isSelected: false},
      {id: '6',type: 'prize',name: '奖品4',isSelected: false},
      {id: '7',type: 'prize',name: '奖品7',isSelected: false},
      {id: '8',type: 'prize',name: '奖品6',isSelected: false},
      {id: '9',type: 'prize',name: '奖品5',isSelected: false},
    ],
    // 抽奖状态,是否转完了
    isTurnOver: true
  },

  // 点击抽奖
  clickPrize() {
    // 如果不在抽奖状态中,则执行抽奖旋转动画
    if (this.data.isTurnOver) {
      // 把抽奖状态改为未完成
      this.setData({
        isTurnOver: false
      })
      // 这里开始假设已经调取后端接口拿到抽奖后返回的ID
      let prize_id = 7;
      // 随机奖品效果
      // const rand = (m, n) => {
      //   return Math.ceil(Math.random() * (n - m + 1) + m - 1)
      // }
      // let prize_id = rand(1, 8)
      // 调用抽奖方法
      this.lottery(prize_id);
    } else {
      wx.showToast({
        title: '请勿重复点击',
        icon: 'none'
      })
    }
  },

  // 抽奖旋转动画方法
  lottery(prize_id) {
    console.log('中奖ID:' + prize_id)
    /*
     * 数组的长度就是最多所转的圈数,最后一圈会转到中奖后的位置
     * 数组里面的数字表示从一个奖品跳到另一个奖品所需要的时间
     * 数字越小速度越快
     * 想要修改圈数和速度的,更改数组个数和大小即可
     */
    let num_interval_arr = [90, 80, 70, 60, 50, 50, 50, 100, 150, 250];
    // 旋转的总次数
    let sum_rotate = num_interval_arr.length;
    // 每一圈所需要的时间
    let interval = 0;
    num_interval_arr.forEach((delay, index) => {
      setTimeout(() => {
        this.rotateCircle(delay, index + 1, sum_rotate, prize_id);
      }, interval)
      //因为每一圈转完所用的时间是不一样的,所以要做一个叠加操作
      interval += delay * 8;
    })
  },

  /*
   * 封装旋转一圈的动画函数,最后一圈可能不满一圈
   * delay:表示一个奖品跳到另一个奖品所需要的时间
   * index:表示执行到第几圈
   * sum_rotate:表示旋转的总圈数
   * prize_id:中奖后的id号
   */
  rotateCircle(delay, index, sum_rotate, prize_id) {
    // console.log(index)
    let _this = this;
    /*
     * 页面中奖项的实际数组下标
     * 0  1  2
     * 3     5
     * 6  7  8
     * 所以得出转圈的执行顺序数组为 ↓
     */
    let order_arr = [0, 1, 2, 5, 8, 7, 6, 3];
    // 页面奖品总数组
    let prize_arr = this.data.prize_arr;
    // 如果转到最后以前,把数组截取到奖品项的位置
    if (index == sum_rotate) {
      order_arr.splice(prize_id)
    }
    for (let i = 0; i < order_arr.length; i++) {
      setTimeout(() => {
        // 清理掉选中的状态
        prize_arr.forEach(e => {
          e.isSelected = false
        })
        // 执行到第几个就改变它的选中状态
        prize_arr[order_arr[i]].isSelected = true;
        // 更新状态
        _this.setData({
          prize_arr: prize_arr
        })
        // 如果转到最后一圈且转完了,把抽奖状态改为已经转完了
        if (index == sum_rotate && i == order_arr.length - 1) {
          _this.setData({
            isTurnOver: true
          })
        }
      }, delay * i)
    }
  }

})

衔接版js: 非第一次转时,会把上一次未跳完的格子转完,逻辑就是点击抽奖时做个标记来判断是否需要把没跳完的格子跳完。

Page({

  /**
   * 页面的初始数据
   */
  data: {
    prize_arr: [
      {id: '1',type: 'prize',name: '奖品1',isSelected: false},
      {id: '2',type: 'prize',name: '奖品2',isSelected: false},
      {id: '3',type: 'prize',name: '奖品3',isSelected: false},
      {id: '4',type: 'prize',name: '奖品8',isSelected: false},
      {id: '5',type: 'btn',name: '按钮',isSelected: false},
      {id: '6',type: 'prize',name: '奖品4',isSelected: false},
      {id: '7',type: 'prize',name: '奖品7',isSelected: false},
      {id: '8',type: 'prize',name: '奖品6',isSelected: false},
      {id: '9',type: 'prize',name: '奖品5',isSelected: false},
    ],
    /*
     * 数组的长度就是最多所转的圈数,最后一圈会转到中奖后的位置
     * 数组里面的数字表示从一个奖品跳到另一个奖品所需要的时间
     * 数字越小速度越快
     * 想要修改圈数和速度的,更改数组个数和大小即可
     */
    num_interval_arr: [90, 80, 70, 60, 50, 50, 50, 100, 150, 250],
    // num_interval_arr: [90, 80],
    /*
     * 页面中奖项的实际数组下标
     * 0  1  2
     * 3     5
     * 6  7  8
     * 所以得出转圈的执行顺序数组为 ↓
     */
    order_arr: [0, 1, 2, 5, 8, 7, 6, 3],
    // 抽奖状态,是否转完了
    isTurnOver: true,
    // 是否需要复原,把没转完的圈转完
    isRecover: false,
    // 记录上一次抽奖后的奖品id
    prize_id_last: ''
  },

  // 点击抽奖
  clickPrize() {
    // 如果不在抽奖状态中,则执行抽奖旋转动画
    if (this.data.isTurnOver) {
      // 把抽奖状态改为未完成
      this.setData({
        isTurnOver: false
      })
      // 这里开始假设已经调取后端接口拿到抽奖后返回的ID
      let prize_id = 7;
      // 随机奖品效果
      // const rand = (m, n) => {
      //   return Math.ceil(Math.random() * (n - m + 1) + m - 1)
      // }
      // let prize_id = rand(1, 8)
      // 调用抽奖方法
      this.lottery(prize_id);
    } else {
      wx.showToast({
        title: '请勿重复点击',
        icon: 'none'
      })
    }
  },

  // 抽奖旋转动画方法
  async lottery(prize_id) {
    console.log('中奖ID:' + prize_id)
    // 如果不是第一次抽奖,需要等待上一圈没跑完的次数跑完再执行
    this.recover().then(() => {
      let num_interval_arr = this.data.num_interval_arr;
      let order_arr = this.data.order_arr;
      // 旋转的总次数
      let sum_rotate = num_interval_arr.length;
      // 每一圈所需要的时间
      let interval = 0;
      num_interval_arr.forEach((delay, index) => {
        setTimeout(() => {
          this.rotateCircle(delay, index + 1, sum_rotate, prize_id, order_arr);
        }, interval)
        //因为每一圈转完所用的时间是不一样的,所以要做一个叠加操作
        interval += delay * 8;
      })
    })
  },

  /*
   * 封装旋转一圈的动画函数,最后一圈可能不满一圈
   * delay:表示一个奖品跳到另一个奖品所需要的时间
   * index:表示执行到第几圈
   * sum_rotate:表示旋转的总圈数
   * prize_id:中奖后的id号
   * order_arr_pre:表示旋转这一圈的执行顺序
   */
  rotateCircle(delay, index, sum_rotate, prize_id, order_arr_pre) {
    // console.log(index)
    let _this = this;
    // 页面奖品总数组
    let prize_arr = this.data.prize_arr;
    // 执行顺序数组
    let order_arr = []
    // 如果转到最后以前,把数组截取到奖品项的位置
    if (index == sum_rotate) {
      order_arr = order_arr_pre.slice(0, prize_id)
    } else {
      order_arr = order_arr_pre;
    }
    for (let i = 0; i < order_arr.length; i++) {
      setTimeout(() => {
        // 清理掉选中的转态
        prize_arr.forEach(e => {
          e.isSelected = false
        })
        // 执行到第几个就改变它的选中状态
        prize_arr[order_arr[i]].isSelected = true;
        // 更新状态
        _this.setData({
          prize_arr: prize_arr
        })
        // 如果转到最后一圈且转完了,并且是非重置圈,把抽奖状态改为已经转完了
        if (index === sum_rotate && i === order_arr.length - 1 && !this.data.isRecover) {
          _this.setData({
            isTurnOver: true,
            isRecover: true,
            prize_id_last: prize_id
          })
        }
      }, delay * i)
    }
  },

  // 复原,把上一次抽奖没跑完的次数跑完
  async recover() {
    if (this.data.isRecover) { // 判断是否需要重置操作
      let delay = this.data.num_interval_arr[0]; // 为了衔接流程,使用第一圈的转速
      // console.log(delay)
      let order_arr = this.data.order_arr;
      // console.log(order_arr)
      let prize_id_last = this.data.prize_id_last; // 上一次抽奖的id
      // console.log(prize_id_last)
      order_arr = order_arr.slice(prize_id_last); // 截取未跑完的格子数组
      // console.log(order_arr)
      return await new Promise(resolve => { // 确保跑完后才去执行新的抽奖
        this.rotateCircle(delay, 1, 1, 8, order_arr); // 第一圈的速度,最多只有一圈,旋转一圈,跑到最后一个奖品为止,未跑完的数组
        setTimeout(() => { // 确保跑完后才告诉程序不用重置复原了
          this.setData({
            isRecover: false,
          })
          resolve() // 告诉程序Promise执行完了
        }, delay * order_arr.length)
      })
    }
  }

})

二、转盘抽奖:

效果:
在这里插入图片描述
思路:
   首先是结构上,我们从设计那拿到psd后,把转盘背景和指针按钮分离成两张图片就好,接着用弹性布局或悬浮布局定一下位置就好。
   接着是逻辑上,我的第一想法使用css3的animation+transform:rotate()来制作,但是想了一下要动态设置旋转的角度,小程序也不好操作dom,所以直接使用官方提供的Animation API来制作了。需要注意一下我这转盘是有6个奖项,所以一个奖项的度数为:60,根据自己的项目,换算一下就好了;另外需要注意的是小程序是否要执行动画是看当前状态和要执行的实例数据间是否存在差值,换句话说就是如果我第一次抽到3等奖,我第二次又是抽中了3等奖,旋转的度数没有变,那么小程序会判断为当前不存在差值,所以会偷懒,直接不转了,具体解决方案看代码。

wxml:

<view class="body">
  <view class="box">
    <image class="bg" src="{{bg}}" animation="{{animationRotate}}"></image>
    <image bindtap="lottery" class="arrows {{isTurnOver?'':'grayscale'}}" src="{{arrows}}"></image>
  </view>
</view>

wxss:

.body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100vh;
}

.box {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 600rpx;
  height: 600rpx;
}

.box .bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.box .arrows {
  position: relative;
  top: -28rpx;
  width: 200rpx;
  height: 249rpx;
}

.grayscale {
  filter: grayscale(70%);
}

js:

Page({

  /**
   * 页面的初始数据
   */
  data: {
    // 转盘奖品背景
    bg: "http://r.photo.store.qq.com/psc?/V14ZaBeY40XWC8/45NBuzDIW489QBoVep5mcT0Lq.gBxwEb6.hf44ibdoBF*Yne9.pzFu6WW1t*k2C7iFgwK3jc0jMvFefvW0IfraslW4FNcfHPYyIdFLecyJ0!/r",
    // 转盘箭头抽奖按钮
    arrows: "http://r.photo.store.qq.com/psc?/V14ZaBeY40XWC8/45NBuzDIW489QBoVep5mcT0Lq.gBxwEb6.hf44ibdoA2KiCwrHNJS*10VP4yg3bnkt2TA7zugdvTHvyHaWr2uplgBObbdubFayYw6e*PbW0!/r",
    // 抽奖状态,是否转完了
    isTurnOver: true
  },

  // 点击抽奖按钮
  lottery() {
    // 如果不在抽奖状态中,则执行抽奖旋转动画
    if (this.data.isTurnOver) {
      // 把抽奖状态改为未完成
      this.setData({
        isTurnOver: false
      })
      // 这里开始假设已经调取后端接口拿到抽奖后返回的ID
      let prize_id = 3;
      // 随机奖品效果
      // const rand = (m, n) => {
      //   return Math.ceil(Math.random() * (n - m + 1) + m - 1)
      // }
      // let prize_id = rand(1, 6)
      // 调用旋转动画方法
      this.rotate(prize_id)
    } else {
      wx.showToast({
        title: '请勿重复点击',
        icon: 'none'
      })
    }
  },

  // 旋转动画方法
  rotate(prize_id) {
    // 执行完动画所需要的时间
    let _duration = 10000;
    let animationRotate = wx.createAnimation({
      duration: _duration,
      timingFunction: 'ease', //动画以低速开始,然后加快,在结束前变慢
    })
    // 解决二次点击抽奖后不再旋转的问题
    animationRotate.rotate(0).step({
      duration: 1
    })
    /*
     * 旋转的角度
     * 这转盘有6个奖项,所以一个奖项的度数为:360除以6等于60、
     * 要转完一圈 → 60 * 6
     * 为了让动画看起来舒服我设置了20圈 → 60 * 6 * 20
     * 又因为转盘是顺时针旋转的,默认指定奖品1
     * 所以需要减去 → 60 * (prize_id - 1) 方可在最后一圈转到对应的位置
     * 可以根据自己的设计稿奖品的个数进行调整
     * */
    let angle = (60 * 6 * 20) - 60 * (prize_id - 1);
    animationRotate.rotate(angle).step()
    this.setData({
      animationRotate: animationRotate.export()
    })
    // 设置倒计时,保证最后一圈执行完了,才更改状态
    setTimeout(() => {
      this.setData({
        isTurnOver: true
      })
    }, _duration)
  }

})

衔接版js: 会在原来的角度上继续旋转,逻辑就是让角度无线放大,比如第一次抽奖圈转了20圈,那么第二次点击就会转到40圈的位置,实际也是转了20圈上下,因为程序偷懒,跑过的20圈并不会再跑了。

Page({

  /**
   * 页面的初始数据
   */
  data: {
    // 转盘奖品背景
    bg: "http://r.photo.store.qq.com/psc?/V14ZaBeY40XWC8/45NBuzDIW489QBoVep5mcT0Lq.gBxwEb6.hf44ibdoBF*Yne9.pzFu6WW1t*k2C7iFgwK3jc0jMvFefvW0IfraslW4FNcfHPYyIdFLecyJ0!/r",
    // 转盘箭头抽奖按钮
    arrows: "http://r.photo.store.qq.com/psc?/V14ZaBeY40XWC8/45NBuzDIW489QBoVep5mcT0Lq.gBxwEb6.hf44ibdoA2KiCwrHNJS*10VP4yg3bnkt2TA7zugdvTHvyHaWr2uplgBObbdubFayYw6e*PbW0!/r",
    // 抽奖状态,是否转完了
    isTurnOver: true,
    // 转的总圈数,最后一圈可能不满
    num_total: 20
  },

  // 点击抽奖按钮
  lottery() {
    // 如果不在抽奖状态中,则执行抽奖旋转动画
    if (this.data.isTurnOver) {
      // 把抽奖状态改为未完成
      this.setData({
        isTurnOver: false
      })
      // 这里开始假设已经调取后端接口拿到抽奖后返回的ID
      let prize_id = 3;
      // 随机奖品效果
      // const rand = (m, n) => {
      //   return Math.ceil(Math.random() * (n - m + 1) + m - 1)
      // }
      // let prize_id = rand(1, 6)
      // 调用旋转动画方法
      this.rotate(prize_id)
    } else {
      wx.showToast({
        title: '请勿重复点击',
        icon: 'none'
      })
    }
  },

  // 旋转动画方法
  rotate(prize_id) {
    // 执行完动画所需要的时间
    let _duration = 10000;
    let animationRotate = wx.createAnimation({
      duration: _duration,
      timingFunction: 'ease', //动画以低速开始,然后加快,在结束前变慢
    })
    /*
     * 旋转的角度
     * 这转盘有6个奖项,所以一个奖项的度数为:360除以6等于60、
     * 要转完一圈 → 60 * 6
     * 为了让动画看起来舒服我设置了20圈 → 60 * 6 * 20
     * 又因为需要连贯抽取非第一次,所以二次抽奖时会在原来的圈数上自加,
     * 也就成了60 * 6 * num_total,num_total每转完一次都会叠加上自身
     * 又因为转盘是顺时针旋转的,默认指定奖品1
     * 所以需要减去 → 60 * (prize_id - 1) 方可在最后一圈转到对应的位置
     * 可以根据自己的设计稿奖品的个数进行调整
     * */
    let num_total = this.data.num_total;
    let angle = (60 * 6 * num_total) - 60 * (prize_id - 1);
    animationRotate.rotate(angle).step()
    this.setData({
      animationRotate: animationRotate.export()
    })
    // 设置倒计时,保证最后一圈执行完了,才更改状态
    setTimeout(() => {
      this.setData({
        isTurnOver: true,
        num_total: num_total + num_total
      })
    }, _duration)
  }

})

素材:


三、Canvas转盘抽奖:

效果:
在这里插入图片描述

ps:如果转盘抽奖数量不确定的需求建议直接驳回需求得了,因为我不相信有童鞋简历上敢写精通canvas吧,uTェTu,小弟用人工智能都折腾了一周做出的效果才勉强有了个雏形,如果设计稿是五花八门的,要你用canvas画出来,那不得脑门都大,非常吃力不讨好的活,狗看了都摇头。┗( 0﹏0 )┛投降

思路: 首先把转盘拆成多个扇形的组合成圆和箭头+小圆组合成的指针,然后用canvas画布去一点一点的把它们拼凑起来。之后加上动画效果就好了,刚开始本想着用wx.createAnimation直接补个动画就完事了,后来发现并不支持操作canvas,因为渲染机制不同,一个是视图层来渲染,一个渲染完全由 JavaScript 控制。所以动画效果只能通过重新绘制所产生的位移差去实现,具体看代码。

wxml:

<view class="body">
    <view class="container">
        <canvas type="2d" id="wheelCanvas" style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"></canvas>
        <button class="button" bind:tap="clickPrize" disabled="{{spinning}}">点击抽奖</button>
    </view>
</view>

wxss:

.body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: 100vw;
    min-height: 100vh;
    background-color: #f0f0f0;
}
.container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.container .button {
    margin-top: 40px;
}

js:

Page({
    data: {
        prize_arr: [], // 奖品数组
        ctx: null, // 画布对象
        canvasWidth: 0, // 画布绘图区域的宽度
        canvasHeight: 0, // 画布绘图区域的高度
        centerX: 0, // 画布x轴中心
        centerY: 0, // 画布y轴中心
        radius: 0, // 转盘圆的半径
        numSegments: 0, // 扇形的个数
        angle: 0, // 扇形的弧度
        rotationAngle: 0, // 记录当前项的弧度位置
        rotationAngleCorrect: 0, // 用于把第一个奖项调到垂直正上方
        spinning: false // 画布是否还在旋转绘图中
    },
    onLoad() {
        this.setCanvas()
        this.getCanvas()
    },
    // 设置画布的宽高
    setCanvas() {
        const that = this
        wx.getSystemInfo({
            success: function (res) {
                let canvasWidth = res.windowWidth
                let canvasHeight = res.windowWidth * 0.8
                that.setData({
                    canvasWidth: canvasWidth,
                    canvasHeight: canvasHeight
                })
            }
        })
    },
    // 获取画布信息
    getCanvas() {
        const that = this
        const query = wx.createSelectorQuery()
        query.select('#wheelCanvas')
            .fields({
                node: true,
                size: true
            })
            .exec((res) => {
                const canvas = res[0].node
                const ctx = canvas.getContext('2d')
                const dpr = wx.getSystemInfoSync().pixelRatio
                canvas.width = res[0].width * dpr
                canvas.height = res[0].height * dpr
                ctx.scale(dpr, dpr)
                that.setData({
                    ctx: ctx
                })
                that.getPrizeData()
            })
    },
    // 模拟接口获取奖品信息数组
    getPrizeData() {
        /**
         * 一般从后端获取到的奖品信息数组的顺序是从小到大的
         * 即:[1,2,3,4,5,6]
         * 画出来的效果
         * 6 → 1 → 2
         * ↑       ↓
         * 5 ← 4 ← 3
         * 但是转盘一般是顺时针旋转,所以我想要下面的效果
         * 2 → 1 → 6
         * ↑       ↓
         * 3 ← 4 ← 5
         * 刚开始我想直接逆时针方向画扇形拼成圆
         * 但是,弄好后,我发现动画也跟着逆时针方向旋转了,
         * 因为canvas的动画也是靠从新画图产生的视觉差实现的,
         * 最终不想折腾了直接调数组算了
         * 即:[1,6,5,4,3,2]
         * 
         */
        const prize_arr = [
            { id: '1', name: '奖品1', background: '#FAECC5' },
            { id: '2', name: '奖品2', background: '#FFFFFF' },
            { id: '3', name: '奖品3', background: '#FAECC5' },
            { id: '4', name: '奖品4', background: '#FFFFFF' },
            { id: '5', name: '奖品5', background: '#FAECC5' },
            { id: '6', name: '奖品6', background: '#FFFFFF' }
          ]
        if (Array.isArray(prize_arr) && prize_arr.length > 0) {
            prize_arr.reverse()
            prize_arr.unshift(prize_arr.pop())
            this.setData({
                prize_arr: prize_arr
            })
            // 设置圆心和半径
            const centerX = this.data.ctx.canvas.clientWidth / 2
            const centerY = this.data.ctx.canvas.clientHeight / 2
            const radius = centerY
            // 扇形区块的数量和角度
            const numSegments = prize_arr.length
            // 弧度值
            const angle = (2 * Math.PI) / numSegments
            // 扇形默认从x轴开始画,计算出距离y轴的弧度
            const rotationAngleCorrect = -angle * (0.75 + (this.data.prize_arr.length - 1) * 0.25);
            // 初始纠正画布的角度
            const rotationAngle = rotationAngleCorrect
            this.setData({
                centerX: centerX,
                centerY: centerY,
                radius: radius,
                numSegments: numSegments,
                angle: angle,
                rotationAngleCorrect: rotationAngleCorrect,
                rotationAngle: rotationAngle
            })
            // 初次绘制转盘
            this.drawWheel(this.data.ctx)
        }
    },
    // 绘制转盘
    drawWheel(ctx) {
        if (!ctx) return
        ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight)
        this.drawSegments(ctx)
        this.drawText(ctx)
        this.drawPointer(ctx)
    },
    // 绘制扇形
    drawSegments(ctx) {
        for (let i = 0; i < this.data.numSegments; i++) {
            ctx.beginPath()
            ctx.moveTo(this.data.centerX, this.data.centerY)
            ctx.arc(this.data.centerX, this.data.centerY, this.data.radius, i * this.data.angle + this.data.rotationAngle, (i + 1) * this.data.angle + this.data.rotationAngle)
            ctx.closePath()
            ctx.fillStyle = this.data.prize_arr[i].background
            ctx.fill()
        }
    },
    // 绘制文本
    drawText(ctx) {
        const cos90 = Math.cos(Math.PI / 2); // 90度的余弦值(用于x坐标变换)
        const sin90 = Math.sin(Math.PI / 2); // 90度的正弦值(用于y坐标变换)
        // 计算文本的位置,使其看起来像是沿着半径旋转了90度
        const textX = -this.data.radius / 2 * cos90;
        const textY = -(this.data.radius / 2 * sin90 + 10)
        for (let i = 0; i < this.data.numSegments; i++) {
            // 计算每个扇形的起始角度
            const startAngle = i * this.data.angle + this.data.rotationAngle
            // 绘制文本(先旋转,再调整位置)
            ctx.save()
            ctx.translate(this.data.centerX, this.data.centerY) // 移动到圆心
            ctx.rotate(startAngle + this.data.angle / 2 + Math.PI / 2) // 旋转到扇形中间并额外旋转90度
            ctx.textAlign = 'center'
            ctx.fillStyle = '#FF851B'
            ctx.font = '16px Arial'
            ctx.fillText(this.data.prize_arr[i].name, textX, textY)
            ctx.restore()
        }
    },
    // 绘制指针部分
    drawPointer(ctx) {
        // 绘制指针的圆部分
        ctx.beginPath()
        ctx.arc(this.data.centerX, this.data.centerY, 20, 0, 2 * Math.PI) // 假设指针的圆半径为20
        ctx.fillStyle = '#FF851B' // 指针圆的颜色
        ctx.fill()

        // 箭头高度,底部宽度
        const arrowHeight = 20
        const arrowBottomWidth = 15
        const arrowX = this.data.centerX
        const arrowY = this.data.centerY - 15 - arrowHeight
        // 绘制箭头两条边
        ctx.beginPath()
        ctx.moveTo(arrowX, arrowY) // 箭头尖端
        ctx.lineTo(arrowX - arrowBottomWidth / 2, arrowY + arrowHeight) // 左侧边
        ctx.lineTo(arrowX + arrowBottomWidth / 2, arrowY + arrowHeight) // 右侧边
        ctx.closePath()
        ctx.fillStyle = '#FF851B' // 箭头颜色
        ctx.fill()
    },
    /**
     * 绘制转盘旋转动画
     * rounds:旋转的圈数
     * prize_id:相应奖品的位置
     * */
    spinWheel(rounds = 1, prize_id) {
        if (this.data.spinning) return // 如果已经在旋转,则直接返回
        else if (!prize_id) return wx.showToast({
            title: '奖品id不能为空',
            icon: 'error'
        })
        else if (prize_id <= 0 || prize_id > this.data.prize_arr.length) return wx.showToast({
            title: '奖品id越界',
            icon: 'error'
        })
        // 设置spinning为true,防止重复触发  
        this.setData({
            spinning: true
        })

        const totalRotation = 2 * Math.PI * rounds // 根据传入的圈数计算总旋转角度  
        const duration = 500 * rounds // 根据圈数调整旋转一圈所需的时间  
        const startTime = Date.now() // 记录开始时间  

        // 使用定时器来模拟动画效果  
        const timer = setInterval(() => {
            const elapsedTime = Date.now() - startTime // 已过时间  
            const fraction = elapsedTime / duration // 已过时间的比例  
            if (fraction < 1) {
                // 如果还未完成旋转,则更新rotationAngle  
                const currentRotation = fraction * totalRotation + this.data.rotationAngleCorrect
                this.setData({
                    rotationAngle: currentRotation
                })
                this.drawWheel(this.data.ctx) // 重新绘制转盘以显示旋转效果  
            } else {
                // 如果已完成旋转,则清除定时器并停止旋转  
                clearInterval(timer)
                // 计算旋转到指定奖项的额外角度  
                const extraRotation = this.data.angle * (prize_id - 1)
                this.setData({
                    rotationAngle: (this.data.rotationAngleCorrect + totalRotation + extraRotation) % (2 * Math.PI),
                    spinning: false // 设置spinning为false,允许再次触发  
                })
                this.drawWheel(this.data.ctx) // 重新绘制转盘以显示最终状态 
            }
        }, 1000 / 60) // 每秒更新60次,以创建平滑的动画效果  
    },

    // 点击事件触发转盘旋转到指定位置
    clickPrize() {
        if (!this.data.spinning) {
            // 这里假设已经调取后端接口拿到抽奖后返回的ID  
            const prize_id = 3 // 假设中了三等奖  
            this.spinWheel(6, prize_id) // 调用spinWheel方法,传入6作为参数和prize_id作为奖项ID  
        }
    }
})

附加题:

概率: 虽然概率这块是放在后端处理,但是它的大概原理是怎么样的呢?接下来我会以我司的抽奖后台方式来和大家探讨。从下面的图中可以看到,每一项奖品都会有一个ID、名称,另外除了奖品数量外还有奖品概率,这就意味着奖品的概率并不一定是按照总数占比来的,并且可以看到红字,一般总概率是要为100(即100%,我猜这样做的目的是为了防止有小数点的情况造成比例不精准)。
在这里插入图片描述

▲抽奖方式可能会有出入的地方:
  1.我司的抽奖方式是这样的,它并不一定是只抽一下的,会按概率的级别依次抽取,每个奖品都会抽一次,比如说先以奖品一的抽奖概率去抽奖品一,如果没有抽中,那么就会去用奖品二的概率抽取奖品二,如果奖品二没有抽中继续往后,如果所有奖品抽过一次都没中,那返回未中提示;
  2.另外,比如我抽到了一等奖,但是一等奖被抽光了,那么此时它会直接向下取去拿奖品二,如果奖品二也没有,会一直往后取。大概意思是我抽中一等奖了,抽完了没有货,那么就有资格直接拿后面级别的奖品,而非重新去抽取。

下面是我通过制作一些抽奖活动后,自个理解后写的抽奖概率代码,当然并不是说后端的代码就是这样,毕竟后端不止一门语言,而且还会涉及到数据库操作,我这代码仅供大家参考参考,提升提升思维而已。(未抽中和谢谢惠顾的意思是一样的)

/*
 * 奖品数组
 * num: 该奖品剩余数量
 * probability:  该奖品抽中概率
 */
let prizeArr = [
		{id: '0',name: '一等奖',num: 1,probability: 10}, 
		{id: '1',name: '二等奖',num: 2,probability: 20}, 
		{id: '2',name: '三等奖',num: 3,probability: 30}, 
		{id: '3',name: '谢谢惠顾',num: 99999,probability: 40}
	];

// 定义数组对象排序函数
function compare(property) {
    return function(a, b) {
        let value1 = a[property];
        let value2 = b[property];
        return value1 - value2; //降序为value2 - value1
    }
}

// 根据奖品的概率重新排序,因为后续如果抽中靠前的奖品,并且该奖抽完了,会直接向下取奖品
prizeArr.sort(compare('probability'))

// 得到一个两数之间的随机数
function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
}

// 点击抽奖
function clickLottery() {
    let sum = prizeArr.reduce((prev, cur) => prev + cur.num, 0); // 剩余奖品的总数
    // console.log(sum)  
    let totalProbability = prizeArr.reduce((prev, cur) => prev + cur.probability, 0); // 全部奖品的总概率
    // console.log(totalProbability)
    if (sum <= 0) {
        console.log("奖品已经抽完了!")
        return
    }
    try {
        prizeArr.forEach((e, index) => {
            let probability = e.probability; // 当前奖品的概率
            // console.log(probability)
            let random_num = getRandomArbitrary(0, totalProbability); // 0到总概率的随机数
            // console.log(random_num)
            if (random_num <= probability) { // 如果随机数小于等于抽奖概率,说明抽中了该项奖品
                // console.log(e.num)
                if (e.num > 0) { // 如果当前奖品还有余量,返回改奖项
                    e.num -= 1; // 相应的奖品数减一
                    throw Error(e.name)
                } else { // 否则无需再抽,直接向下取奖品
                    if (index + 1 < prizeArr.length) { // 如果抽中的不是最后一个奖品,数组未越界
                        for (let i = index + 1; i <= prizeArr.length; i++) {
                            if (prizeArr[i].num > 0) { // 如果后面的奖品还有余量,返回该奖项
                                prizeArr[i].num -= 1; // 相应的奖品数减一
                                throw Error(prizeArr[i].name)
                            }
                        }
                    }
                }
            }
        })
    } catch (e) {
        console.log(e.message)
        return
    }
    console.log("很遗憾未抽中!")
}
// 触发抽奖
clickLottery();
// console.log(prizeArr)

在这里插入图片描述

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值