Leaflet风向粒子实战:js与json资源直接拆包 简介这份资源面向需要在 Web 地图中呈现动态风场的开发者尤其是使用 Leaflet 构建气象可视化页面的前端与 GIS 从业者。包内共 3 个文件包含 1 个 js 插件脚本、1 个 css 样式文件和 1 个 json 数据文件压缩包约 231KB分别承担粒子渲染逻辑、图层视觉样式定义以及全球风向风速网格数据的职责。其中 js 插件基于 D3.js 解析流体动力学数据可将风速与风向转化为直观的粒子流动动画css 用于调整粒子大小、颜色与透明度json 则以网格形式存储各点位的风速风向信息供图层动态读取。已有 873 人学习下载说明该组合在风向粒子场景中具备一定参考价值。读者可据此快速搭建可交互的风场图层理解数据解析、图层创建与视图更新的完整链路并在此基础上按项目风格自定义粒子表现适合作为气象可视化入门与二次开发的实践素材。1. 风向粒子在 Leaflet 里到底怎么跑起来一份能直接拆包的 js 与 json 资源如果你做过气象可视化大概率遇到过这种需求底图上要有一层会流动的风场粒子箭头方向、疏密、速度都得跟着真实数据走而不是贴一张静态的 PNG 糊弄。Leaflet 本身只负责瓦片和交互粒子动画得靠leaflet-velocity这类插件来扛。问题在于很多人卡在第一步——插件 js 文件从哪来、风场 json 长什么样、字段名对不上就白屏。我手上这份资源就是干这个的一份可直接引入的leaflet-velocity脚本加一份结构完整的风向粒子 json 数据。它解决的不是“粒子动画原理”而是“我拿到数据后怎么在半小时内让粒子在图上转起来”。适合做气象看板、环境监测大屏、GIS 方向的前端和数据处理同学尤其是那种不想从零啃 GRIB 解码、只想先跑通效果的人。这份资源的核心是两件事js 负责渲染逻辑json 负责喂数据。leaflet-velocity的渲染思路是把风场拆成 U/V 两个分量在 canvas 上撒粒子粒子按分量方向移动并留下拖尾形成流线感。json 里如果只有风速没有风向或者 U/V 写反粒子就会朝反方向跑这种玄学问题排查起来很费时间。下面按“先看懂数据、再引入脚本、然后调参、最后避坑”的顺序拆开讲每一步都落到能抄的代码和参数上。2. 拆开风向粒子 jsonU/V 分量、网格与时间维度怎么对齐2.1 风场 json 的字段结构长什么样leaflet-velocity对数据格式有明确要求不是随便一个 json 都能喂。它期望的是一个数组数组里每个对象代表一个“层”或一个时间步常见结构如下[ { header: { parameterCategory: 2, parameterNumber: 2, lo1: 100.0, la1: 45.0, lo2: 130.0, la2: 15.0, dx: 0.5, dy: 0.5, nx: 61, ny: 61 }, data: [ ... ] } ]这里header描述网格范围lo1/la1是左上角经纬度lo2/la2是右下角dx/dy是网格步长nx/ny是横向和纵向格点数。data是一个一维数组长度必须是nx * ny按行优先排列。很多人拿到气象接口返回的二维数组直接塞进去结果粒子只在一个角落动就是因为没展平或者行列顺序反了。提示lo1不一定小于lo2la1也不一定大于la2取决于数据源坐标系。插件内部会按 header 计算网格映射但如果你发现粒子整体偏移先检查这四个值是否和实际数据范围一致。2.2 U/V 分量与风速风向的换算风场数据通常给的是 U东西方向分量和 V南北方向分量单位是 m/s。U 为正表示向西不对气象约定里 U 正为自西向东V 正为自南向北。leaflet-velocity内部用的就是这套约定。如果你手上只有风速和风向角需要先换算import math def wind_to_uv(speed, direction_deg): # direction_deg 是气象风向表示风从哪个方向吹来 # 转为弧度并转成 U/V rad math.radians(direction_deg) u -speed * math.sin(rad) v -speed * math.cos(rad) return u, v # 示例风速 10 m/s风向 45 度东北风 u, v wind_to_uv(10, 45) print(u, v) # 约 -7.07, -7.07逻辑说明气象风向 0 度表示北风从北吹来90 度表示东风。U 分量指向东为正V 分量指向北为正。所以从北吹来的风V 为负从东吹来的风U 为负。上面公式里负号就是干这个的。参数speed单位要和 json 里其他数据一致direction_deg用 0-360 度。如果换算反了粒子会朝风吹来的方向跑看起来就像倒放。2.3 多时间步数据怎么组织如果 json 数组里有多个对象leaflet-velocity会按顺序当作不同时间层。你可以用setData切换也可以配合时间滑块做动画。常见做法是每个时间步一个对象header 里的网格参数保持一致只换data。如果网格变了切换时会闪一下因为插件要重新计算映射。我一般会先把所有时间步的 header 对齐确保nx/ny/dx/dy完全相同再批量生成。// 假设 velocityData 是包含多个时间步的数组 var velocityLayer L.velocityLayer({ data: velocityData[0], displayValues: true, maxVelocity: 15 }); velocityLayer.addTo(map); // 切换到第二个时间步 velocityLayer.setData(velocityData[1]);参数maxVelocity决定粒子颜色映射的上限单位 m/s。设太小大风区全红设太大小风区看不出差别。一般取数据里风速的 95 分位左右或者直接看业务需求。3. 引入 leaflet-velocity 脚本从文件放置到图层初始化3.1 脚本与样式文件的引入顺序这份资源里的 js 是leaflet-velocity的构建产物通常还有一个配套的 css 或者不需要 css。引入顺序必须是 Leaflet 先、velocity 后否则L.velocityLayer会报未定义。link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script script src./js/leaflet-velocity.min.js/script如果你是在模块化环境里用比如 webpack 或 vite也可以import leaflet-velocity但要注意它依赖全局L得先import L from leaflet并挂到 window 上。我一般图省事直接 script 标签引入避免构建工具版本打架。3.2 初始化图层与关键参数初始化代码不长但参数决定效果var map L.map(map).setView([30, 110], 5); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; OpenStreetMap }).addTo(map); var velocityLayer L.velocityLayer({ displayValues: true, // 鼠标悬停显示风速风向 displayOptions: { velocityType: 风向粒子, position: bottomleft, emptyString: 无数据, angleConvention: bearingCCW, showCardinal: true, speedUnit: m/s }, data: velocityData, maxVelocity: 15, // 颜色映射上限 colorScale: [#3288bd, #66c2a5, #abdda4, #e6f598, #fee08b, #fdae61, #f46d43, #d53e4f], particleAge: 90, // 粒子寿命帧数 particleMultiplier: 1 / 300, // 粒子密度系数 lineWidth: 1.5, // 粒子线宽 frameRate: 15, // 帧率 opacity: 0.9 // 图层透明度 }).addTo(map);逻辑说明particleAge越大拖尾越长但太大会糊成一片particleMultiplier控制粒子数量分母越大粒子越少性能越好但流线感弱frameRate影响动画流畅度移动端建议降到 10。colorScale是颜色数组从低到高映射数量不限插件会自动插值。3.3 数据加载与异步处理json 文件通常几百 KB 到几 MB直接内联在 js 里会让文件很大。常见做法是 fetch 加载fetch(./data/wind-field.json) .then(function (res) { return res.json(); }) .then(function (data) { var velocityLayer L.velocityLayer({ data: data, maxVelocity: 15, particleAge: 90 }).addTo(map); }) .catch(function (err) { console.error(风场数据加载失败, err); });注意如果 json 是数组且包含多个时间步data传整个数组插件默认用第一个传单个对象也行。加载失败时先看控制台是不是 404再看 json 格式是否合法。我遇到过 json 末尾多逗号导致解析失败浏览器报错不明显用JSON.parse单独试一下就能定位。4. 参数调优与性能排查粒子密度、帧率与移动端适配4.1 粒子密度与视觉效果的平衡particleMultiplier是最容易调出玄学效果的参数。它的实际含义是“每个像素对应的粒子生成概率系数”值越小粒子越多。我一般从1/300开始试如果图上粒子稀稀拉拉改成1/500如果卡顿改成1/150。但要注意粒子数量还和地图缩放级别有关放大后同样系数会生成更多粒子因为像素多了。所以最好在目标缩放级别下调而不是在全局视野下调好就完事。// 根据缩放级别动态调整粒子密度 map.on(zoomend, function () { var zoom map.getZoom(); var multiplier zoom 7 ? 1 / 500 : 1 / 200; velocityLayer.setOptions({ particleMultiplier: multiplier }); });setOptions是插件提供的方法可以动态改参数。但频繁调用会重绘建议加个防抖。4.2 帧率与 CPU 占用frameRate默认 15已经比较流畅。如果设备性能差降到 10 甚至 8肉眼差别不大但 CPU 占用能降不少。另外particleAge和帧率共同决定粒子存活时间帧率降了粒子移动变慢可以适当减小particleAge保持拖尾长度。注意在移动端浏览器上canvas 的requestAnimationFrame可能被节流导致粒子动画卡顿。如果发现移动端明显掉帧先把frameRate降到 10再考虑减少粒子数量。4.3 数据量过大时的降采样如果网格是 0.1 度分辨率全国范围就是几百乘几百data数组几十万长度粒子计算量很大。常见做法是降采样到 0.5 度或 1 度视觉上差别不大性能提升明显。降采样时注意保持 U/V 的物理意义不要简单隔点取最好做区域平均。import numpy as np def downsample_uv(u, v, nx, ny, factor5): # u, v 是一维数组长度 nx*ny u2d u.reshape(ny, nx) v2d v.reshape(ny, nx) # 按 factor 分块平均 new_ny ny // factor new_nx nx // factor u_ds u2d[:new_ny*factor, :new_nx*factor].reshape(new_ny, factor, new_nx, factor).mean(axis(1,3)) v_ds v2d[:new_ny*factor, :new_nx*factor].reshape(new_ny, factor, new_nx, factor).mean(axis(1,3)) return u_ds.flatten(), v_ds.flatten(), new_nx, new_ny逻辑说明先 reshape 成二维再分块求平均最后展平。factor是降采样倍数5 表示 5x5 格子合成一个。新的nx/ny要同步更新到 header 里否则粒子位置会错乱。5. 避坑与常见问题字段错位、白屏与方向反了怎么查5.1 粒子不动或白屏现象图层加进去了但图上没有任何粒子控制台也没报错。 原因最常见的是data为空数组或者header里nx/ny和data长度不匹配。插件内部会做校验但不一定抛错可能直接静默不画。 解决在初始化前打印data.length和header.nx * header.ny两者必须相等。如果不等检查数据生成脚本是不是漏了最后一列或最后一行。5.2 粒子方向与风向相反现象粒子流动方向明显和实际风向相反比如东北风粒子往东北跑。 原因U/V 分量符号搞反了或者用了“风吹向”而不是“风来自”。气象数据通常给的是风来自的方向换算时负号不能丢。 解决拿一个已知风向的格点手动验算。比如正北风风从北吹来U 应为 0V 应为负。如果算出来 V 为正说明公式错了。5.3 粒子只覆盖地图一部分现象粒子集中在某个角落其余区域空白。 原因header里的lo1/la1/lo2/la2和实际数据范围不一致或者dx/dy写反了。插件按 header 计算每个格点的经纬度如果范围偏了粒子就画偏了。 解决把 header 的四个角点标到地图上看是否和数据实际覆盖区域吻合。常见错误是把lo1和lo2写反导致经度跨度变成负值。5.4 切换时间步时闪烁或错位现象调用setData切换数据后粒子突然跳到别的位置。 原因不同时间步的 header 不一致尤其是nx/ny或dx/dy变了。插件会重新初始化网格视觉上就是跳变。 解决统一所有时间步的 header只改data。如果数据源本身网格不同先重采样到同一网格再合并。5.5 移动端触摸交互冲突现象在手机上滑动地图时粒子动画卡住或地图拖不动。 原因leaflet-velocity的 canvas 层可能拦截了触摸事件或者粒子渲染占用了主线程。 解决给 canvas 层加pointer-events: none让事件穿透到地图。插件一般会处理但某些版本需要手动加 css.leaflet-velocity-layer canvas { pointer-events: none; }6. 进阶用自定义色带和悬停提示把风场讲清楚6.1 自定义色带匹配业务分级默认色带是彩虹色但气象业务里常用分级色带比如蓝色系表示小风、红色系表示大风。colorScale接受任意 CSS 颜色数组你可以按风速区间手动指定var customScale [ #f7fbff, #deebf7, #c6dbef, #9ecae1, #6baed6, #4292c6, #2171b5, #08519c, #08306b ];这个蓝色渐变适合表示风速从低到高。如果要做预警可以在maxVelocity附近加一个红色突变但注意颜色数组是均匀插值的突变需要多放几个相近色标。6.2 悬停提示显示风速风向displayValues: true开启后鼠标悬停会显示当前格点的风速和风向。displayOptions里可以改单位、角度约定和位置。angleConvention常用bearingCCW表示逆时针从北起算和气象风向一致。speedUnit可以写m/s、km/h或knots但插件不会自动换算只是显示你给的数据单位所以 json 里是什么单位就写什么。displayOptions: { velocityType: 风场, position: bottomright, emptyString: 超出范围, angleConvention: bearingCCW, showCardinal: true, speedUnit: m/s, directionString: 风向, speedString: 风速 }6.3 验证数据是否正确的笨办法我每次拿到新的风场 json都会先做一件事挑一个格点手动算出它的经纬度然后在地图上打个标记再对比粒子经过该点时的方向。如果方向一致、位置吻合基本就没大问题。这个习惯帮我省了很多来回调试的时间。另外把maxVelocity临时设成 1所有粒子都会变成同一颜色更容易看清流向。提示如果 json 里同时有多个高度层比如 10m 和 100m 风数组顺序要和业务预期一致。插件不会自动识别高度只是按数组顺序渲染切换时自己控制索引。从那以后我每次拿到新的风场数据都强制先跑一遍单格点验证再上大图。希望帮到你。本文还有配套的精品资源点击获取