前言
大家好,这里是 CSS 兼 WebGL 魔法使——alphardex。
最近开始玩起了 p5.js,发现这是一个很有意思的库,用它能创作出各种新奇有趣的特效,本文我们将一起来实现下图的鼠标移动特效。
让我们开始吧!
准备工作
笔者的 p5.js 模板,点击右下角可以 fork 一份
创作开始
创建点类
在开始前,让我们创建一个最简单的点类,只有 2 个参数:x 坐标和 y 坐标
1 2 3 4 5 6 7 8
| class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }
|
由微粒组成的线
我们都知道:两点确定一条直线。
但是,如果点不止 2 个呢,能确定一条直线吗?答案当然是能,只要将这些点均匀排在一条线上,不久看起来是一条直线了吗,其实这种分割思想也是实现许多微粒特效的基本思想。
创建一个 PointLine 类,表示由微粒组成的线
- 用
dist
函数求出 2 点的距离,用ceil
取整,即是两点间的点数
- 用
map
函数将点数映射到点 AB 的 xy 坐标,就获得了两点间的点的坐标
- 用
ellipse
函数将两点间的点全部描绘处来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class PointLine { s: p5; p1: Point; p2: Point; thickness: number; dotCount: number; constructor(s: p5, p1: Point, p2: Point, thickness = 1) { this.s = s; this.p1 = p1; this.p2 = p2; this.thickness = thickness; let dotCount = s.dist(p1.x, p1.y, p2.x, p2.y); this.dotCount = s.ceil(dotCount); } draw() { for (let i = 0; i < this.dotCount; i++) { let x = this.s.map(i, 0, this.dotCount, this.p1.x, this.p2.x); let y = this.s.map(i, 0, this.dotCount, this.p1.y, this.p2.y); this.s.ellipse(x, y, this.thickness, this.thickness); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| const sketch = (s: p5) => { const draw = () => { ...
s.translate(s.width / 2, s.height / 2);
const p1 = new Point(0, 0); const p2 = new Point(50, 50); const line = new PointLine(s, p1, p2, 6); line.draw(); }; };
|
由线组成的任意多边形
创建一个 PointShape 类,表示由线组成的任意多边形
- 创建一个圆,接受 xy 坐标和半径 r
- 尝试将这个圆分割为多边形
- 圆的参数方程为
x=a+rcosθ;y=b+rsinθ;
,利用这个可算出线段所有点的坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class PointShape extends PointLine { x: number; y: number; r: number; edgeCount: number; lines: PointLine[]; constructor( s: p5, x: number, y: number, r: number, edgeCount: number, thickness = 1 ) { super(s, new Point(x, y), new Point(x, y), thickness); this.x = x; this.y = y; this.r = r; this.edgeCount = edgeCount; this.lines = []; for (let i = 0; i < this.edgeCount; i++) { const x1 = this.x + this.r * this.s.cos((this.s.TWO_PI * i) / this.edgeCount); const y1 = this.y + this.r * this.s.sin((this.s.TWO_PI * i) / this.edgeCount); const x2 = this.x + this.r * this.s.cos((this.s.TWO_PI * (i + 1)) / this.edgeCount); const y2 = this.y + this.r * this.s.sin((this.s.TWO_PI * (i + 1)) / this.edgeCount); const p1 = new Point(x1, y1); const p2 = new Point(x2, y2); const line = new PointLine(this.s, p1, p2, thickness); this.lines.push(line); } } draw() { this.lines.forEach((line) => { line.draw(); }); } }
|
1 2 3 4 5 6 7 8
| const sketch = (s: p5) => { const draw = () => { ...
const shape = new PointShape(s, 0, 0, 80, 6, 6); shape.draw(); }; };
|
高斯随机函数
由于线和图形都是由微粒组成的,我们可以尝试让微粒动起来,在 p5.js 中,常用的随机函数有高斯随机函数randomGaussian
,可以对微粒的 x,y 加上用这个函数生成的偏移量,来产生微粒随机运动的效果
- 创建一个修改 xy 坐标的函数
- 创建模糊函数
blur
,将点数和粗细加上对应的模糊值,并对 x,y 坐标加上高斯随机函数生成的偏移量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class PointLine { ... modifyFunc: Function; constructor(s: p5, p1: Point, p2: Point, thickness = 1) { ... const modifyFunc = (x: number, y: number) => [x, y]; this.modifyFunc = modifyFunc; } draw() { for (let i = 0; i < this.dotCount; i++) { let x = this.s.map(i, 0, this.dotCount, this.p1.x, this.p2.x); let y = this.s.map(i, 0, this.dotCount, this.p1.y, this.p2.y); [x, y] = this.modifyFunc(x, y); this.s.ellipse(x, y, this.thickness, this.thickness); } } blur(seed = 0, amount = 0) { const blurPower = 1 + this.s.sq(amount); this.dotCount *= blurPower; const blurPower2 = 1 - amount; this.thickness *= blurPower2; this.modifyFunc = (x: number, y: number) => { x += seed * amount * this.s.randomGaussian(0, 1); y += seed * amount * this.s.randomGaussian(0, 1); return [x, y]; }; } }
class PointShape extends PointLine { ... blur(seed = 0, amount = 0) { this.lines.forEach((line) => { line.blur(seed, amount); }); } }
|
1 2 3 4 5 6 7 8 9
| const sketch = (s: p5) => { const draw = () => { ...
const shape = new PointShape(s, 0, 0, 80, 6, 6); shape.blur(10, 0.5); shape.draw(); }; };
|
跟随鼠标移动
- 创建一个数组,用来保存鼠标的坐标,设置一个保存的上限(这里是 12 个)
- 每画一次,就添加一个当前的鼠标坐标,如果超限,则将之前添加的坐标删除
- 根据当前的鼠标坐标勾画出图形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const sketch = (s: p5) => { let mousePositions: Point[] = []; const maxPos = 12;
...
const draw = () => { ...
const mousePos = new Point(s.mouseX, s.mouseY);
mousePositions.unshift(mousePos); if (mousePositions.length > maxPos) { mousePositions.pop(); }
mousePositions.forEach((pos, i) => { const ratio = i / mousePositions.length; const shape = new PointShape(s, pos.x, pos.y, 80, 6, 6); shape.blur(10, ratio); shape.draw(); }); }; };
|
炫起来!
- 应用
HSB
色彩模式,并为每个图形填充不同的颜色
- 应用
ADD
混合模式,产生炫光的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const sketch = (s: p5) => { ...
const setup = () => { ... s.colorMode(s.HSB, 1); };
const draw = () => { ...
mousePositions.forEach((pos, i) => { s.blendMode(s.ADD); const ratio = i / mousePositions.length; s.fill(0.5 + ratio, 0.7, 0.25); const shape = new PointShape(s, pos.x, pos.y, 80, 6, 6); shape.blur(10, ratio); shape.draw(); s.blendMode(s.BLEND); }); }; };
|
项目地址
Blur Particle Trail