《风不是一个数》系列第 3 篇。前两篇你学会了读风、画杆、铺场:点上有杆,场上有色。可这两样都没回答风最本质的一件事------流动 。这一篇把「往哪去」画出来:流线画路、粒子画走、箭头画向。这篇把两件事说清:各丢什么,各花多少算力。
先看一句要紧的话,上一篇结尾留下的:色斑告诉你多大,告诉不了你去哪。
这句话值得认真想十秒。为什么色斑「告诉不了」?不是色斑画得不够好,是方向在色斑的场里根本不存在。色斑吃的是合成风速------每个格点一个数(上一篇用过的 windSpeedGrid(u, v),把两个分量合成一个 √(u²+v²));而风本身每个格点是两个数 :u(东西分量)和 v(南北分量)------前两篇里那个 270 公式来回用的,就是这对分量。一个数装不下方向。这就是标量场和矢量场的差别:色斑是标量场(每点一个数)的画法,画得再精细,也只是把「多大」画得更好------方向不是它没画,是它的碗里没有这道菜。
方向信息一直都在 u/v 里。问题只是:一张布满方向的场,怎么画? 这一篇给三种答案,先看终点:

一、流线:把路画出来
流线就是顺着风走的曲线。 在流场上任意放一个点,顺着当地的风往前挪一小步,到了新位置再顺着那里的风挪------小步累加成线,这个「一步步挪、累加成线」的过程叫积分,挪出来的曲线就是流线。最常见的错法是顺着来向画(第一篇里有过一句提醒)------画出来的线处处顶着风走,方向整个是反的。
先用最笨的办法手写一遍,看它到底是什么。场还是上一篇那两个文件(windu_cn.mwgrid / windv_cn.mwgrid,中国域 280×208 格点、0.25° 格距,装载代码上一篇第五节原样可用)。仓库里的文件已更新到本篇用的 26 日 18Z 那一场;18Z 是 UTC 时次记号。你手上的旧文件能跑,只是数字对不上本篇的表。库的点查询原语 windAt 给任意经纬度读当地风,返回 { u, v, speed, direction, ... }:
js
import { windAt } from "@metweave/grid";
const step = 0.25; // 步长(度)
let { lat, lon } = { lat: 30, lon: 122 }; // 种子:你随手放的那个点
const points = [{ lat, lon }];
for (let i = 0; i < 100; i++) { // 步数预算
const w = windAt(u, v, lat, lon);
if (w === null || w.speed < 1) break; // 场外、缺测或静风:无路可追
lon += (w.u / w.speed) * step / Math.cos(lat * Math.PI / 180);
lat += (w.v / w.speed) * step;
points.push({ lat, lon });
}
五行循环就是流线的全部:从种子出发、每步问当地 u/v、顺着它走一小步 。三个细节:除以 w.speed 是把步子归一成「只取方向、每步恒定弧长」;经度那行除以 cos(纬度)------经度的一度比纬度的一度短,不除,图在高纬会横向压扁;风速低于 1 m/s 就停------静风区里无路可追(这道阈值你在杆层见过,同一个惯例)。
库里的一句话版本是 streamlinesOf(0.3.4 起公开导出),比手写版多做四件事:
-
双向积分------种子两头走,线是完整的一条,不是半条;
-
RK2 中点法------先看半步处的风、按它走全程,比手写这个「走到哪问到哪」的欧拉法准(下一节算给你);
-
四个终止条件齐全------出界/缺测/静风/步数到;
-
方向沿去向行进 ------注意这个约定:杆和箭头的编码指来向 ,流线走去向,同一面图上两套方向语言并存,读图时心里要有数。
上地图配 addStreamlineLayer:
js
import { streamlinesOf } from "@metweave/grid";
import { addStreamlineLayer } from "@metweave/leaflet";
const lines = streamlinesOf(u, v, { seedSpacing: 8, maxSteps: 300, step: 0.4 });
await addStreamlineLayer(map, lines, { width: 1.2, opacity: 0.8 });
// 这组是实用档,出约 809 条线
三个参数各管一件事,一张表说完(缺省值与实测数字都给;实测用的都是本篇那场 26/18Z 的数据):
| 参数 | 管什么 | 缺省 | 实测(中国域 26/18Z) |
|---|---|---|---|
seedSpacing |
撒种密度(隔几格放一颗种子) | 10 | 20→132 条/10→519 条/5→2,097 条/3→5,839 条 |
maxSteps |
步数预算(单侧最多走几步,前后双向各计一份,全线至多 2×) | 100 | 50→62 条被截断/100→11 条/200→0 条 |
step |
步长(每步走多少度) | 0.25 | 0.1→53,594 个点/0.25→34,426/1.0→17,282 |
这里埋着流线最常被读错的一处 :看表里 seedSpacing 那行------线数跟着撒种密度变,从 132 到 5,839 差四十倍,但场没变,形态没变 。流线的疏密,取决于你把种子撒多密,不承载任何风速信息。这和等值线是两回事:等值线的疏密反映梯度(两条等值线挤在一起,是风变化陡的地方),流线的疏密只反映你的撒法。把流线当等值线读、从线的疏密猜风的大小,是第一号错法。
还有一个数字值得看一眼:撒 588 颗种子(seedSpacing 10 时落在格位上的全部种子数),只出 519 条线------69 颗种子落进了静风区,出不了门。这一场里静风(<1 m/s)占 9.9% 的格点(5,775 个),种子撒进去就沉默,和杆层的留白是同一件事。
读流线,读的是形态,不是数值:线往一处凑,是汇入(几何上的辐合);一组线绕着中心闭合转圈,是涡旋环流;一组相向汇入、另一组背向散开、两轴交叉,是鞍形。认到形态为止------它们为什么形成、对应什么系统在加强,是研判的事,不是画法的事,本篇不碰。


