canvas炸烟花思路总结

canvas炸烟花思路总结

效果

源码

html

点击查看代码
复制<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>烟花效果</title>
    <style>
      canvas {
        margin: 80px auto;
        display: block;
        box-shadow: 0 0 10px rgb(0 0 0 / 50%);
      }
    </style>
  </head>
  <body onload="draw()">
    <canvas width="800" height="500"></canvas>

    <script src="./js/烟花.js"></script>
    <script src="./js/烟花粒子.js"></script>
    <script>
      let timeTicker = 0
      // 动画执行80次,发射8个烟花
      let timeTotal = 80
      let fireworks = []
	  // 一个烟花有多少个粒子
      const particlesCount = 50
      let particles = []
      function draw() {
        const canvas = document.querySelector('canvas')
        const ctx = canvas.getContext('2d')
        // 透明度控制拖尾效果
        ctx.fillStyle = 'rgba(0,0,0,0.03)'
        ctx.fillRect(0, 0, canvas.width, canvas.height)

        // 画烟花
        for (let index = 0; index < fireworks.length; index++) {
          const firework = fireworks[index]
          firework.draw(ctx)
          if (firework.isArrived()) {
            fireworks.splice(index, 1)
            index--
            for (let j = 0; j < particlesCount; j++) {
              particles.push(
                new Particle(
                  ...firework.getTargetCoordinate(),
                  firework.getHue()
                )
              )
            }
          }
        }

        // 画粒子
        for (let index = 0; index < particles.length; index++) {
          const particle = particles[index]
          particle.draw(ctx)
          if (particle.isVanished()) {
            particles.splice(index, 1)
            index--
          }
        }

        // 函数循环80次自动发射8支烟花
        if (timeTicker >= timeTotal) {
          for (let index = 0; index < timeTotal / 10; index++) {
            fireworks.push(
              new FireWork(
                canvas.width / 2,
                canvas.height,
                getRandomRange(0, canvas.width),
                getRandomRange(100, canvas.height / 2)
              )
            )
          }
          timeTicker = 0
        } else {
          timeTicker++
        }

        requestAnimationFrame(draw)
      }

      function getRandomRange(min, max) {
        return Math.random() * (max - min) + min
      }
    </script>
  </body>
</html>

烟花.js

点击查看代码
class FireWork {
  constructor(startX, startY, endX, endY) {
    // 烟花起始点(从哪个地方开始发射)
    this.startX = startX
    this.startY = startY

    // 当前坐标(烟花移动过程中使用)
    this.x = this.startX
    this.y = this.startY

    // 烟花结束点(发射到哪个地方)
    this.endX = endX
    this.endY = endY

    // 目标距离
    this.targetDistance = this._getPointsDistance(
      this.startX,
      this.startY,
      this.endX,
      this.endY
    )

    // 当前烟花移动的距离
    this.currentDistance = 0

    // hsla 色调
    this.hue = this._getRandomRange(0, 360)
    // hsla 亮度
    this.lightness = this._getRandomRange(50, 70)

    // 被发射的角度(根据给定的开始、结束位置计算角度)
    this.angle = Math.atan2(this.endY - this.startY, this.endX - this.startX)

    // 移动速度
    this.speed = 2
    this.acceleration = 1.05 // 加速度系数
  }

  draw(ctx) {
    ctx.save()
    this.speed *= this.acceleration
    const moveX = this.x + Math.cos(this.angle) * this.speed
    const moveY = this.y + Math.sin(this.angle) * this.speed
    ctx.strokeStyle = `hsla(${this.hue}, 100%, ${this.lightness}%, 60%)` // 仅亮度会变化
    ctx.lineWidth = 1
    ctx.beginPath()
    ctx.moveTo(this.x, this.y)
    ctx.lineTo(moveX, moveY)
    ctx.stroke()
    ctx.restore()

    this.currentDistance = this._getPointsDistance(
      this.startX,
      this.startY,
      moveX,
      moveY
    )

    this.x = moveX
    this.y = moveY
  }

