它在做什么
FIELD MAP 把一个确定性 seed 映射成连续方向场,再让粒子沿局部切线移动。指针不是在“拖动图片”,而是作为一个局部吸引或旋转源,实时改变附近粒子的方向。相同 seed 与参数会得到可以复现的初始结构;交互则让结构逐渐偏离。
关键实现
渲染循环只更新可见画布。IntersectionObserver 负责离屏暂停,visibilitychange 处理后台标签页;设备像素比上限为 2,避免高密度屏幕把像素量无条件放大。控件更新会复用画布与上下文,不创建新的循环。
键盘焦点进入画布后,方向键移动扰动点,空格触发脉冲,Escape 复位。触摸与鼠标共享 Pointer Events 路径,因此没有两套分叉的交互状态。
性能与降级
动态版本使用 Canvas 2D,不依赖 WebGL。减少动态效果时停止持续 RAF,改画静态方向采样;无 JavaScript 时保留内联 SVG。画布不可用或初始化失败时也不会遮住标题、控件说明与原理正文。
浏览器兼容
现代 Chromium、Firefox 与 Safari 均支持所需的 Canvas、Pointer Events 和 Observer 能力。较旧浏览器会停留在 SVG 回退,不影响页面导航与内容阅读。
KEY SOURCE / LIVE IMPLEMENTATION
运行源码节选
这不是为文档复制的示例。构建过程直接读取当前实验使用的 TypeScript 文件,并提取下方标记区段。
const angleAt = (x: number, y: number, time: number) => {
const nx = x / Math.max(width, 1);
const ny = y / Math.max(height, 1);
const phase = (seed % 997) / 997;
const slowTime = reducedMotion ? 0 : time * 0.000045;
let angle =
Math.sin((nx * 3.1 + phase + slowTime) * TAU) * 1.1 +
Math.cos((ny * 2.7 - phase * 0.7 - slowTime) * TAU) * 0.85 +
Math.sin((nx + ny) * TAU * 1.35 + phase * 8) * 0.45;
if (pointer.active || pulse > 0.01) {
const dx = x - pointer.x * width;
const dy = y - pointer.y * height;
const distance = Math.max(24, Math.hypot(dx, dy));
const influence = Math.min(1, (Math.min(width, height) * 0.42) / distance);
const tangent = Math.atan2(dy, dx) + (pointerDown ? Math.PI : Math.PI / 2);
angle = angle * (1 - influence * 0.72) + tangent * influence * (0.72 + pulse * 0.28);
}
return angle;
}; 同一段方向函数同时驱动实时粒子与 reduced-motion 静态采样。