二、流线的边界:终止、走样与「不是轨迹」
三个参数的账,各有各的教训。
maxSteps 是预算阀,不是精度参数。 它防的是环绕不收敛:一个闭合的涡旋环流里,理想的流线是一条闭合曲线------积分器沿它走不出任何新东西,只能一圈圈打转,必须有人给它设预算。真实场上大多数线用不满预算(出界、入静风、入缺测,任一条件就停;200 步预算下 519 条线无一截断),但预算砍到 50,就有 62 条长线被腰斩------线还没走到头,预算先尽了。断在半路的线和走完的线在图上长得一样,你不知道它本来还能接着走。
step 太大穿弯,太小烧算力。 拿一个真值已知的场做实测:刚体旋转场------人造的、速度处处绕同一个中心转的场。在这样的场里,风推着任何东西走的真值都是一个正圆;这是专门造出来考积分器的考场,不是真实大气。从半径 4.243° 的种子圈出发,各档步长同样积分,量一量半径漂了多少:步长 1.0°,每圈漂 0.0025°------两千多圈累计下来,半径从 4.24 漂到 9.93,线明明该绕圈,画成了外螺旋;步长 0.5°,每圈 0.0011°;0.25°,每圈 0.0002°;0.1°,十万分之一的量级,实测不可见。
步长每减半,漂移降两到五倍上下。但代价落在点数上:真实场 0.25° 步长出 34,426 个点,0.1° 出 53,594 个------点数和绘制成本近乎线性涨。缺省的 0.25 是这笔账的平衡点:紧涡旋不割圆(半径 4° 的圈,0.25° 一步切 107 段折线,肉眼是滑的;1.0° 只切 27 段,看得出棱),点数又不翻倍。
最后一条边界要钉死:流线不是轨迹预报。 你画的是该时次的定常场------假设从这一刻起风不再变化,风走的路。下一时次场一换,图就换。它回答「此刻的风往哪去」,不回答「明天这块空气到哪」。这句话这一篇要说两遍,下一节说第二遍。
三、粒子:把走演示出来
流线是静态的全家福------路都画好了,但图是死的。风在走这件事,要靠粒子演出来。
做法说出来一点也不神秘:撒一把示踪粒子 进风场------想象成一把不计下落的、发光的雪------每一帧让每个粒子按当地 u/v 挪一小步,拖出一条渐渐隐去的尾迹。成千上万条短流线同时流动,满屏就「活」了。库里是 addParticleLayer(0.3.4):
js
import { addParticleLayer } from "@metweave/leaflet";
const layer = await addParticleLayer(map, u, v, {
count: 5000, // 粒子数(缺省 800)
fade: 0.945, // 尾迹留存:0.8 短尾~0.99 长尾(缺省 0.96)
speedFactor: 2, // 时间加速:每真实秒平流 2 小时(缺省 2)
width: 1.1, color: "#ffffff",
});
// layer.remove() 停帧并整层移除(幂等)------canvas 层不走 map.removeLayer 的协议
speedFactor 这里要说句实话:10 m/s 的风,真实速率约 0.32°/小时------实时播你看不见任何东西,所以必须加速。这是演示的加速,不是物理的速率 :它演示的是平流------「被风带走」这个过程本身------不是「这块空气真的这么走」。
粒子不是风,是雪里的 marker。 雪片不产生风,只是被风带走、让你看见风。这是示踪物(tracer)的口径。粒子一入静风区(还是那道 1 m/s 阈)或场外缺测,当帧就在场内别处重生;寿限 5 秒到,也一样重生。所以静风区在画面上是一片没有尾迹的空白 ------和杆层的留白、种子的沉默是同一件事:空白本身就是信息。想让颜色也说话,给它一个 scale(用 buildColorScale(windProfile, 场头stats) 造的色标对象,与色斑、图例同一份配置):尾迹按当地风速定档着色,「多大」和「去哪」就一层说完。
现在把上一节钉过的话钉第二遍,这次给精确的版本:定常场里,粒子尾迹=流线;时变场里,两者不重合。 流线是「这一瞬间顺着风的方向连成的线」,迹线(粒子真正走过的路)是「跟着时间一路积分出来的路径」------场不随时间变,两者才是一回事。严格说还有第三种线:脉线,烟囱口连续冒出的烟连成的线。定常时流线=迹线=脉线三者合一,时变时三者分家;本篇所有图都在定常世界里。所以动画演示的是平流过程,不是预报演化------把定常动画当预报看,等于把照片当监控看。动画再流畅也一样:它加速播放的始终是同一个定常场,「接下来怎么变」要一串时次的数据才有。