  // 是否到达目标地点
  isArrived() {
    return this.currentDistance >= this.targetDistance
  }

  // 获取烟花色调
  getHue() {
    return this.hue
  }

  // 获取目标位置的坐标
  getTargetCoordinate() {
    return [this.endX, this.endY]
  }

  _getRandomRange(min, max) {
    return Math.random() * (max - min) + min
  }

  // 计算直角(笛卡尔)坐标系中两点间距离
  _getPointsDistance(x1, y1, x2, y2) {
    return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
  }
}

烟花粒子.js

点击查看代码
class Particle {
  constructor(x, y, hue) {
    // 初始位置
    this.x = x
    this.y = y

    // 从烟花那得来的hsla色调
    this.hue = this._getRandomRange(hue - 20, hue + 20)
    //this.hue = hue
    // 随机hsla亮度
    this.lightness = this._getRandomRange(50, 80)

    // 初始透明度
    this.alpha = 1
    // 随机的透明度衰变系数(透明度减淡)
    this.alphaDecay = this._getRandomRange(0.015, 0.03)

    // 模拟下坠摩擦力(让粒子减速)
    this.friction = 0.95
    // 模拟重力加速度
    this.gravity = 1

    // 随机角度
    this.angle = this._getRandomRange(0, 2 * Math.PI)
    // 随机初始速度
    this.speed = this._getRandomRange(1, 10)
  }

  draw(ctx) {
    ctx.save()
    // 减速
    this.speed *= this.friction
    // 计算新位置
    const moveX = this.x + Math.cos(this.angle) * this.speed
    const moveY = this.y + Math.sin(this.angle) * this.speed + this.gravity

    ctx.beginPath()
    ctx.moveTo(this.x, this.y)
    ctx.lineTo(moveX, moveY)

    ctx.strokeStyle = `hsla(${this.hue}, 100%, ${this.lightness}%, ${this.alpha})`
    ctx.lineWidth = 3
    ctx.lineCap = 'round'
    ctx.stroke()
    ctx.restore()
    this.x = moveX
    this.y = moveY
    // 透明度衰减
    this.alpha -= this.alphaDecay
  }

  _getRandomRange(min, max) {
    return Math.random() * (max - min) + min
  }

  // 粒子是否已经看不见
  isVanished() {
    return this.alpha < this.alphaDecay
  }
}

主要流程

烟花被发射到指定位置后消失,粒子绽放,实现烟花绽放的效果

烟花发射

  • 烟花射速控制,动画执行80次后,发射8个烟花(控制发射频率)
  • 烟花发射前,其角度、颜色、目标位置均已确定
    • 角度、颜色、目标位置都是随机的
  • 烟花发射时有初始速度和加速度(表现在烟花消失时刻的那个冲劲儿)
  • 烟花到达目标位置后消失,绽放粒子

粒子绽放

  • 每个粒子绽放时,初始位置、速度、角度、颜色均已确定
    • 初始位置:烟花的结束位置
    • 速度:初始是随机的。移动过程中模拟摩擦力/阻力,速度逐渐减小
    • 角度:随机。生成后不再发生变化
    • 颜色:烟花的颜色。可以围绕烟花的颜色有些许变化,呈现五颜六色的效果
  • 为模拟物体下坠,粒子移动过程中需要有模拟的摩擦力(粒子移速减小)、重力加速度(模拟粒子有重力)
  • 粒子移动过程中,颜色逐渐透明(表示粒子/烟花消失)。低于一定的透明度时,粒子消失

路线形成

烟花和粒子的移动路线计算都符合下面的流程

  • 初始位置随机
  • 移动过程中,随着速度发生变化,根据角度计算移动的坐标
  • 用上一次的结束坐标作为这一次的开始坐标,并重新根据速度、角度(因为要沿着同一个方向走)计算要移动的坐标距离(坐标变化量)
    • 新坐标=旧坐标+坐标变化量

拖尾效果

fillStyle的透明度控制拖尾效果,越透明,拖尾越明显

image

参考

Canvas放烟花

posted @   酉云良  阅读(273)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示