用p5js库实现动效

726 阅读3分钟

前置知识

p5js中有两个函数:

  1. setup:初始化时执行一次,通常用于创建画布,或其他初始化操作
  2. 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>