本文为通用技术教程,全部代码可在浏览器控制台或单页项目中复现。
为什么“自己写”反而更轻
提到力导向图,第一反应是引 D3-force。但处理几十到几百个节点的规模时,核心物理只有三股力,O(n²) 的两两斥力在这个量级毫无压力。自研的好处是:能精确控制何时停帧(省电)、如何降级(reduced-motion)、以及坐标系统(与缩放平移共用一套换算)。
三股力的配平
每个 tick 依次施加:
// 1) 斥力:任意两节点之间,距离越近推得越狠
for (let i = 0; i < nodes.length; i += 1) {
for (let j = i + 1; j < nodes.length; j += 1) {
const dx = a.x - b.x,
dy = a.y - b.y;
const distSq = Math.max(1, dx * dx + dy * dy);
if (distSq > 90000) continue; // 远处的力忽略,性能与稳定双赢
const force = REPULSION / distSq;
const dist = Math.sqrt(distSq);
a.vx += (dx / dist) * force;
b.vx -= (dx / dist) * force;
}
}
// 2) 弹簧力:有边的节点被拉向理想长度
const force = (dist - SPRING_LENGTH) * STIFFNESS;
// 3) 向心力:防止整张图飘走
node.vx -= node.x * GRAVITY;
然后是积化和衰减:
node.vx *= DAMPING; // 阻尼,0.86 左右
node.x += node.vx * alpha;
参数的量级感:斥力 2000–5000,弹簧长度 80–120,刚度 0.01–0.02,重力 0.001–0.002。斥力与弹簧的比值决定“图是抱团还是散开”,重力决定“能不能被弹簧拉出凸包”。
alpha:让图自己停下来
物理循环最大的工程问题是 CPU。解法是引入衰减因子 alpha:
alpha = Math.max(0.02, alpha * 0.996);
每帧位移乘以 alpha,运动幅度指数衰减;当所有节点速度足够小(或 alpha 触底)就 cancelAnimationFrame,循环彻底停止。用户拖拽、缩放或改筛选时再把 alpha 抬回 0.55 重新加热——绝大多数时间 CPU 占用为零。
const frame = () => {
tick();
draw();
if (alpha <= 0.03 && !dragging) {
running = false;
return;
}
requestAnimationFrame(frame);
};
拖拽、缩放、平移的坐标换算
Canvas 的世界坐标与屏幕坐标通过一个仿射变换关联:
// 世界 → 屏幕:先平移到画布中心,再缩放
screenX = worldX * scale + width / 2 + panX;
// 屏幕 → 世界(命中测试用)
worldX = (screenX - width / 2 - panX) / scale;
三个要点:
- 命中测试必须用世界坐标。把鼠标位置反算回世界系,再找半径 +6/scale 范围内最近的节点,才能保证任何缩放下手感一致。
- 滚轮缩放要锚定光标。缩放前后,光标下的世界点必须静止不动:
const factor = Math.exp(-event.deltaY * 0.0012); panX = cursorX - width / 2 - worldX * nextScale; scale = nextScale; - 拖拽节点时把该节点标记为 pinned,物理循环跳过它的位置积分,松手解除。节点被“拎”起来时,弹簧会自然把邻居拖过来——这是力导向图最有生命感的瞬间。
绘制层的两件事
- DPR 适配:
canvas.width = cssWidth * devicePixelRatio,再setTransform(dpr, 0, 0, dpr, 0, 0),否则视网膜屏上全是锯齿。 - 视觉编码:半径 ∝ √degree,高连接节点画径向渐变光晕;标签只在“选中、悬停、大节点、放大”四种情况绘制,避免几百个文字糊成一团。
reduced-motion 与节能降级
prefers-reduced-motion 不是“不画”,而是“不逐帧画”:
if (reducedMotion.matches) {
for (let i = 0; i < 240; i += 1) tick(); // 同步跑完物理
draw(); // 画一帧静态终态
}
拖拽、缩放、平移是直接操作,应当保留——只是没有惯性动画。另外两个节能开关:IntersectionObserver 监听画布离开视口即停帧;document.hidden 时暂停。
停止条件清单
发布前逐条自检:
- alpha 触底或最大速度 < 阈值时,rAF 停止;
- 拖拽/平移期间循环保持运转,松手后重新衰减;
- 画布离开视口、标签页隐藏时停帧,回来时恢复;
- ResizeObserver 而非 window resize 适配容器尺寸;
-
astro:before-swap(或路由卸载)时取消帧、断开所有 Observer。
小结
力导向图的“高级感”不来自物理公式的复杂度,而来自三件事:力量配平的参数手感、彻底停帧的工程纪律、以及直接操作(拖拽/缩放)的坐标换算。三件事都做对,一个 200 行的自研引擎就能在几十节点规模上取代整个图库。