【Canvas】记录一次从0到1绘制风场空间分布图的过程
前言
📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步!
🍅 个人主页:南木元元
目录
背景
前置知识
风场数据
绘制风场
准备工作
生成二维网格
获取格点风矢位置
风力等级
计算风矢坐标位置
旋转角度
绘制格点风矢
结语
背景
项目里遇到个需求,要求绘制出风场的空间分布图,一开始的想法是:这有什么难的,直接用echarts不就可以了。但当我看完设计图后,不得不感叹一句,好家伙,这还真有点复杂。最终要实现的效果如下图所示:
由于自定义的程度比较高,echarts肯定是不行的,思来想去,于是决定用canvas来从0到1自己实现,同时也可以顺带把canvas的知识巩固一下(温馨提示:全文可能有点长)。
前置知识
首先解释一下什么是风场空间分布图。
风场空间分布图:一种用于展示区域内风速和风向随空间位置变化的图表,这种图表通常以箭头或风矢的形式来表示风的方向和强度。这使我们可以直观地看到风速、风向的变化规律,它常常在气象学、风能工程等领域中被广泛使用。
本文采用风矢的形式来进行风场的可视化。在气象学中,风矢是用于表示风向和风速的符号图标。风矢由2部分组成,分别为风向杆与风羽。
- 风向杆:表示风的方向
- 风羽:分别用长划线和短划线或者与风三角组合的方式表示风速的大小。
了解了上面的概念后,我们下面就将使用Canvas来展示如何绘制风场的空间分布图。
风场数据
数据来源于用户自建的气象观测产品库,原始数据一般是netcdf或grib2的格式,需要后端将其解析成json格式的数据,解析后的数据格式大致如下:
{"yaxis": [10, 20, ...],"xaxis": [[39.4, 107.16], [37.286667, 107.72223], ...]"elementDataList": [{"name": "windS","subData": [{"level": "10","data": [8.9,10.3,...]},{"level": "20","data": [4.6,8.1,...]},...},{"name": "windD","subData": [{"level": "10","data": [59.8,65.0,...]},{"level": "20","data": [60.1,58.5,...]},...]}]
}
纵轴yaxis代表不同的高度层,横轴xaxis代表不同的经纬度坐标,要素列表elementDataList中目前只有一个风场要素(还有其它的气象要素如温度、降水量等,这里不展开),由于风场是矢量要素,同时具有大小和方向,所以这里将风的数据拆分成了windS风速列表和windD风向列表,列表中的值分别为每个高度层所对应的数据。
绘制风场
准备工作
定义一个绘制的类,做一些初始化的操作:属性设置,获取canvas的2d渲染上下文。
class drawWind {constructor(data){ //网格属性this.property = {OFFSET_X: 42, //x轴间隔OFFSET_Y: 20, //y轴间隔};//获取2d渲染上下文this.canvas2d = document.getElementById('canvas');this.ctx2d = this.canvas2d.getContext("2d");//后端返回数据this.data = data;this.xaxis = data.xaxis;this.yaxis = data.yaxis;//处理后的数据this.wind10S = [];this.wind10D = [];}//初始化数据init() {//处理一下返回的风速和风向数据,这里不详细展开,最终处理成网格点数据即可this.wind10S = this.handleData("wind10S");this.wind10D = this.handleData("wind10D");}}
}
还需要处理一下后端返回的数据,变成二维网格点数据,如下:
- 风速数据
- 风向数据
最终需要的数据就是网格点数据,即每个网格点都对应其风速和风向数据。
生成二维网格
生成风场需要构造二维网格,canvas绘制二维网格的思路很简单,先使用strokeRect设置一个矩形的边框,然后分别遍历横坐标和纵坐标列表,进行虚线的绘制。
draw2dMesh() {//生成矩形边框this.ctx2d.strokeRect(0, 0, this.canvas2d.width, this.canvas2d.height);//设置虚线样式this.ctx2d.lineWidth = 0.6;this.ctx2d.strokeStyle = "rgb(192, 192, 192)";this.ctx2d.beginPath();//遍历绘制纵向虚线for (let i = 1; i <= this.xaxis.length; i++) {this.ctx2d.setLineDash([5, 3]);this.ctx2d.moveTo(this.property.OFFSET_X * i, 0);this.ctx2d.lineTo(this.property.OFFSET_X * i, this.canvas2d.height);}//遍历绘制横向虚线for (let i = 1; i <= this.yaxis.length; i++) {this.ctx2d.setLineDash([5, 3]);this.ctx2d.moveTo(0, this.property.OFFSET_Y * i);this.ctx2d.lineTo(this.canvas2d.width, this.property.OFFSET_Y * i);}this.ctx2d.stroke();
}
绘制的网格如下:
获取格点风矢位置
每个网格点上的风矢形状是下面这样的。
所以在正式绘制前,我们还需要先计算每个风矢中的风杆和风羽数,得到每个点的位置。
风力等级
风力等级的计算公式:
可以参考这两篇文章:风力的级别换算和风力、等级、风速对照表和计算公式。
这里我们采用的是32个等级,可以预先定义好每个等级对应的风杆、长短划线以及风三角的数量。
this.Level = {"TRIANGLE": 20,"LONG": 4,"SHORT": 2,
},
this.Count = {"TRIANGLE": 10,"LONG": 2,"SHORT": 1,
},
//32个风力等级,每个数组中的四个值依次代表风杆数量、短划线数量、长划线数量、风三角数量
this.windLevel = [[0, 1, 0, 0],[1, 1, 0, 0],[1, 0, 1, 0],[1, 1, 1, 0],[1, 0, 2, 0],[1, 1, 2, 0],[1, 0, 3, 0],[1, 1, 3, 0],[1, 0, 4, 0],[1, 1, 4, 0],[1, 0, 0, 1],[1, 1, 0, 1],[1, 0, 1, 1],[1, 1, 1, 1],[1, 0, 2, 1],[1, 1, 2, 1],[1, 0, 3, 1],[1, 1, 3, 1],[1, 0, 4, 1],[1, 1, 4, 1],[1, 0, 0, 2],[1, 1, 0, 2],[1, 0, 1, 2],[1, 1, 1, 2],[1, 0, 2, 2],[1, 1, 2, 2],[1, 0, 3, 2],[1, 1, 3, 2],[1, 0, 4, 2],[1, 1, 4, 2],
],
//风矢属性:风杆长,长划线长,短划线长,划线间隔,风三角边长
this.featherProperty = {poleLength: 10,longLine: 10,shortLine: 5,lineSpace: 1,triangle: 2,
};
定义计算风力等级的方法。
// 根据风速计算风力等级,公式:v = 0.836 * b^(3/2) v:风速 b:风级
calWindLevel(speed) {let triangle = Math.floor(speed / this.Level.TRIANGLE);let long = Math.floor((speed - this.Level.TRIANGLE * triangle) / this.Level.LONG);let short = Math.floor((speed - this.Level.TRIANGLE * triangle - this.Level.LONG * long) / this.Level.SHORT);let idx = triangle * this.Count.TRIANGLE + long * this.Count.LONG + short * this.Count.SHORT;if (idx > 30) {idx = 30;}return idx;
}
计算风矢坐标位置
接下来需要计算得到每个网格点上的风矢中每个点的位置,这部分是整个流程中最为复杂的。
来说说我的思路:定义一个数组,用于存放当前格点的风矢位置,然后获取计算得到的风杆、长短划线等数量,从风杆顶部开始,依次放入风杆、风三角、长划线、短划线的位置。
//用于存放所有网格点风矢的位置
let position = [];
// 计算坐标位置:Num为当前网格点对应的风力等级,包含各种数量
getPointPosition(Num) {//用于存放当前格点风矢的位置let position = []; let pole = Num[0]; //风杆数量let short = Num[1]; //短划线数量let long = Num[2]; //长划线数量let triangle = Num[3]; //风三角数量//当前顶点纵坐标位置从风杆顶部开始,这里为负是由于canvas坐标系y轴向下为正let yOffset = -this.featherProperty.poleLength;if (pole == 0) { //风杆数为0position.push(0, 0,this.featherProperty.shortLine, 0,this.featherProperty.shortLine, 0 //为了和风三角的三个一组一致,多加了一个点);//把当前格点的风羽位置放入数组position.push(position);return;}//放入风杆位置position.push(0, 0,0, -this.featherProperty.poleLength, //向上为负0, -this.featherProperty.poleLength);//判断风三角是否为0,不为0向其中添加顶点if (triangle != 0) {for (let i = 0; i < triangle; ++i) {position.push(0, yOffset,0, yOffset + this.featherProperty.triangle, //triangle为三角形边长this.featherProperty.longLine, yOffset + (this.featherProperty.triangle / 2));//每画完一个三角形,当前y坐标就要下移,由于canvas向下为正,所以即为加上三角形边长再加划线和三角形的间距yOffset = yOffset + this.featherProperty.triangle + this.featherProperty.lineSpace;}}//判断长划线是否为0,不为0向其中添加顶点if (long != 0) {for (let i = 0; i < long; ++i) {position.push(0, yOffset,this.featherProperty.longLine, yOffset,this.featherProperty.longLine, yOffset);yOffset = yOffset + this.featherProperty.lineSpace;}}//判断短划线是否为0,不为0向其中添加顶点if (short != 0) {for (let i = 0; i < short; ++i) {position.push(0, yOffset,this.featherProperty.shortLine, yOffset,this.featherProperty.shortLine, yOffset);yOffset = yOffset + this.featherProperty.lineSpace;}}//把当前格点的风羽位置放入数组position.push(position);
}
得到的风矢各个点的坐标数组大致如下:
旋转角度
风向决定了每个风矢在格点的旋转角度,由于旋转的时候以每个格点坐标为中心,所以记录一下每个格点的坐标位置。
// 获取旋转角度
getRotateData() {// 保存旋转中心点,即网格点坐标let center = [];// 保存风向let angle = [];for (let y = 0; y < this.yaxis.length; y++) {for (let x = 0; x < this.xaxis.length; x++) {// 获取风向let angle_point = this.angle[x + y * this.xaxis.length];// 计算网格点坐标let center = [(x + 1) * this.offsetX, (y + 1) * this.offsetY];center.push(center); angle.push([angle_point]);}}return {angle: angle,center: center,};
}
绘制格点风矢
做完上述操作后,终于可以开始绘制啦。绘制的思路:由于之前在计算位置的时候就统一3个坐标为一组(即画线只需两个坐标点,但我们也多加了一个重复的点,为了和画三角形统一),所以现在只需遍历顶点数组,来绘制每个格点的风矢就可以了。
// 绘制
drawFeather(data, color, size) {// 设置样式this.ctx.lineWidth = size; this.ctx.strokeStyle = color; this.ctx.fillStyle = color;// 让虚线变成实线条this.ctx.setLineDash([]);let position = data.position;let center = data.center;let angle = data.angle;// 遍历顶点数组,绘制每个格点的风矢for(let i = 0; i < center.length; i++) {for(let j = 0; j < position[i].length; j += 6) {// 保存画布 (canvas) 的所有状态this.ctx.save(); // 移动canvas原点到此处,使得当前格点为坐标为原点(0,0)this.ctx.translate(center[i][0],center[i][1]); this.ctx.rotate(angle[i][0] * Math.PI/180);this.ctx.beginPath();// 之前处理后的数据都是三个为一组(包括线条),直接画线即可this.ctx.moveTo(position[i][j], position[i][j+1]);this.ctx.lineTo(position[i][j+2], position[i][j+3]);this.ctx.lineTo(position[i][j+4], position[i][j+5]);this.ctx.fill(); this.ctx.stroke(); // 恢复 canvas 状态this.ctx.restore(); }}
}
注意:在绘制每个格点风矢的时候,都需要save保存一下将当前canvas的状态入栈,绘制完后restore弹出恢复状态,为的是绘制下一个格点的风矢时都可以重新从canvas的坐标原点(0,0)开始平移到网格中心点,然后进行旋转操作。
最终的效果:
现在主要的部分我们都已经完成了,剩下的其实就是绘制横坐标和纵坐标,由于这部分比较简单,其实就是利用canvas绘制文字,这里就不再详细展开了。
结语
本文主要记录了一次自己使用canvas从0到1绘制风场空间分布图的经历,整个过程还是蛮复杂的,不过也刚好巩固了一下自己的canvas知识,将其运用到了实践中,同时也发现自己对知识的理解其实还存在许多的不足,需要继续努力!
🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~
相关文章:

【Canvas】记录一次从0到1绘制风场空间分布图的过程
前言 📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步! 🍅 个人主页:南木元元 目录 背景 前置知识 风场数据 绘制风场 准备工作 生成二维网格 获取…...

如何用gpt改写文章 (1) 神码ai
大家好,今天来聊聊如何用gpt改写文章 (1),希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧: 如何用GPT改写文章 一、引言 随着人工智能技术的飞速发展,自然语言处理领域取得了重大突…...
IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Spring依赖注入数值问题
第一章 Spring依赖注入数值问题 1.1 字面量数值 数据类型:基本数据类型及包装类、String语法:value属性或value标签 1.2 CDATA区 语法:<![CDATA[]]>作用:在xml中定义特殊字符时,使用CDATA区 1.3 外部已声明…...

egen3 rowwise().maxCoeff()的使用
1、安装eigen3 2、引用头文件 3、代码测试 MatrixXf aaa(2, 4);aaa << 1, 2, 3, 4, 5, 6, 7, 8; Vector2f diff(10, 20);aaa.colwise() diff;std::cout << "new_aaa : " << aaa << endl; 全部代码: int main() {MatrixXf …...
关于Pytorch和Numpy中的稀疏矩阵sparse的知识点
Pytorch和Numpy中的稀疏矩阵sparse 0 稀疏矩阵类别0.1 coo_matrix0.2 dok_matrix0.3 csr_matrix0.4 csc_matrix0.5 bsr_matrix0.6 bsc_matrix0.7 lil_matrix0.8 dia_matrix 1 pytorch中的稀疏矩阵1.1 to_sparse()1.2 to_sparse_csr()1.3 sparse_coo_tensor()1.4 sparse_csr_ten…...

2024年AI云计算专题研究报告:智算带来的变化
今天分享的人工智能系列深度研究报告:《2024年AI云计算专题研究报告:智算带来的变化》。 (报告出品方:华泰证券) 报告共计:32页 Al 云计算 2024:关注智算带来的新变化 通过对海内外主要云厂商及其产业链…...

孩子还是有一颗网安梦——Bandit通关教程:Level 5 → Level 6
🕵️♂️ 专栏《解密游戏-Bandit》 🌐 游戏官网: Bandit游戏 🎮 游戏简介: Bandit游戏专为网络安全初学者设计,通过一系列级别挑战玩家,从Level0开始,逐步学习基础命令行和安全概念…...

vue2-elementUI部分组件样式修改
el-radio样式: /deep/ .el-radio__input .el-radio__inner {width: 20px;height: 20px;position: relative;cursor: pointer;-webkit-appearance: none;-moz-appearance: none;appearance: none;border: 1px solid #999;border-radius: 0;outline: none;transition…...

fijkplayer flutter 直播流播放
fijkplayer flutter 直播流播放 fijkplayer 是 ijkplayer 的 Flutter 封装, 是一款支持 android 和 iOS 的 Flutter 媒体播放器插件, 由 ijkplayer 底层驱动。 通过纹理(Texture)接入播放器视频渲染到 Flutter 中。 前言 目前使用…...
Javascript的基本语法(规范)
JS的基本语法规范 1.JS中严格区分大小写 2.JS中每一个指令被称为一个语句,每一个语句都应该以分号结尾 - 在JS中有自动的添加分号的机制,如果不写分号浏览器会自动为你添加 - 有些情况下,浏览器可能会给你加错了(几率低&#…...

vue chrome debugger 无效
昨天晚上debbger可以正常运行的,但是早上起来突然间所有的debugger都不会被命中,重装了vscode,也清了浏览器缓存,可是这个bitch还是不行!整整折腾了一早上,就是无法解决,没办法只能找找资料 ,搜…...
JRT实现Cache的驱动
我只给PostGreSql和iris写了连接驱动,永国的库是Cache,他就自己写了个驱动,驱动其实就是把数据库差异接口抽取了出来,然后只要配对应数据库驱动就能连响应的数据库了。 package JRT.Dal.Base;import JRT.Core.MultiPlatform.JRTC…...
ESP32网络开发实例-Web串口(WebSerial)
Web串口(WebSerial) 文章目录 Web串口(WebSerial)1、软件准备2、硬件准备3、代码实现4、接收数据在本文中,我们将介绍如何实现的基于 Web 的 ESP32 串行监视器。 1、软件准备 Arduino IDE在前面的文章中,如何搭建ESP32的Arduino IDE开环境,主参考: ESP32-Arduino-开发实…...

P2 Qt Creator创建第一个Qt程序
前言 🎬 个人主页:ChenPi 🐻推荐专栏1: 《C_ChenPi的博客-CSDN博客》✨✨✨ 🔥 推荐专栏2: 《LLinux C应用编程(概念类)_ChenPi的博客-CSDN博客》✨✨✨ 🌺本篇简介 :这一章我们学…...
加班、效率和价值
效率不等于单位时间单位人干的活,而是等于单位时间单位人产出的价值,衡量工作量的难度很大,而如何选择工作重点,挖掘工作价值难度更大。 加班的不可持续在于两点,第一点是对身体和精神的损害,降低内在动力…...

【QT 5 调试软件+(Linux下验证>>>>串口相关初试串口)+Windows下qt代码在Linux下运行+参考win下历程+基础样例】
【QT 5 调试软件Linux下验证>>>>串口相关初试串口参考win下历程基础样例】 1、前言2、实验环境3、先行了解4、自我总结-win下工程切到Linux下1、平台无关的代码:2、依赖的库:3、文件路径和换行符:4、编译器差异:5、构…...

地址栏不安全提示
在使用浏览器时访问网站的时候,我们可能会遇到地址栏提示不安全的情况。这种情况通常都是是由于未安装有效SSL证书或者网站SSL证书过期等原因导致的。本文将介绍如何处理地址栏提示不安全的问题,以确保我们的上网安全。 1,缺少SSL证书&#x…...
glib编译与实战
文章目录 下载编译修正实战参考 下载 https://ftp.acc.umu.se/pub/GNOME/sources/glib/ 编译 cd glib mkdir buildmeson --prefix/home/glib build ninja -C build ninja -C build install修正 meson.build:1:0: ERROR: Meson version is 0.53.2 but project requires >…...
PHP基础(4)
目录 一、PHP 创建用户定义函数 二、数组 数组的排序函数 一、PHP 创建用户定义函数 用户定义的函数声明以单词 "function" 开头: PHP自定义函数是指用户自行定义的函数,以满足自己的编程需求。在PHP中,可以通过以下语法来定义一…...

软件安全设计
目录 一,STRIDE 威胁建模 1,STRIDE 2,总体流程(关键步骤) 3,数据流图的4类元素 二,安全设计原则 三,安全属性 一,STRIDE 威胁建模 1,STRIDE STRIDE 是…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
Vite中定义@软链接
在webpack中可以直接通过符号表示src路径,但是vite中默认不可以。 如何实现: vite中提供了resolve.alias:通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...