P5js实现烟花动画

1,903 阅读1分钟

前置知识:

  • 使用p5js库p5js.org/zh-Hans/
    • map,可以计算比例
    • lerpColor,计算中间颜色
  • 使用dat.GUI进行参数调节
  • 项目使用vite+ts+vue3

动画效果:

  • 具体效果参看:web.coinpx.io/#/p5/firewo…
  • 烟花先上升,后绽放,绽放有重力下降效果
  • 烟花上升时逐渐变淡
  • 绽放时会有颜色变化
  • 绽放后,逐渐淡出

实现代码:

项目采用vue3

<template>
  <div id="canvas"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from "vue"
import P5 from "p5js/p5.js/p5.min.js"
import dat from "dat.gui"
let p5: any = null
// GUI用于调整参数,使用时删除相关功能即可
const GUI = new dat.GUI()
onMounted(() => {
  p5 = new P5(sketch, "canvas")
})

onUnmounted(() => {
  p5.clear()
  p5.noLoop()
  p5.noCanvas()
  GUI.destroy()
})

const options = {
  h: 200,
  r: 10,
  thread: 7,
  ballCount: 10,
  speed: 5,
  maxDist: 80,
  shadow: 225,
  num: 20,
  background: "#000000",
  colors: ["#FCEC83", "#BEE6DE", "#FC5D5D", "#F97B4F", "#FFF200"],
}

const sketch = (animate: any) => {
  // 参数调整
  GUI.addColor(options, "background")
  const folder = GUI.addFolder("颜色调整") // 接收目录名
  options.colors.forEach((item, i) => {
    folder.addColor(options.colors, String(i))
  })
  GUI.add(options, "shadow").min(0).max(255).step(1).name("阴影")
  GUI.add(options, "h")
    .name("基础高度")
    .min(20)
    .max(500)
    .step(1)
    .onChange(() => {
      init()
    })
  GUI.add(options, "thread")
    .name("绽放时的层数")
    .min(5)
    .max(20)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  GUI.add(options, "ballCount")
    .name("每层的粒子数")
    .min(5)
    .max(20)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  GUI.add(options, "r")
    .name("半径")
    .min(5)
    .max(20)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  GUI.add(options, "speed")
    .min(1)
    .max(10)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  GUI.add(options, "maxDist")
    .name("最大绽放大小")
    .min(50)
    .max(200)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  GUI.add(options, "num")
    .min(1)
    .max(100)
    .step(1)
    .onFinishChange(() => {
      init()
    })
  let w = window.innerWidth,
    h = window.innerHeight

  // 烟花类
  class Fireworks {
    x: number // 坐标
    y: number
    r: number // 半径
    h: number
    thread: number
    ballCount: number
    speed: number
    maxDist: number
    c1: string   //颜色
    c2: string
    upDuring: boolean   // 是否上升期间
    constructor(c1: string, c2: string) {
      this.x = animate.random(20, w - 20)
      this.y = h
      this.h = h - options.h - animate.random() * (h - options.h)
      this.r = options.r
      this.thread = options.thread - 1
      this.ballCount = options.ballCount
      this.speed = options.speed
      this.maxDist = options.maxDist
      this.upDuring = true
      this.c1 = c1
      this.c2 = c2
    }
    // 绽放后,重置数据,再次燃放
    reset() {
      this.x = animate.random(10, w - 10)
      this.y = h
      this.h = h - options.h - animate.random() * (h - options.h)
      this.r = options.r
      this.thread = options.thread - 1
      this.ballCount = options.ballCount
      this.speed = options.speed
      this.maxDist = 60
      this.upDuring = true
    }
    
    run() {
      if (this.upDuring) {
        // 上升
        this.up()
        this.upDuring = this.y > this.h
      } else {
        // 绽放
        this.boom()
      }
    }

    up() {
      this.y -= this.speed
      // 计算上升期间的透明度
      const al = animate.map(this.y, this.h, 400, 0, 255)
      animate.fill(animate.red(this.c1), animate.green(this.c1), animate.blue(this.c1), al)
      animate.ellipse(this.x, this.y, this.r)
    }

    boom() {
      if (this.speed > this.maxDist) {
        // 绽放完毕,重置数据
        this.reset()
      } else {
        // 绽放半径不断增加
        this.speed += 0.5
        // 重力下降效果
        this.y += 1.5 
        // 计算绽放时的透明度
        const al = animate.map(this.speed, 0, this.maxDist, 255, 0)
        // 计算绽放时颜色变化比例
        const ra = animate.map(this.speed, 0, this.maxDist / 2, 0, 1)
        const between = animate.lerpColor(animate.color(this.c1), animate.color(this.c2), ra)
        animate.fill(animate.red(between), animate.green(between), animate.blue(between), al)
        // 中心点
        animate.ellipse(this.x, this.y, this.r / 2)
        // 相邻两个点的弧度,用于计算每个点的具体位置
        const radian = animate.TWO_PI / this.ballCount / 2
        for (let j = 1; j <= this.thread; j++) {
          // 每一圈离中心点的距离
          const r = (this.speed * j) / 3
          for (let i = 0; i < this.ballCount * 2; i++) {
            // 这里让每个一层的点错开一点,不然太规整了
            if ((i + j) % 2) {
              // 利用cos与sin计算每个点的坐标
              const x = animate.sin(radian * i) * r + this.x
              const y = this.y - animate.cos(radian * i) * r
              animate.ellipse(x, y, this.r / 2)
            }
          }
        }
      }
    }
  }

  let f: Fireworks[] = []

  // 创建烟花
  function init() {
    f = []
    let len = options.colors.length - 1
    for (let i = 0; i < options.num; i++) {
      let index1 = Math.round(animate.random(0, len))
      let index2 = Math.round(animate.random(0, len))
      f.push(new Fireworks(options.colors[index1], options.colors[index2]))
    }
  }

  animate.setup = function () {
    animate.createCanvas(w, h)
    animate.background(0)
    init()
  }

  animate.draw = function () {
    animate.background(
      animate.red(options.background),
      animate.green(options.background),
      animate.blue(options.background),
      255 - options.shadow
    )
    animate.noStroke() // 无边框
    f.forEach(item => {
      item.run()
    })
  }
}
</script>