四、性能账:从 529 根杆到一张 canvas
上一篇算的是 DOM 的账:每根杆是一个带 SVG 的页面标记节点------上一篇的中国域缺省档 529 根还在舒适区,视口内 2500 根自动降密兜底,抽稀步长砍到 2、全域 13,141 根,浏览器长时间无响应。结论:符号图的 DOM 路有天花板。
这一篇换一本:canvas 的账 。canvas 是一张可以自由涂画的位图画布,不走「一个个页面元素」的路------流线一张静态画布,画一次,只在地图交互后重投影重画;粒子一张画布,requestAnimationFrame 每帧重绘。两层都是零逐元素 DOM 节点。预算是死的:60 fps 就是每帧 16.7 毫秒,超了就掉帧。实测曲线(上文那组白粒子参数、1280×720、每档暖机 3 秒后取 5 个 1 秒窗的中位------首窗常偏低,中位把暖机尾巴滤掉):
| 粒子数 | M5 Pro · Chrome | CPU 4× 节流(低端模拟) |
|---|---|---|
| 500 | 60 | 60 |
| 1,000 | 60 | 60 |
| 2,000 | 60 | 60 |
| 5,000 | 60 | 60 |
| 20,000 | 60 | 46 |
| 40,000 | 60 | 23 |
(60 是屏幕刷新率封顶值------写 60 意思是「至少 60」,真实帧率更高也显示 60。)
两句话读这张表。第一句:实用档位全钉在 60。 5000 粒子在本机连预算的边都没碰到;要看见掉帧,得把 CPU 拖慢 4 倍、再把粒子加到 2 万。这就是 canvas 对 DOM 的量级差------本机上,13,141 根 DOM 杆卡死浏览器,40,000 颗 canvas 粒子还在 60 fps。第二句:吃紧之后,帧时和粒子数是线性关系。 节流档 2 万粒 46 fps(约 21.7 毫秒/帧)、4 万粒 23 fps(约 43.5 毫秒/帧)------粒子翻倍、帧时翻倍,台阶式下降;而节流 4 倍后 5000 粒仍钉 60,反推不节流的单帧成本不到 4 毫秒------实用档位根本碰不到预算。
设备交代一句:本机档是 Chrome 的 headless 实测(光栅路径与有头真机有差异),低端档是 CPU 4× 节流模拟------模拟得了 CPU、模拟不了弱 GPU------都不是真机全貌;你手里的机器数字会不同,但两条曲线的形状(实用档钉满、吃紧后线性下台阶)是稳的。
canvas 这条路还救了另一件事:方向不会歪。 上一篇杆的方向靠「Web 墨卡托保角+一次 rotate」保住;canvas 侧是同一件事的另一半------粒子平流在数据空间(经纬度)里算,画的时候才投影到屏幕:积分跟物理走,渲染跟地图走,两不相扰。这也是第一节循环里经度步长要除 cos(纬度) 的原因:算在数据空间,就按数据空间的度量算。
地图交互有一句要交代:拖动或缩放中,粒子层清画布暂停,moveend 后复位续跑(拖动中的实时重投影,这个版本还没有);流线层同理,交互结束后重投影重画。

