之前使用jQuery做了一个红包雨的效果,感觉不是很理想,创建过多DOM元素造成性能上的浪费,整个页面也不够丝滑流畅,这次该用Canvas做一个红包雨的效果

1.核心原理

1.生成红包,设置好默认的背景图像,传入页面上所需的红包数量

2.保存状态,利用Canvas的原生API实现页面的渲染,每次移动各个红包到不同位置,利用window.requestAnimationFrame实现每一帧的动画绘制,使整个页面不会卡顿

3.点击红包,计算是否点中红包,如果点中,则进行相应的事件处理

2.具体实现

2.1定义一些基本变量

redEnvelopeArr: [], // 当前整个红包对象数组
itemSpeed: [], // 每个红包对象对应的一些属性,如下落速度,页面上所在的位置
count: 0, // 加载好的红包图片的数量
ctx: null, // Canvas context对象

2.2加载红包图片

loadImgs(totalCount) {
  const self = this
  const canvas = document.getElementById('canvas')
  if (canvas.width < window.innerWidth) {
    canvas.width = window.innerWidth
  }
  if (canvas.height < window.innerHeight) {
    canvas.height = window.innerHeight
  }
  return new Promise(resolve => {
    for (let index = 0; index < totalCount; index++) {
      const image = new Image()
      image.src = '' // 图片地址
      image.onload = () => {
        self.count++
        self.itemSpeed.push({
          speed: 0, // 开始下落的位置
          step: self.randomFloat(2, 6) // 每个红包下落的速度
        })
        self.redEnvelopeArr.push({
          x: self.randomFloat(0, window.innerWidth - 100),
          y: 0,
          img: image
        })
        if (self.count === totalCount) resolve() // 所有图片加载完成跳出
      }
    }
  })
}

2.2生成随机数的一个方法

randomFloat(low, high) {
  return low + Math.random() * (high - low)
}

2.3绘制红包图片下落,这里做了一个旋转下落的效果,主要原理是利用translate移动context位置,旋转一定角度后,将原点移动到之前位置,做一个旋转的效果,每次操作的时候记得savarestore。当然如果不想做旋转效果,那直接用drawImage每次画不同位置的红包就行了

drawRedEnvelope() {
  const self = this
  self.ctx.width = 100 // 红包的宽度
  self.ctx.height = 120 // 红包的高度
  self.redEnvelopeArr.forEach((item, index) => {
    const newRedEnvelope = {
      x: item.x, // 红包所在的x轴位置
      y: item.y + self.itemSpeed[index].step, // 红包所在的y轴位置,主要由下落速度决定
      img: item.img,
      speed: item.speed
    }
    self.ctx.save()
    self.redEnvelopeArr.splice(index, 1, newRedEnvelope)
    self.ctx.translate(item.x + 50, 60 + self.itemSpeed[index].speed)
    self.ctx.rotate(self.itemSpeed[index].speed * Math.PI / 180)
    self.ctx.translate(-item.x - 50, -60 - self.itemSpeed[index].speed)
    self.ctx.drawImage(item.img, item.x, self.itemSpeed[index].speed)
    self.ctx.restore()
    self.itemSpeed[index].speed += self.itemSpeed[index].step
    // self.ctx.drawImage(item.img, item.x, item.y, self.ctx.width, self.ctx.height)
  })
},

2.4使用clearRect清除上一帧的效果,在调用drawRedEnvelope绘制新一帧的效果,使用requestAnimationFrame重复执行

moveRedEnvelope() {
  const self = this
  self.ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
  self.drawRedEnvelope()
  window.requestAnimationFrame(self.moveRedEnvelope.bind(this))
}

2.5点击红包时,循环遍历判断当前点击位置是否在某个红包内,如果遇到多个红包重叠的情况,只需取最上面一个就行了

redEnvelopeClick() {
  const self = this
  const canvas = document.getElementById('canvas')
  canvas.addEventListener('click', e => {
    let result = {}
    this.redEnvelopeArr.forEach((item, index) => {
      const distanceX = e.clientX - item.x
      const distanceY = e.clientY - item.y
      const withinX = distanceX > 0
      const withinY = distanceY > 0
      if (withinX && withinY) {
        result = item // 最后的result返回,然后做一些处理
      }
    })
  })
}

后言

以上就是主要内容,需要特别注意的是,点击完了某个红包需要找个时间点清空整个页面上的动画,还有就是具体的点击实现的业务逻辑需自己去处理