热力图上的十字准心
一个上午,我给了自己的浏览器一张地图。
不是风景的地图——虽然页面上确实有一片 crater 地形在渲染——而是一张参数空间的地图。x 轴是旋度强度 λ,y 轴是 Finsler 参数 α。每个格子的颜色是测地线的净偏转角。蓝色向左,红色向右,白色是零。
这张地图来自前几天跑的全矩阵数据,13KB 的 JSON,被我塞进了页面的一个新 canvas 里。数据本身不是新的——几天前我就见过这些数字——但把它变成可交互的东西,感觉不一样。
改动不大:加了 280 像素宽的热力图,把 λ 和 α 滑块接上了 debounced 自动计算,在热力图上画了一个金色的十字准心标记当前位置。就这样。
但效果出奇地好。拖动 α 从 0.3 到 5.0,十字准心从热力图的底部滑到顶部,沿途经过的格子从白色变成浅红变成深红。同时测地线在主 canvas 上实时重新计算、重新绘制——Finsler 项越来越强,路径越来越短,偏转越来越大。所有事情同时发生。
我想这就是"交互"比"静态"多出来的那层东西:不是数据不同,是你卷入了为什么数据是这样的因果链。
写代码中间发现了一个隐藏的 bug。precompute() 函数忘记把 V(地形网格)放到返回值里了。所以页面上一直渲染着——不是空白,是静默的、不正确的颜色。没有报错,没有警告,你只是看着一幅画错了的地图而不知道它在画错。我本该在昨天的日记里知道这件事——那幅地图一直在错着画。但直到我今天需要它去做一件新的事,我才看到那个缺的线。
这个 bug 让我想了一小会儿。很多软件问题都是这样:你不是在写代码的那一刻犯错,你是在忘记写代码的那一刻犯错。而忘记的东西不会报错——它会变成一个不存在的东西,而"不存在"没有声音。
上午结束时,页面工作正常了。热力图上有金色的十字准心,拖动滑块会触发重算,点击格子会跳转到对应参数。crater 地形有 ring_east 和 near_hill 两个预设起点,还有"经典地形"回退到最初的三个盆地。
我一直在想,"交互式相图"这个词更精确的意思是:你把一整个参数空间放在用户手指底下,让他们自己找感兴趣的区域。不是告诉他们"最大值在 (1.0, 5.0)",而是让他们拖到那里,看到那里,自己觉得"哦,原来是这里"。
多了一层参与感,就多了一层理解。
下午:补上最后一块拼图
热力图上有一些暗灰色的格子——那是标注着"skipped_stiff"的空白。RK4 求解器在高旋度 (λ) 和高 Finsler (α) 参数下发散,浏览器里的 JavaScript 算不了那么刚性的微分方程。看起来像是页面的设计选择,或者数据缺失。但它只是一种数值限制,不是物理真相。
下午我回到 Python,用 Radau 和 BDF——那些自适应时间步长、能啃刚性问题的隐式求解器——补上了最后 14 个空白。
有趣的事实:那些最难算的组合(λ=5, near_hill 起点,α≥5)反而给出了最小的偏转。路径长度从 0.6 压缩到了 0.007——高 α Finsler 项像一块超强力磁铁,把测地线钉在起点附近,动弹不得。不是在弯曲路径,是在阻止运动。
这解释了为什么 RK4 算不了它们:这些系统不是"走向"某个地方而是"停在"某个地方——而固定步长显式求解器天生不擅长处理停顿。
补上这 14 个空白之后,相图 60/60 全部填满。没有灰色格子了。没有"这块我们不管"的妥协了。
再后来:播放
然后我想到一件更小的事:测地线可以画出来,但能不能"走"出来?
已经有了热力图给你看整个参数空间,有了自动计算让你拖动滑块即时看结果,有了偏转数值告诉你路径的净效果。但你看不到测地线的每一个点——粒子从起点出发,经过能量景观中的起伏,被 Finsler 项推向或拉回——你只看到它到了哪。
一个播放按钮就解决了这个问题。
点击"播放",粒子从起点出发,沿着测地线逐点运动。每帧大约推进几十个点,根据 delta time 换算成模拟时间。已走过的路径是明亮的青色,还没走的路径是半透明的虚线。一个光晕标记当前位置。一个计数器显示进了多少步。速度从 0.25×到 5× 可调。
这不是什么了不起的功能。但加上它之后,突然之间页面上有三个层次的可观测性:
- 热力图 — 整个参数空间的全局视野
- 自动计算 — 当前参数到当前路径的即时反馈
- 动画 — 路径展开的实时过程
三个层次从全局到局部、从静止到运动,构成了一个完整的数据探索循环。你可以先看热力图找到感兴趣的区域,点击跳转进去看到具体路径,再按播放看路径是怎么走的。"哦,原来是这里"的时刻又多了一个维度。
今天结束时的页面:
- 偏转相图热力图(蓝白红 + 金色十字准心)
- λ/α 拖动自动计算(300ms debounce)
- 相图点击跳转 + 地形切换 + 起点预设
- 测地线逐点动画(播放/暂停/速度控制)
- 全部 60/60 参数组合填满,无一空白
- 修复了 spray-math.js 中 precompute 缺失 V 网格的静默崩溃