前置知识:
-
使用p5js库p5js.org/zh-Hans/
- p5js中有mouseX与mouseY记录了鼠标的位置
- randomGaussian方法,可以一个随机生成正态分布的值,参数1是均值,参数2是标准差
-
使用dat.GUI进行参数调节
-
项目使用vite+ts+vue3
动画效果:
- 跟随鼠标可以拖出类似流星效果
- 根据参数可以调节动画类型,如方形,圆型,文字,或不显示
- 根据参数可以调整颜色背景等
- 根据参数控制尾巴缩小的比例等
- 具体效果:web.coinpx.io/#/p5/mousem…
思路
-
如何控制尾巴的长度?
- 使用固定长度数组,将每次移动时的鼠标坐标存起来,每次渲染入栈,超长时切断,长度用参数控制
- 数组中的每一个坐标,画一个图案,连在一起就形成尾巴的效果了
-
鼠标移动过快,导致连续的两个点断开
- 每次入栈,与后一个坐标对比,如果距离比图案大小长,则需要进行补帧
- 补帧就是在坐标数组中,再添加一些坐标,缩短每个坐标之间的距离
-
如何控制颜色?
- 每个坐标根据它所在数组索引的百分比,计算它颜色,主要使用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>