前置知识
p5js中有两个函数:
- setup:初始化时执行一次,通常用于创建画布,或其他初始化操作
- draw:每次重绘都会执行,也就是window.requestAnimationFrame(draw)。这样便产生了动画效果
本次动画的效果:
- 随机大小不一的线条,文字或粒子(可通过参数配置),这些元素形成一个小圆环
- 圆环转动至一定的圈数后膨胀。其中速度,圈数,膨胀大小,均可通过参数控制
- 通过dat.GUI控制参数,控制参数变化
- 演示效果链接:web.coinpx.io/#/p5/boom
实现代码
本次项目使用:vite+ts+vue3
boom.ts
import dat from 'dat.gui'
// 配置参数
const options = {
lowerRadius: 120, // 小圆环的半径
thickness: 100, // 小圆环的粗细
radius: 13, // 粒子大小的最大值
nums: 1600, // 粒子数量
speedFirst: 15, // 小圆环转动时的速度
speedLast: 8, // 膨胀后转动的速度
count: 3, // 转动多少圈开始膨胀
maxRadius: 200, // 膨胀的后的大小
shadow: 30, // 转动时残影的透明度
background: '#232336', // 背景颜色
color1: '#cbceff', // 粒子渐变颜色1
color2: '#619ce3', // 粒子渐变颜色2
type: 'line', // 粒子是线条
text: 'text' // 如果粒子是文字,文字内容
}
// 调节参数
export const GUI = new dat.GUI()
export default function (animate: any) {
// 增加需要调节的参数
GUI.addColor(options, 'background')
GUI.addColor(options, 'color1').onChange(()=>{
animate.reDraw()
})
GUI.addColor(options, 'color2').onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'lowerRadius').min(20).max(120).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'thickness').min(10).max(100).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'shadow').min(0).max(255).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'radius').min(1).max(40).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'nums').min(10).max(2000).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'speedFirst').min(1).max(50).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'speedLast').min(1).max(20).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'count').min(0.1).max(10).step(0.1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'maxRadius').min(200).max(800).step(1).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'type', ['circle', 'text', 'rectangle', 'line']).onChange(()=>{
animate.reDraw()
})
GUI.add(options, 'text').onChange(()=>{
animate.reDraw()
})
// 粒子构造器,一个粒子就是一个对象
class Dot {
x: number // x轴坐标
y: number // y轴坐标
r: number // 粒子大小
dist: number // 粒子离原心的距离
sizeRate: number // 粒子大小在最大和最小之间的比率,用于计算速度:越小的转动越慢
firstRadian: number // 初始化后的弧度大小
count: number // 转动的弧度
between: any // 当前粒子的颜色,根据粒子大小不同,颜色不同
constructor(lowerRadius: number, thickness: number) {
// 构造函数两个参数:小圆环的半径,小圆环的粗细
const innerRadius = lowerRadius - thickness // 小圆环,内圆的半径
// 随机生成x坐标,值是在圆环半径内
this.x = animate.round(animate.random(-lowerRadius, lowerRadius))
// 计算Y的坐标范围,oprand随机正或负
const oprand = animate.random() < 0.5 ? -1 : 1
// 利用勾股定理,计算y坐标的最大值,不理解的可以画图看看
const maxy = animate.pow(animate.sq(lowerRadius) - animate.sq(this.x), 0.5)
let miny
if (animate.abs(this.x) >= innerRadius) {
miny = 0 // x坐标大于内圆环的半径,则y最小值为0
} else {
// 利用勾股定理计算y轴最小值
miny = animate.pow(animate.sq(innerRadius) - animate.sq(this.x), 0.5)
}
// y轴随机范围
this.y = animate.random(miny, maxy) * oprand
// 随机粒子大小
this.r = animate.random(0, options.radius)
// 粒子离原点的距离
this.dist = animate.dist(0, 0, this.x, this.y)
// 粒子大小的比例
this.sizeRate = animate.norm(this.r, 0, options.radius)
// atan2计算弧度
this.firstRadian = animate.atan2(this.y, this.x)
this.count = 0
// 计算颜色
const from = animate.color(options.color1);
const to = animate.color(options.color2);
this.between = animate.lerpColor(from, to, this.sizeRate);
}
// 根据参数画出粒子
show() {
animate.fill(this.between)
if (options.type === 'circle') { // 圆
animate.ellipse(this.x, this.y, this.r, this.r)
} else if (options.type === 'rectangle') { // 矩形
animate.rect(this.x, this.y, this.r, this.r);
} else if (options.type === 'text') { // 文字
animate.textSize(this.r)
animate.text(options.text, this.x, this.y)
} else if (options.type === 'line') { // 线条
animate.stroke(this.between)
animate.strokeWeight(this.r / 10);
animate.noFill();
animate.line(this.x, this.y, this.x + 2 * this.r * animate.sin(this.firstRadian), this.y + 2 * this.r * animate.sin(this.firstRadian));
}
// 画完后移动粒子,下次draw时,就在新的位置,产生动画效果
this.move()
}
move() {
if (enlarge) {// 是否膨胀
if (this.dist < options.maxRadius) { // 是否膨胀到头
// 膨胀增加粒子离原点的距离
this.dist += options.thickness
this.count += options.speedFirst * this.sizeRate / 200
} else {
// 膨胀到头,保持速度2继续转动
this.count += options.speedLast * this.sizeRate / 500
}
} else {
// 没有膨胀,保持第一速度继续旋转
this.count += options.speedFirst * this.sizeRate / 100
}
// 转动弧度大小
const radian = this.count + this.firstRadian
if (this.count > animate.TWO_PI * options.count) {
enlarge = true // 装满后,开始膨胀
}
// 根据转动弧度与距离原点距离,计算下一次的x和y坐标
this.x = animate.cos(radian) * this.dist
this.y = animate.sin(radian) * this.dist
}
}
let dots: any = [] // 存放所以粒子
let enlarge = false // 是否膨胀
animate.setup = function () {
// 创建画布
animate.createCanvas(innerWidth, innerHeight);
animate.noStroke() // 不要边框
// 初始化粒子
for (let i = 0; i < options.nums; i++) {
dots[i] = new Dot(options.lowerRadius, options.thickness)
}
}
animate.draw = function () {
// 背景
animate.background(animate.red(options.background), animate.green(options.background), animate.blue(options.background), (255 - options.shadow));
// 移动原点到中心位置
animate.translate(animate.width / 2, animate.height / 2)
const len = dots.length
for (let i = 0; i < len; i++) {
dots[i].show()
}
}
// 重新开始
animate.reDraw = function(){
enlarge = false
dots = []
for (let i = 0; i < options.nums; i++) {
dots[i] = new Dot(options.lowerRadius, options.thickness)
}
}
}
Boom.vue组件
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue"
import P5 from 'p5js/p5.js/p5.min.js' // npm i -S p5js
import sketch,{GUI} from '../../hooks/boom'
onMounted(()=>{
GUI.domElement.style.display = 'block'
new P5(sketch,'canvas')
})
onUnmounted(()=>{
// 隐藏GUI
GUI.domElement.getElementsByTagName('ul')[0].innerHTML=''
GUI.domElement.style.display = 'none'
})
</script>
<template>
<div id="canvas"></div>
</template>