跳到主要内容

流动场

打开流动场实验。按住红色的涡流控制点或使用方向键可平移扰动中心;支持调节扰动强度滑块、切换暂停/播放状态,或重新生成随机流线初始相位。

速度向量场数学公式

每个粒子的行进方向由二维解析速度场 V(x,y)=(vx,vy)\mathbf{V}(x, y) = (v_x, v_y) 决定。该向量场融合了基础水平层流、正弦波动分量以及以 (xs,ys)(x_s, y_s) 为中心的旋转涡流扰动:

  1. 距离平滑核: r2=(xxs)2+(yys)2+0.025,r^2 = (x - x_s)^2 + (y - y_s)^2 + 0.025, 其中微小常数 ϵ=0.025\epsilon = 0.025 用于消除粒子接近涡流核心时的除零奇点。
  2. 水平与垂直速度分量: vx=0.80.22k(yys)r2,v_x = 0.8 - \frac{0.22 k (y - y_s)}{r^2}, vy=0.13sin(9x+seedmod19)+0.22k(xxs)r2,v_y = 0.13 \sin(9x + \text{seed} \bmod 19) + \frac{0.22 k (x - x_s)}{r^2}, 其中 kk 为涡流强度,seed\text{seed} 用于控制正弦波相位偏移。
  3. 步长归一化: 速度向量经归一化后乘以固定步长系数 0.0090.009 Vstep=0.009(vx,vy)(vx,vy).\mathbf{V}_{\text{step}} = 0.009 \cdot \frac{(v_x, v_y)}{\|(v_x, v_y)\|}.
const field = (x, y) => {
const {source: s, strength: k, seed: currentSeed} = settings.current;
const dx = x - s.x;
const dy = y - s.y;
const radius = dx * dx + dy * dy + 0.025;
const vx = 0.8 - dy * 0.22 * k / radius;
const vy = 0.13 * Math.sin(x * 9 + currentSeed % 19) + dx * 0.22 * k / radius;
const length = Math.hypot(vx, vy) || 1;
return {x: vx / length * 0.009, y: vy / length * 0.009};
};

双层渲染与离屏缓冲区优化

若在每一帧动画中对 62 条流线重复执行 250 步积分,每帧需进行 62×250=15,50062 \times 250 = 15{,}500 次向量场求值,将严重占用 CPU。

为保证渲染帧率稳定,渲染器将静态背景流线与动态行进粒子彻底分离:

  1. 离屏流线预绘制(p5.Graphics): 仅在参数调节或画布尺寸改变时,执行一次流线数值积分,将线条直接画入离屏图形缓冲区(paper),并将关键路径坐标保存在内存数组中。
  2. 动态粒子行进: 每一帧重绘时,画布首先以单次绘制调用贴图背景(p.image(paper, 0, 0)),随后仅需按帧计数模运算沿预存点集绘制移动的粒子圆点:
const trace = () => {
const css = getComputedStyle(node);
paper.background(css.getPropertyValue('--instrument-panel').trim());
ink = css.getPropertyValue('--instrument-red').trim();
const metal = css.getPropertyValue('--instrument-metal').trim();
paper.noFill();
paper.strokeWeight(0.8);
p.randomSeed(settings.current.seed);
paths = [];

for (let row = 0; row < 62; row++) {
let x = -0.1 + p.random(-0.025, 0.025);
let y = row / 51 - 0.1;
const points = [];
const color = p.color(row % 6 === 0 ? ink : metal);
color.setAlpha(row % 6 === 0 ? 145 : 78);
paper.stroke(color);
paper.beginShape();

for (let step = 0; step < 250; step++) {
points.push({x: x * p.width, y: y * p.height});
paper.vertex(x * p.width, y * p.height);
const v = field(x, y);
x += v.x; y += v.y;
if (x > 1.1 || y < -0.3 || y > 1.3) break;
}
paper.endShape();
paths.push(points);
}
dirty = false;
};

p.draw = () => {
if (!paper || disposed) return;
if (dirty) trace();
p.image(paper, 0, 0); // 单次贴图呈现全部静态流线

p.noStroke(); p.fill(ink);
paths.forEach((points, index) => {
const point = points[(frame + index * 17) % points.length];
p.circle(point.x, point.y, index % 6 === 0 ? 3 : 1.6);
});
if (settings.current.playing) frame++;
};

交互手势与无障碍设计

  • 指针捕获(Pointer Capture): 拖动手柄激活 setPointerCapture,确保光标移出容器边界时拖拽仍持续生效,避免跳帧或手势丢失。
  • 键盘可访问性: 控制手柄支持 ArrowLeftArrowRightArrowUpArrowDown 方向键以 0.025 为单位微调坐标。
  • 自动播放: 首页预览和独立实验页均默认播放,不受系统“减少动态效果”设置影响。首页不提供播放按钮;独立页可手动暂停和播放。展品离开视口或浏览器标签页隐藏时,动画仍会暂停。
  • 生命周期回收: 组件卸载时销毁事件监听、观察者,并显式调用 paper.remove()sketch.remove()
探索关联打开关联网络