2023教大家实现烟花小球

370 阅读3分钟

最近在瞎逛的时候,无意间去到某个大佬的博客,看到其点击鼠标有一个酷炫的小球散开的效果,按了F12,发现是canvas实现的,于是我决定自己动手来实现一下。

click.gif

从简到难

    1. 点击生球(在画布里鼠标点击的地方画个球)
    1. 让小球抛物线下坠
    1. 生成多个小球执行抛物线下坠

点击生球

通过点击事件获取到鼠标的坐标然后用canvas绘制小球球

// html
<canvas id="canvas" width="600" height="600"></canvas>
// js
const canvas = document.getElementById('canvas');
const ctx =canvas.getContext('2d');
canvas.addEventListener('click', function(e){
  ctx.fillStyle = '#000'
  ctx.beginPath()
  ctx.arc(e.clientX, e.clientY, 5, 0, Math.PI * 2) // 绘制圆
  ctx.fill()
})

点击画布即可生球,具体效果如下

让小球抛物线下坠

思路: 获取小球的某一时刻抛物线的坐标,动态的绘制小球;每次绘制当前的小球之前清除画布,不展示小球的轨迹

抛物线的轨迹公式如下

// angle为抛物线发射角度
// speed为发射速率
// renderCount 可以理解为渲染的某一时刻
const x = (Math.sin(angle) * speed) + x
const y = (Math.cos(angle) * speed) + y + (renderCount * 0.3)

得到公式之后,就可以遍历获取小球抛物线不同时刻的位置并绘制

const canvas = document.getElementById('canvas');
const ctx =canvas.getContext('2d');
const angle = Math.PI / 4 // 角度
const speed = 3 // 速率
let renderCount = 0
// 小球起始位置
const origin = {
  x: 0,
  y: 0
}
canvas.addEventListener('click', function(e){
  origin.x = e.clientX
  origin.y = e.clientY
  run()
})
// 绘制小球
function drawCircle (x, y) {
  ctx.fillStyle = '#000'
  ctx.beginPath()
  ctx.arc(x, y, 5, 0, Math.PI * 2)
  ctx.fill()
}
// 绘制抛物线轨迹
function run () {
  origin.x = (Math.sin(angle) * speed) + origin.x
  origin.y = (Math.cos(angle) * speed) + origin.y + (renderCount * 0.3)
  // 如果超出画布则停止绘制
  if (origin.x > 600 || origin.y> 600) {
    renderCount = 0
    return
  }
  ctx.clearRect(0, 0, 600, 600) // 清除画布
  drawCircle(origin.x, origin.y)
  requestAnimationFrame(run.bind(this))
  renderCount++
}

生成多个小球执行抛物线下坠

要让多个小球从不同方向与距离执行抛物线运动,只需要生成多个小球并以不一样的角度初速度执行运动即可

首先我们需要定义一个球类,需要有如下参数

  1. 点击的原始坐标
  2. 初速度
  3. 角度
  4. 球体颜色
  5. 绘制画布的上下文
class Ball {
  constructor({ origin, speed, angle, color, context }) {
    this.origin = origin // 起始坐标
    this.position = { ...this.origin } // 运动轨迹坐标
    this.color = color // 填充颜色
    this.speed = speed // 速率
    this.angle = angle // 角度
    this.context = context
    this.renderCount = 0
  }
  
  draw() {
    // 抛物线运动轨迹坐标计算
    this.position.x = (Math.sin(this.angle) * this.speed) + this.position.x
    this.position.y = (Math.cos(this.angle) * this.speed) + this.position.y + (this.renderCount * 0.3)
    // 绘制小球
    this.context.fillStyle = this.color
    this.context.beginPath()
    this.context.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2)
    this.context.fill()
    this.renderCount++
  }
}

然后我们还需要定义一个类,作为每次点击生成一次动画的构造对象,可以配置每次点击生成多少个小球执行运动

class Boom {
  constructor({ origin, count = 10, context, area }) {
    this.origin = origin // 起始坐标
    this.count = count // 小球数量
    this.context = context
    this.area = area // 画布的大小
    this.stop = false // 运动状态
    this.balls = []
  }
  // 初始化创建指定数量随机小球
  init () {
    for(let i = 0; i < this.count; i++) {
      const ball = new Ball({
        origin: { x: this.origin.x, y: this.origin.y },
        context: this.context,
        color: randomColor(),
        angle: randomRange(Math.PI - 1, Math.PI + 1),
        speed: randomRange(1, 6)
      })
      this.balls.push(ball)
    }
  }
  draw () {
    this.balls.forEach((ball, index) => {
      // 如果超出画布
      if (ball.position.x > this.area.width || ball.position.y > this.area.height) {
        return this.balls.splice(index, 1)
      }
      ball.draw()
    })
    if (this.balls.length == 0) {
    this.stop = true
    }
  }
}

最后一步,创建一个跟屏幕大小一样的画布,监听mousedown事件,每次点击就生成一个Boom的实例并执行动画即可

const canvas = document.createElement('canvas');
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const ctx =canvas.getContext('2d');
document.body.append(canvas)

const booms = []
let running = false

function handleMouseDown(e) {
  const boom = new Boom({
      origin: {x: e.clientX, y: e.clientY}, 
      count: 20, 
      context: ctx, 
      area: {width: window.innerWidth, height: window.innerHeight}
  })
  boom.init()
  booms.push(boom)
  // 执行运动
  const run = () => {
    running = true
    if (booms.length == 0) {
    return running = false
    }
    requestAnimationFrame(run.bind(this))
    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
    booms.forEach((item,index) => {
      if (item.stop) {
        return booms.splice(index, 1)
      }
      item.draw()
    })
  }
  running || run()
}
window.addEventListener('mousedown', handleMouseDown.bind(this))

最后看一下效果