文章

用 20,000 个点画一只会呼吸的水母:p5.js 生成艺术

把一段高度压缩的 p5.js 数学代码展开为可交互的发光水母:支持鼠标跟随、视差、扰动、拖尾、渐变和实时参数控制。

用 20,000 个点画一只会呼吸的水母:p5.js 生成艺术

一段不到两行的 p5.js 代码,可以让 20,000 个半透明点组合成一只持续呼吸、摆动和流动的“水母”。它没有模型文件,没有图片,也没有粒子物理引擎;每一个点的位置都来自同一组由索引和时间驱动的数学公式。

我把原始作品展开为一个完整的交互网页,并在保持核心公式不变的基础上加入了:

  • 鼠标与触摸跟随;
  • 基于深度的视差;
  • 随指针距离增强的局部扰动;
  • 可调节的帧间拖尾;
  • Canvas 发光和 Screen 混合;
  • 沿纵向、深度与时间流动的 HSB 渐变;
  • 粒子数量、速度、拖尾、发光、色相等实时控制;
  • 暂停、重置、隐藏面板与 PNG 保存。

在独立页面中全屏体验 →

移动鼠标或在触屏上拖动,观察水母的跟随、视差与扰动。若设备性能有限,可先降低“粒子数量”或关闭发光。

原始代码

原始 code-golf 版本为:

1
2
a=(y,d=mag(k=(8+sin(i/19+t))*cos(i/49),e=y/8-12)**2/79+1)=>point(k/d*4-e*sin(k)+k/(d*d)*(12+d*6*sin(d*d-t+cos(t/3)+.3*sin(e)))+200,12*sin(d*2.6-t)+d*66+40)
t=0,draw=$=>{t||createCanvas(w=400,w);background(9).stroke(w,66);for(t+=PI/80,i=2e4;i--;)a(i/99)}//

这里用到了大量压缩技巧:省略变量声明、复用全局变量、用默认参数执行赋值,并把初始化塞入 draw()。展开后,它的核心计算可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const y = i / 99;
const k = (8 + sin(i / 19 + t)) * cos(i / 49);
const e = y / 8 - 12;
const d = mag(k, e) ** 2 / 79 + 1;

const x =
  k / d * 4
  - e * sin(k)
  + k / (d * d) *
    (12 + d * 6 * sin(d * d - t + cos(t / 3) + 0.3 * sin(e)))
  + 200;

const py = 12 * sin(d * 2.6 - t) + d * 66 + 40;
point(x, py);

y 在这里不是屏幕纵坐标,而是从粒子索引换算出的采样参数。真正的屏幕纵坐标是最后计算出的 py

公式如何构成水母

1. 采样轴

1
y = i / 99

i 原本从 0 扫描到 19,999。除以 99 后,得到一条约为 0 到 202 的参数轴。每个索引都对应一个确定的点,因此这里所谓的“粒子”,更准确地说是参数化点云的离散采样。

交互版本允许改变粒子数量,但不会直接改变公式中的采样范围。它把任意数量的点重新映射回原作的固定区间:

1
sampleI = n / (count - 1) × 19999

所以无论绘制 3,000 还是 30,000 个点,水母的完整轮廓都会保留,改变的只是密度与性能负载。

2. 横向摆动 k

1
k = (8 + sin(i / 19 + t)) × cos(i / 49)

8 + sin(...) 给出稳定的基本宽度和小幅呼吸,cos(i/49) 则以另一种频率改变符号与幅度。两个不同频率相乘会形成拍频和干涉,因此轮廓不会像规则圆环一样机械。

时间 t 进入第一个正弦波,让相位沿点云持续传播。k 后续既参与横坐标,又参与深度 d,所以它同时影响宽度、卷曲和纵向层次。

3. 中心化参数 e

1
e = y / 8 - 12

这一步压缩采样轴,并把零点移到采样区间内部,使 e 同时拥有负值和正值。后面的 -e × sin(k) 借此产生左右翻折;sin(e) 又会给内部波纹加入细小褶皱。

4. 深度层级 d

1
2
d = mag(k, e)² / 79 + 1
  = (k² + e²) / 79 + 1

d 是点 (k,e) 到参数原点的平方距离,经缩放后再加 1。平方距离让远处点增长更快,形成明显的内外层次。

+1 也很关键:后面多次出现 1/d1/d²,它避免了除以零。最终纵坐标包含 66d,所以 d 越大的点总体越靠下,形成从伞盖到裙边的展开。

5. 内部波相位

1
phase = d² - t + cos(t / 3) + 0.3 × sin(e)
  • 让不同深度拥有非线性变化的相位,产生层状波纹;
  • -t 让波纹持续向前传播;
  • cos(t/3) 是低频的全局推拉;
  • 0.3sin(e) 沿采样轴加入不规则的小褶皱。

6. 最终横坐标

1
2
3
4
x = 4k/d
    - e × sin(k)
    + k/d² × (12 + 6d × sin(phase))
    + 200

它可以理解为四层叠加:

  1. 4k/d:中心附近的横向摆动;越远离核心,贡献越小。
  2. -e × sin(k):产生左右翻折和交叉的裙边结构。
  3. k/d² × (...):把中心收束与沿深度传播的波结合起来。
  4. +200:把数学原点移到原始 400 像素画布的水平中心。