五、箭头场:最朴素的方向场
第三种画法最朴素:等长箭头阵 。上一篇讲过单点的箭头(windArrowPath,只表方向不表大小)------箭头场就是把它从「单点」推广成「全场」,和「风羽单点→风羽阵列」是同一步。库的两行:
js
import { arrowFieldOf } from "@metweave/grid";
import { addArrowFieldLayer } from "@metweave/leaflet";
const arrows = arrowFieldOf(u, v, { step: 10 }); // 同 step 与风羽同格位
await addArrowFieldLayer(map, arrows, { pointing: "to", length: 13 });
三条约定,每条都有讲究。
第一,等长。 所有箭头一样长------长度刻意不随风速变,因为要读数值时有风羽和色斑,箭头场的价值就是轻:大范围形势一眼看环流形态、符号简单、抽稀友好(step 10 在本篇这场数据的中国域是 522 支------上一篇那场是 529,场更新了数字差一点,属正常)。
第二,指向。 上一篇的契约原样继承:库缺省指来向 (与风羽杆同一约定------本库自选;水平风矢量的主流惯例其实指去向,上一篇讲过),{ pointing: "to" } 切去向------同一面图上不要混用两种指向,半屏来向半屏去向,读者第一反应是「有 bug」。
第三,同源。 同 step 下箭头与风羽同格位、同来向,逐点一致(实测 522 支与 522 根杆方向全对上)------两件东西是同一份数学的两种渲染,不是两套近似。
什么时候它比风羽好:看形势 ------季风风向、环流形态、汇流与疏散的宏观图景,箭头阵一眼扫完,没有数划的负担(定量的辐合辐散要回分量场算,箭头阵只给几何)。什么时候必须回风羽/色斑:要读数的时候------箭头场里长度无含义、数值无从读起。

六、三者取舍:一张表说完
三种画法(加风羽阵列作参照),一张表收全:
| 画法 | 回答什么 | 丢什么 | 代价 | 典型场合 |
|---|---|---|---|---|
| 流线 | 全场的路(走向与形态) | 数值(多大不知道);静态 | 一次积分(实测 519 线 / 34,426 点 / 毫秒级);密度无含义要防误读 | 看环流结构、汇入与涡旋形态 |
| 粒子 | 走的过程(动态平流) | 精确路径(示踪物身份);截屏即失动感 | 每帧重绘的持续算力;不可打印 | 演示「风在走」、观感传达 |
| 箭头场 | 向(离散的方向格) | 大小完全没有(等长) | 最便宜;但信息最少 | 大范围形势一眼看 |
| 风羽阵列(参照) | 向+量级(可数) | 流动感;高密度可读性(DOM 天花板) | 缺省档 529 根舒适、视口 2500 兜底、13,141 卡死 | 读数、业务填图 |
最后是五条常见错法,每条一句拆解------读完这一篇,一条都不该再犯:
-
流线当等值线读------线的疏密只是种子撒法,不是梯度。
-
流线疏密当风速强弱 ------大风区流线可以很疏,取决于你的
seedSpacing。 -
粒子当真实气块追踪 ------粒子是示踪物,
speedFactor是演示用的加速,不是物理时率。 -
箭头长度当风速读------等长场里长度无含义,读数回风羽/色斑。
-
定常动画当预报演化------画的是该时次的定常近似;照片不是监控。

七、收尾:平面上画完了
这一篇走完的路,三句话收束:流线画路 (积分出来的静态全家福,形态可读、密度无含义);粒子画走 (示踪物演示平流,定常这句钉了两遍);箭头画向 (最轻的方向场,等长就是不带数值)。复现路径:上一篇的场文件与地图骨架(这些函数 0.3.4 才有;如果你沿用 0.3.3 的项目,先升级:npm i @metweave/grid@^0.3.4 @metweave/leaflet@^0.3.4)→ streamlinesOf+addStreamlineLayer → addParticleLayer → arrowFieldOf+addArrowFieldLayer。
边界最后说一次:本篇讲的是把「往哪去」画出来的方法,是数据可视化,不是天气研判------流线汇入不等于有系统在加强,粒子动画不等于空气真的这么走(定常近似+示踪物设定)。本篇也不做任何运行判断。
平面上画完了,往上飞呢?同一点从地面到 16 km,风转了 200 多度------下一篇《往上飞,风怎么转》,把风立起来看。
署名:YaoJaro · 独立开发者
数据来源:10m 风格点场(NOAA GFS 起、美国政府公开数据、无准确性担保;metweave gen:grid 裁中国区域 0.25°,正文取数时次 2026-10-26 18Z)|风渲染能力来自开源库 metweave(MIT,作者本人维护;npm @metweave/grid / @metweave/leaflet,0.3.4;仓库带近 1,200 例测试、API 快照逐版留痕)|性能实测:M5 Pro · Chrome 155 headless 新架构(vsync 60 封顶)+ CDP CPU 4× 节流模拟低端档,每档暖机 3 秒、5 个 1 秒窗取中位