前置知识:
- 使用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>