第三项还可以展开为:

1
12k/d² + 6k/d × sin(phase)

第一部分在核心附近更强,第二部分负责沿深度传播的摆动。1/d²1/d 的不同衰减速度,是伞盖内部和外围运动尺度不一致的原因之一。

7. 最终纵坐标

1
py = 12 × sin(2.6d - t) + 66d + 40

66d 是向下展开的主要骨架,12sin(2.6d-t) 则在纵向加入传播波,让各层上下起伏。+40 为顶部留出空间。

水母感并不来自某个单独的“水母函数”,而是来自多层结构同时作用:平方距离建立深度,倒数项收束核心,多组不同频率的三角函数制造褶皱和干涉,时间再让横向与纵向相位共同流动。

动画与视觉效果

主循环与时间

p5.js 只调用一次 setup(),然后持续调用 draw()。原作每帧执行:

1
t += π / 80

网页版本加入了帧时间修正:

1
t += (π / 80) × speed × dt

这样动画速度更接近真实时间,不会在 120 Hz 屏幕上快一倍,也不会因为短暂掉帧产生过大的时间跳变。

Alpha 叠加

单个点的透明度很低,但大量点在相近位置重叠后,亮度会逐渐累积。作品对粒子使用 Screen 混合模式,使重叠区域越来越亮,适合表现光、水和雾状体积。

拖尾

网页没有在每帧彻底清空画布,而是在旧画面上盖一层半透明深色矩形。旧像素因此经过多帧逐渐衰减:

  • 拖尾为 0 时,覆盖层接近不透明,上一帧立即消失;
  • 拖尾提高后,覆盖层变得更透明,运动轨迹保留得更久。

这种方式不需要保存历史粒子,直接利用 Canvas 已有的帧缓冲即可。

发光

发光由 Canvas 的 shadowBlur 与稀疏的第二层粗点共同完成。为了避免绘制量直接翻倍,第二层只抽样一部分粒子。发光比纯点绘制更消耗性能,因此低性能设备应优先降低发光强度。

颜色渐变

作品使用 HSB 色彩空间,色相由纵向位置、深度和时间共同决定:

1
2
3
hue = baseHue
      + hueSpread × verticalMix
      + 18 × sin(0.45t + 0.7d)

水母上下区域因此拥有不同颜色,渐变还会随时间缓慢流动。亮度、饱和度和透明度同时依赖深度,进一步强化了体积感。

鼠标跟随、视差与扰动

指针位置先从画布坐标映射到 -1…1,再经过两层线性插值。第一层过滤突然跳变,第二层制造滞后,视觉上更像柔软身体带着惯性追随。

d 被进一步转换成一个近似深度值。靠近核心的点获得更多指针位移,远层获得较少位移,因此产生柔和的二维视差。

鼠标离画面中心越远,pointerEnergy 越高。它会放大一组依赖粒子索引与时间的正弦扰动,所以移动到边缘时水母会变得更活跃,而回到中心后逐渐平静。

参数和快捷键

参数默认值作用
粒子数量18,000点云密度;对性能影响最大
动画速度时间变量增长速度
拖尾长度78%旧帧保留时间
发光强度18 px光晕模糊半径
基础色相190°整体主色调
渐变跨度95°水母上下区域的颜色差异
鼠标扰动0.8指针引发的局部波动
跟随幅度34 px整体跟随与视差强度

快捷键:

按键功能
Space暂停或继续
H隐藏或显示面板
R恢复默认值
S保存当前画面为 PNG
T开关拖尾
G开关发光
[ / ]减少或增加粒子
↑ / ↓调整速度
← / →调整色相

暂停只会停止时间演化,鼠标交互仍然有效,便于观察同一数学形态在不同指针位置下的变化。

性能建议

如果动画不够流畅,按这个顺序调整:

  1. 将粒子数量降到 8,000–12,000;
  2. 降低发光强度,或按 G 关闭发光;
  3. 缩短拖尾,防止画面过度累积;
  4. 关闭其他高负载页面或应用。

网页固定使用 pixelDensity(1),避免高分屏把 Canvas 实际像素数放大数倍。窗口改变尺寸时,公式仍然在原始的 400 × 400 坐标系里计算,只在外部统一缩放,因此水母不会因屏幕比例不同而变形。

后续可以继续做什么

  • 增加“深海蓝”“霓虹紫”“极光绿”等预设;
  • 把参数写进 URL,让特定视觉状态可以分享;
  • 用 Web Audio API 让低频控制呼吸、高频控制发光;
  • 使用 MediaRecorder 录制 WebM 动画;
  • 把公式迁移到 WebGL 顶点着色器,扩展到十万级粒子;
  • 同时绘制多个不同大小、色相和时间偏移的水母;
  • 加入自动性能调节,根据实时帧率降低光晕或粒子密度。

从短短两行 code-golf 到完整的交互作品,真正保持不变的是它的数学骨架。所有鼠标控制、发光和渐变都只是叠加层;水母之所以像软体生物一样呼吸,仍然来自 it、距离衰减和多组周期函数之间的干涉。

本文由作者按照 CC BY 4.0 进行授权