p5js实现与鼠标的交互效果

552 阅读3分钟

前置知识:

  • 使用p5js库p5js.org/zh-Hans/

    • p5js中有mouseX与mouseY记录了鼠标的位置
    • randomGaussian方法,可以一个随机生成正态分布的值,参数1是均值,参数2是标准差
  • 使用dat.GUI进行参数调节

  • 项目使用vite+ts+vue3

动画效果:

  • 跟随鼠标可以拖出类似流星效果
  • 根据参数可以调节动画类型,如方形,圆型,文字,或不显示
  • 根据参数可以调整颜色背景等
  • 根据参数控制尾巴缩小的比例等
  • 具体效果:web.coinpx.io/#/p5/mousem…

思路

  1. 如何控制尾巴的长度?

    • 使用固定长度数组,将每次移动时的鼠标坐标存起来,每次渲染入栈,超长时切断,长度用参数控制
    • 数组中的每一个坐标,画一个图案,连在一起就形成尾巴的效果了
  2. 鼠标移动过快,导致连续的两个点断开

    • 每次入栈,与后一个坐标对比,如果距离比图案大小长,则需要进行补帧
    • 补帧就是在坐标数组中,再添加一些坐标,缩短每个坐标之间的距离
  3. 如何控制颜色?

    • 每个坐标根据它所在数组索引的百分比,计算它颜色,主要使用p5js中的lerpColor计算中间色

实现代码

mousemove.ts

import dat from 'dat.gui'
export const GUI = new dat.GUI()
// 参数
const options = {
  size: 30, // 大小
  radius: 2, // 粒子大小
  nums: 100, // 粒子数量
  tail: 20,  // 拖动长度
  shadow: 0, // 残影 
  color1: '#ffff00',
  color2: '#ff00ff',
  background: '#000',
  reductionRatio: 1, // 缩小比率
  particle: true,  // 是否显示粒子
  type: 'text',
  text: '嘿,朋友'
}
export default function (animate: any) {
  let stack: any // 存放坐标的实例
  let from: any, to: any // 颜色
  let textlist: string[] // 文本数组
  let textLen: number  
	// 增加需要控制的参数
  GUI.addColor(options, 'background')
  GUI.addColor(options, 'color1').onChange(()=>{
    init(animate)
  })
  GUI.addColor(options, 'color2').onChange(()=>{
    init(animate)
  })
  GUI.add(options, 'size').min(0).max(50).step(1)
  GUI.add(options, 'radius').min(0).max(20).step(1).name('粒子大小')
  GUI.add(options, 'nums').min(0).max(400).step(1).name('粒子数量')
  GUI.add(options, 'tail').min(0).max(50).step(1).name('尾巴长度')
  GUI.add(options, 'shadow').min(0).max(255).step(1).name('残影')
  GUI.add(options, 'reductionRatio').min(0).max(1).step(0.01).name('尾巴缩小比例')
  GUI.add(options, 'type',['circle','text','rectangle',null]).onChange(()=>{
    init(animate)
  })
  GUI.add(options, 'text').onChange(()=>{
    init(animate)
  })
  GUI.add(options, 'particle').name('是否显示粒子')
  // p5js的setup函数,初始化执行一次
  animate.setup = function () {
    animate.noStroke()
    animate.createCanvas(innerWidth, innerHeight);
    init(animate)
  }
  //p5js的draw方法,每次渲染都会执行
  animate.draw = function () {
    animate.background(animate.red(options.background), animate.green(options.background), animate.blue(options.background), (255 - options.shadow));
    stack.add(animate.mouseX, animate.mouseY)
  }
  // 定义初始化数据
  function init(_p5: any) {
    // 初始化颜色
    from = _p5.color(options.color1)
    to = _p5.color(options.color2)
    if (options.type === 'text') {
      textlist = options.text.split('')
      textLen = textlist.length
    }
    // 生成存放坐标的实例
    stack = new Stack(_p5)
  }
  // 定义画图函数 类型包括:['Circle', 'text', 'Rectangle']
  function show(_p5: any, x: number, y: number, percent: number = 1, text: string = '') {
    let len = options.nums * percent // percent表示当前坐标在数组中的百分比
    let delta = options.size * percent   // 粒子的数量,图案大小会根据百分比不同而变化
    // 如果需要粒子,画出粒子
    if (options.particle) {
      for (let i = 0; i < len; i++) {
        let calcX = _p5.randomGaussian(x, delta)
        let calcY = _p5.randomGaussian(y, delta)
        _p5.ellipse(calcX, calcY, options.radius, options.radius)
      }
    }
    // 根据参数画不同的图案
    if (options.type === 'circle') {
      _p5.ellipse(x, y, delta, delta)
    } else if (options.type === 'rectangle') {
      _p5.rect(x - delta / 2, y - delta / 2, delta, delta)
    } else if(options.type === 'text'){
      _p5.textSize(delta * 2);
      _p5.textAlign(_p5.CENTER,_p5.CENTER) 
      _p5.text(text, x, y);
    }
  }
    // 定义存放坐标的类
  class Stack {
    mouseList: any
    _p5: any
    constructor(_p5: any) {
      this.mouseList = []
      this._p5 = _p5
    }
    // 每次渲染都会添加一个坐标
    add(mouseX: number, mouseY: number) {
      this.mouseList.unshift([mouseX, mouseY])
      if (this.mouseList.length > options.tail) {
        this.mouseList = this.mouseList.slice(0,options.tail)
      }
        // 补帧,type是文字时,不进行补帧
      if (options.type !== 'text' && this.mouseList[1]) {
          let [prex, prey] = this.mouseList[1]
          let de = this._p5.dist(mouseX, mouseY, prex, prey) // 计算距离
          while (de > options.size) { 
            let rate = options.size / de
            prex = prex - rate * (prex - mouseX)
            prey = prey - rate * (prey - mouseY)
            this.mouseList.splice(1, 0, [prex, prey])
            de -= options.size
          }
      }
      if (options.shadow === 255) {
        // 如果阴影透明,不要尾巴,实现手写效果
        show(this._p5, mouseX, mouseY)
      } else {
        const len = this.mouseList.length
        let preN = -1
        // 倒叙遍历,先画尾巴再画头,避免尾巴把头给覆盖了
        for (let i = len - 1; i >= 0; i--) {
          let [x, y] = this.mouseList[i]
          // 计算颜色的比例
          let per = this._p5.norm(i, 0, len) 
          // 计算当前图标颜色
          let between = this._p5.lerpColor(from, to, per)
          this._p5.fill(between)
          // 计算比例
          let percent = this._p5.map(i, options.tail, 0, options.reductionRatio, 1)
          if (options.type === 'text') {
          	let n = this._p5.floor(this._p5.map(i, 0, len, 0, textLen))
            if (n !== preN) {
              // 画出当前坐标的文字
              show(this._p5, x, y, percent, textlist[n])
              preN = n
            }
          } else {
            // 画出图案
            show(this._p5, x, y, percent)
          }
        }
      }
    }
  }
}

组件中使用

<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/mousemove'
  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>