尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Leaflet 风向粒子动效实战:JS 引擎与 JSON 风场数据解析

Leaflet 风向粒子动效实战:JS 引擎与 JSON 风场数据解析 简介这份资源面向需要在 Web 地图中呈现动态风场的开发者提供 Leaflet 风向粒子效果所需的插件与数据文件。包内共 3 个文件包含 1 个 js 脚本、1 个 css 样式和 1 个 json 数据压缩包约 231KB体积轻巧便于集成。其中 js 文件负责解析风速风向数据并驱动粒子动画css 用于定义粒子大小、颜色与透明度等视觉样式json 则以网格形式存储全球风向风速信息三者配合即可在 Leaflet 底图上叠加流动的风场图层。资源已获得 873 人学习适合具备一定 Leaflet 基础、希望快速实现气象可视化效果的前端或 GIS 开发者参考可省去自行寻找插件与样例数据的环节直接用于地图应用的风场展示与教学演示。1. 风向粒子动效为什么值得用 Leaflet 重做一遍在气象可视化大屏、环保监测平台或者农业物联网项目里风向风速的呈现方式往往决定了整个页面的专业感。静态箭头图能表达方向但看不出流速的连续变化风玫瑰图适合统计却不适合实时展示。真正让数据“活”起来的方案是粒子流场动画——成千上万个细小粒子沿着风场矢量漂移形成类似风迹的流动效果。Leaflet 作为轻量级地图库本身不提供粒子渲染能力需要借助 Canvas 图层和一套风场数据来驱动。标题里提到的“js 和 json 资源”指的正是两样东西一份负责粒子运动逻辑与 Canvas 绘制的 JavaScript 文件一份描述网格化风向风速的 JSON 数据文件。前者是引擎后者是燃料。这套组合适合有地图开发基础、需要在 Web 端做风场可视化的前端或 GIS 工程师也适合想从零搭一个气象图层的数据可视化从业者。接下来我会把这两类资源的格式、调用方式、参数调优和常见翻车点逐一拆开讲。2. 风场 JSON 的数据结构从网格定义到矢量分量2.1 为什么不用逐点经纬度列表最常见的错误做法是把每个气象站点的经纬度和风向风速写成一条记录前端拿到后逐点画箭头。这种结构在站点稀疏时能用一旦要做粒子流场就彻底失效——粒子需要在任意位置插值出风向风速而不是只在离散点上有值。正确的做法是规则网格把目标区域按等经纬度间隔切成若干行列每个网格节点存两个分量 u 和 v。u 表示东西方向风速分量v 表示南北方向风速分量正值分别代表向东和向北。粒子在任意位置的速度通过对周围四个网格节点做双线性插值得到这样粒子才能平滑地穿过整个区域。2.2 一份可用的风场 JSON 长什么样下面是一个最小可用的风场 JSON 结构覆盖经度 100 到 120、纬度 20 到 40 的区域网格间隔 1 度共 21 列 21 行。{ header: { la1: 40, lo1: 100, la2: 20, lo2: 120, dx: 1, dy: 1, nx: 21, ny: 21, refTime: 2025-01-01T00:00:00Z }, data: [ { u: 3.2, v: -1.1 }, { u: 2.8, v: -0.9 } ] }header 里 la1/lo1 是左上角纬度和经度la2/lo2 是右下角dx/dy 是网格间距nx/ny 是列数和行数。data 是一个扁平数组按行优先排列长度必须等于 nx 乘以 ny。每个元素包含 u 和 v 两个数值单位通常是米每秒。实际项目中 data 数组会有几百到几万个元素这里只示意前两个。2.3 生成 JSON 的 Python 脚本与三个校验点手工拼 JSON 不现实通常用 Python 从 NetCDF 或 GRIB 文件转换。下面这段脚本读取一个假设的二维数组并输出上述结构。import json import numpy as np # 假设 u_data 和 v_data 是 shape 为 (ny, nx) 的二维数组 ny, nx 21, 21 u_data np.random.uniform(-5, 5, (ny, nx)) v_data np.random.uniform(-5, 5, (ny, nx)) header { la1: 40, lo1: 100, la2: 20, lo2: 120, dx: 1, dy: 1, nx: nx, ny: ny, refTime: 2025-01-01T00:00:00Z } data [] for j in range(ny): for i in range(nx): data.append({ u: round(float(u_data[j, i]), 2), v: round(float(v_data[j, i]), 2) }) with open(wind_field.json, w, encodingutf-8) as f: json.dump({header: header, data: data}, f, ensure_asciiFalse) print(grid points:, len(data), expected:, nx * ny)逻辑说明双重循环按行优先顺序遍历保证 data 的顺序和 header 中 nx/ny 定义的网格一致。round 保留两位小数避免 JSON 体积过大。参数说明la1 必须大于 la2因为纬度从北向南递减dx 和 dy 的单位是度不是米refTime 用 ISO 8601 格式方便前端显示数据时间。三个校验点第一len(data) 必须等于 nx*ny否则粒子插值会越界第二u 和 v 不能出现 null 或 NaNJSON 不支持这些值遇到缺测要填 0 或做插补第三经度跨越 180 度时要特殊处理否则粒子会在日期变更线附近跳变。3. 粒子引擎 JS 的调用方式从初始化到逐帧渲染3.1 核心思路Canvas 覆盖层加双缓冲Leaflet 的粒子层不能直接画在 DOM 上否则每帧创建销毁元素会拖垮浏览器。常见做法是创建一个继承自 L.Layer 的自定义图层内部持有一个 Canvas 元素用 requestAnimationFrame 驱动逐帧绘制。粒子本身不存经纬度而是存屏幕坐标和年龄。每帧根据当前屏幕坐标反算经纬度再从风场 JSON 中插值出 u/v换算成像素位移更新粒子位置。粒子超出边界或年龄耗尽就重置到随机位置。为了做出拖尾效果每帧不是清空画布而是用半透明黑色矩形覆盖让旧轨迹逐渐淡出。3.2 初始化参数与调用代码下面是一个简化的粒子图层实现可以直接嵌入 Leaflet 项目。L.WindParticleLayer L.Layer.extend({ options: { particleCount: 2000, particleAge: 80, velocityScale: 0.01, fadeOpacity: 0.96, lineWidth: 1.2, color: rgba(255,255,255,0.9) }, initialize: function (windData, options) { L.setOptions(this, options); this._windData windData; this._particles []; }, onAdd: function (map) { this._map map; var canvas this._canvas L.DomUtil.create(canvas, leaflet-wind-layer); canvas.style.position absolute; canvas.style.pointerEvents none; map.getPanes().overlayPane.appendChild(canvas); this._ctx canvas.getContext(2d); this._resize(); map.on(moveend zoomend resize, this._resize, this); this._initParticles(); this._animate(); }, _resize: function () { var size this._map.getSize(); this._canvas.width size.x; this._canvas.height size.y; this._canvas.style.width size.x px; this._canvas.style.height size.y px; }, _initParticles: function () { var size this._map.getSize(); this._particles []; for (var i 0; i this.options.particleCount; i) { this._particles.push({ x: Math.random() * size.x, y: Math.random() * size.y, age: Math.floor(Math.random() * this.options.particleAge) }); } }, _animate: function () { var self this; function frame() { self._drawFrame(); self._rafId requestAnimationFrame(frame); } frame(); }, _drawFrame: function () { var ctx this._ctx; var size this._map.getSize(); ctx.fillStyle rgba(0,0,0, this.options.fadeOpacity ); ctx.fillRect(0, 0, size.x, size.y); ctx.lineWidth this.options.lineWidth; ctx.strokeStyle this.options.color; ctx.beginPath(); for (var i 0; i this._particles.length; i) { var p this._particles[i]; var geo this._map.containerPointToLatLng([p.x, p.y]); var uv this._interpolate(geo.lng, geo.lat); if (!uv) { this._resetParticle(p); continue; } var next this._map.latLngToContainerPoint([ geo.lat uv.v * this.options.velocityScale, geo.lng uv.u * this.options.velocityScale ]); ctx.moveTo(p.x, p.y); ctx.lineTo(next.x, next.y); p.x next.x; p.y next.y; p.age; if (p.age this.options.particleAge) this._resetParticle(p); } ctx.stroke(); }, _resetParticle: function (p) { var size this._map.getSize(); p.x Math.random() * size.x; p.y Math.random() * size.y; p.age 0; }, _interpolate: function (lng, lat) { var h this._windData.header; var fx (lng - h.lo1) / h.dx; var fy (h.la1 - lat) / h.dy; var i Math.floor(fx), j Math.floor(fy); if (i 0 || j 0 || i h.nx - 1 || j h.ny - 1) return null; var tx fx - i, ty fy - j; var idx function (r, c) { return r * h.nx c; }; var d this._windData.data; var u (1 - ty) * ((1 - tx) * d[idx(j, i)].u tx * d[idx(j, i 1)].u) ty * ((1 - tx) * d[idx(j 1, i)].u tx * d[idx(j 1, i 1)].u); var v (1 - ty) * ((1 - tx) * d[idx(j, i)].v tx * d[idx(j, i 1)].v) ty * ((1 - tx) * d[idx(j 1, i)].v tx * d[idx(j 1, i 1)].v); return { u: u, v: v }; }, onRemove: function (map) { cancelAnimationFrame(this._rafId); map.off(moveend zoomend resize, this._resize, this); L.DomUtil.remove(this._canvas); } });逻辑说明onAdd 里创建 Canvas 并挂到 overlayPane保证粒子层在地图平移缩放时跟随。_drawFrame 每帧用半透明黑覆盖实现拖尾然后遍历粒子把屏幕坐标转成经纬度插值出 u/v再换算回屏幕坐标画线段。_interpolate 做双线性插值越界返回 null 触发粒子重置。参数说明particleCount 控制粒子总数2000 在普通笔记本上能跑满 60 帧超过 5000 会明显掉帧particleAge 是粒子寿命值越大拖尾越长但粒子更新越慢velocityScale 是风速到像素的缩放系数需要根据地图缩放级别调整通常 0.005 到 0.02 之间fadeOpacity 越接近 1 拖尾越长0.9 以下会显得很干净但缺少流动感。3.3 把 JSON 和 JS 接起来初始化时先 fetch JSON再创建图层并加到地图上。fetch(wind_field.json) .then(function (res) { return res.json(); }) .then(function (windData) { var windLayer new L.WindParticleLayer(windData, { particleCount: 3000, velocityScale: 0.008, color: rgba(180,220,255,0.85) }); windLayer.addTo(map); });这段代码里 fetch 拿到 JSON 后直接传给图层构造函数。注意 JSON 的 header 字段名必须和 _interpolate 里读取的一致否则插值会返回 undefined。如果后端返回的是压缩过的数组而不是对象数组需要在传给图层前做一次转换把扁平数组还原成 {u, v} 对象。4. 避坑与排查粒子不动、拖尾异常、缩放错位4.1 粒子全部静止不动现象Canvas 上能看到粒子点但完全不移动。原因通常是 u 和 v 全为 0或者 velocityScale 太小导致位移不足一个像素。解决先检查 JSON 里 data 数组的 u/v 是否有非零值再逐步调大 velocityScale 到 0.05 观察。另一个可能是 _interpolate 里经纬度顺序写反导致始终返回 null粒子不断重置看起来像静止。4.2 拖尾变成一团糊现象粒子轨迹糊成一片看不出流动方向。原因是 fadeOpacity 设得太高旧帧没有及时淡出。解决把 fadeOpacity 降到 0.9 以下同时减小 particleAge让粒子更快重置。如果地图本身有深色底图可以把覆盖色改成半透明白色避免和底图混在一起。4.3 地图缩放后粒子偏移现象放大或缩小地图后粒子位置和实际风向对不上。原因是 Canvas 尺寸没有随地图容器更新或者 containerPointToLatLng 在缩放动画期间返回了中间状态的值。解决在 moveend 和 zoomend 事件里重新调用 _resize 并重置所有粒子不要在 zoom 事件里做插值计算。另外确保 Canvas 的 CSS 宽高和属性宽高一致否则会出现坐标缩放。4.4 JSON 加载成功但粒子不显示现象控制台没有报错fetch 也返回了 200但 Canvas 上什么都没有。原因可能是 Canvas 的 z-index 被底图盖住或者 pointerEvents 设置导致 Canvas 不可见。解决检查 overlayPane 里 Canvas 是否在 tilePane 之上必要时手动设置 z-index。另外确认 _animate 里的 requestAnimationFrame 确实在循环调用可以在 frame 函数里打一个计数器验证。4.5 移动端帧率骤降现象桌面端流畅手机浏览器上粒子卡顿。原因是 particleCount 过高移动端 GPU 填充率不足。解决根据 window.devicePixelRatio 和屏幕宽度动态调整粒子数移动端降到 800 到 1200。同时把 lineWidth 降到 1 以下减少每帧的绘制开销。5. 进阶技巧让风场粒子更贴近真实气象5.1 用颜色映射风速大小默认粒子是单色看不出风速强弱。可以在 _drawFrame 里根据插值出的 u/v 计算风速再映射到颜色。var speed Math.sqrt(uv.u * uv.u uv.v * uv.v); var ratio Math.min(speed / 15, 1); var r Math.floor(100 155 * ratio); var g Math.floor(180 - 80 * ratio); var b Math.floor(255 - 155 * ratio); ctx.strokeStyle rgba( r , g , b ,0.9);这段逻辑把风速 0 到 15 米每秒映射到蓝到红的渐变。参数说明15 是最大参考风速超过按 1 处理颜色分量根据项目配色调整不必照搬。注意每帧改 strokeStyle 会增加状态切换开销可以按风速分档每档一个颜色减少切换次数。5.2 按缩放级别动态调整粒子密度地图缩小时粒子显得稀疏放大时又太密。可以在 zoomend 里根据当前缩放级别重新计算 particleCount。map.on(zoomend, function () { var z map.getZoom(); var count Math.floor(800 (z - 3) * 400); windLayer.options.particleCount Math.max(500, Math.min(count, 5000)); windLayer._initParticles(); });逻辑说明缩放级别 3 时约 800 个粒子每增加一级多 400 个上限 5000。这样小比例尺下不会太卡大比例尺下细节足够。参数说明基数和增量根据实际地图范围和屏幕尺寸调整没有万能值。5.3 用时间序列 JSON 做多帧切换如果 JSON 里包含多个时刻的数据可以把 data 改成三维数组增加一个时间维度。前端用定时器每隔几秒切换一帧并更新 refTime 显示。这种做法适合展示风场演变过程但要注意 JSON 体积会成倍增长建议按需加载而不是一次性全量拉取。5.4 一个我常犯的错误早期做风场粒子时我总想把 particleCount 调到最大觉得粒子越多越好看。结果在一台老笔记本上直接卡成幻灯片浏览器风扇狂转。后来才明白粒子数量要和屏幕分辨率、帧率预算匹配2000 个粒子在 1080p 下已经足够形成连续流场再多就是浪费。另一个血泪经验是JSON 里的 u/v 单位一定要和 velocityScale 匹配如果数据是节而不是米每秒位移会差一倍以上粒子看起来像被冻住。每次换数据源先拿一个已知风速值手动算一遍像素位移确认量级对了再调其他参数。希望帮到你。本文还有配套的精品资源点击获取
返回列表