HTML5的WebGL实现的3D和2D拓扑树

2024-08-30 11:48

本文主要是介绍HTML5的WebGL实现的3D和2D拓扑树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

HT for Web中2D和3D应用都支持树状结构数据的展示,展现效果各异,2D上的树状结构在展现层级关系明显,但是如果数据量大的话,看起来就没那么直观,找到指定的节点比较困难,而3D上的树状结构在展现上配合HT for Web的弹力布局组件会显得比较直观,一眼望去可以把整个树状结构数据看个大概,但是在弹力布局的作用下,其层次结构看得就不是那么清晰了。所以这时候结构清晰的3D树的需求就来了,那么这个3D树具体长成啥样呢,我们来一起目睹下~

 

要实现这样的效果,该从何下手呢?接下来我们就将这个问题拆解成若干个小问题来解决。

1. 创建一个树状结构

有了解过HT for Web的朋友,对树状结构数据的创建应该都不陌生,在这里我就不做深入的探讨了。树状结构数据的创建很简单,在这里为了让代码更简洁,我封装了三个方法来创建树状结构数据,具体代码如下:

/*** 创建连线* @param {ht.DataModel} dataModel - 数据容器* @param {ht.Node} source - 起点* @param {ht.Node} target - 终点*/
function createEdge(dataModel, source, target) {// 创建连线,链接父亲节点及孩子节点var edge = new ht.Edge();edge.setSource(source);edge.setTarget(target);dataModel.add(edge);
}/*** 创建节点对象* @param {ht.DataModel} dataModel - 数据容器* @param {ht.Node} [parent] - 父亲节点* @returns {ht.Node} 节点对象*/
function createNode(dataModel, parent) {var node = new ht.Node();if (parent) {// 设置父亲节点node.setParent(parent);createEdge(dataModel, parent, node);}// 添加到数据容器中dataModel.add(node);return node;
}/*** 创建结构树* @param {ht.DataModel} dataModel - 数据容器* @param {ht.Node} parent - 父亲节点* @param {Number} level - 深度* @param {Array} count - 每层节点个数* @param {function(ht.Node, Number, Number)} callback - 回调函数(节点对象,节点对应的层级,节点在层级中的编号)*/
function createTreeNodes(dataModel, parent, level, count, callback) {level--;var num = (typeof count === 'number' ? count : count[level]);while (num--) {var node = createNode(dataModel, parent);// 调用回调函数,用户可以在回调里面设置节点相关属性callback(node, level, num);if (level === 0) continue;// 递归调用创建孩子节点createTreeNodes(dataModel, node, level, count, callback);}
}

嘿嘿,代码写得可能有些复杂了,简单的做法就是嵌套几个for循环来创建树状结构数据,在这里我就不多说了,接下来我们来探究第二个问题。

2. 在2D拓扑下模拟3D树状结构每层的半径计算

在3D下的树状结构体最大的问题就在于,每个节点的层次及每层节点围绕其父亲节点的半径计算。现在树状结构数据已经有了,那么接下来就该开始计算半径了,我们从两层树状结构开始推算:

 

我现在先创建了两层的树状结构,所有的子节点是一字排开,并没有环绕其父亲节点,那么我们该如何去确定这些孩子节点的位置呢?

首先我们得知道,每个末端节点都有一圈属于自己的领域,不然节点与节点之间将会存在重叠的情况,所以在这里,我们假定末端节点的领域半径为25,那么两个相邻节点之间的最短距离将是两倍的节点领域半径,也就是50,而这些末端节点将均匀地围绕在其父亲节点四周,那么相邻两个节点的张角就可以确认出来,有了张角,有了两点间的距离,那么节点绕其父亲节点的最短半径也就能计算出来了,假设张角为a,两点间最小距离为b,那么最小半径r的计算公式为:

r = b / 2 / sin(a / 2); 

那么接下来我么就来布局下这个树,代码是这样写的:

/*** 布局树* @param {ht.Node} root - 根节点* @param {Number} [minR] - 末端节点的最小半径*/
function layout(root, minR) {// 设置默认半径minR = (minR == null ? 25 : minR);// 获取到所有的孩子节点对象数组var children = root.getChildren().toArray();// 获取孩子节点个数var len = children.length;// 计算张角var degree = Math.PI * 2 / len;// 根据三角函数计算绕父亲节点的半径var sin = Math.sin(degree / 2),r = minR / sin;// 获取父亲节点的位置坐标var rootPosition = root.p();children.forEach(function(child, index) {// 根据三角函数计算每个节点相对于父亲节点的偏移量var s = Math.sin(degree * index),c = Math.cos(degree * index),x = s * r,y = c * r;// 设置孩子节点的位置坐标child.p(x + rootPosition.x, y + rootPosition.y);});
}

在代码中,你会发现我将末端半径默认设置为25了,如此,我们通过调用layout()方法就可以对结构树进行布局了,其布局效果如下:

从效果图可以看得出,末端节点的默认半径并不是很理想,布局出来的效果连线都快看不到了,因此我们可以增加末端节点的默认半径来解决布局太密的问题,如将默认半径设置成40的效果图如下:

现在两层的树状分布解决了,那么我们来看看三层的树状分布该如何处理。

将第二层和第三层看成一个整体,那么其实三层的树状结构跟两层是一样的,不同的是在处理第二层节点时,应该将其看做一个两层的树状结构来处理,那么像这种规律的处理用递归最好不过了,因此我们将代码稍微该着下,在看看效果如何:

不行,节点都重叠在一起了,看来简单的递归是不行的,那么具体的问题出在哪里呢?

仔细分析了下,发现父亲节点的领域半径是由其孩子节点的领域半径决定的,因此在布局时需要知道自身节点的领域半径,而且节点的位置取决于父亲节点的领域半径及位置信息,这样一来就无法边计算半径边布局节点位置了。

那么现在只能将半径的计算和布局分开来,做两步操作了,我们先来分析下节点半径的计算:

首先需要明确最关键的条件,父亲节点的半径取决于其孩子节点的半径,这个条件告诉我们,只能从下往上计算节点半径,因此我们设计的递归函数必须是先递归后计算,废话不多说,我们来看下具体的代码实现:

/*** 就按节点领域半径* @param {ht.Node} root - 根节点对象* @param {Number} minR - 最小半径*/
function countRadius(root, minR) {minR = (minR == null ? 25 : minR);// 若果是末端节点,则设置其半径为最小半径if (!root.hasChildren()) {root.a('radius', minR);return;}// 遍历孩子节点递归计算半径var children = root.getChildren();children.each(function(child) {countRadius(child, minR);});var child0 = root.getChildAt(0);// 获取孩子节点半径var radius = child0.a('radius');// 计算子节点的1/2张角var degree = Math.PI / children.size();// 计算父亲节点的半径var pRadius = radius / Math.sin(degree);// 设置父亲节点的半径及其孩子节点的布局张角root.a('radius', pRadius);root.a('degree', degree * 2);
}

OK,半径的计算解决了,那么接下来就该解决布局问题了,布局树状结构数据需要明确:孩子节点的坐标位置取决于其父亲节点的坐标位置,因此布局的递归方式和计算半径的递归方式不同,我们需要先布局父亲节点再递归布局孩子节点,具体看看代码吧:

/*** 布局树* @param {ht.Node} root - 根节点*/
function layout(root) {// 获取到所有的孩子节点对象数组var children = root.getChildren().toArray();// 获取孩子节点个数var len = children.length;// 计算张角var degree = root.a('degree');// 根据三角函数计算绕父亲节点的半径var r = root.a('radius');// 获取父亲节点的位置坐标var rootPosition = root.p();children.forEach(function(child, index) {// 根据三角函数计算每个节点相对于父亲节点的偏移量var s = Math.sin(degree * index),c = Math.cos(degree * index),x = s * r,y = c * r;// 设置孩子节点的位置坐标child.p(x + rootPosition.x, y + rootPosition.y);// 递归调用布局孩子节点layout(child);});
}

代码写完了,接下来就是见证奇迹的时刻了,我们来看看效果图吧:

不对呀,代码应该是没问题的呀,为什么显示出来的效果还是会重叠呢?不过仔细观察我们可以发现相比上个版本的布局会好很多,至少这次只是末端节点重叠了,那么问题出在哪里呢?

不知道大家有没有发现,排除节点自身的大小,倒数第二层节点与节点之间的领域是相切的,那么也就是说节点的半径不仅和其孩子节点的半径有关,还与其孙子节点的半径有关,那我们把计算节点半径的方法改造下,将孙子节点的半径也考虑进去再看看效果如何,改造后的代码如下:

/*** 就按节点领域半径* @param {ht.Node} root - 根节点对象* @param {Number} minR - 最小半径*/
function countRadius(root, minR) {……var child0 = root.getChildAt(0);// 获取孩子节点半径var radius = child0.a('radius');var child00 = child0.getChildAt(0);// 半径加上孙子节点半径,避免节点重叠if (child00) radius += child00.a('radius');……
}

下面就来看看效果吧~

哈哈,看来我们分析对了,果然就不再重叠了,那我们来看看再多一层节点会是怎么样的壮观场景呢?

哦,NO!这不是我想看到的效果,又重叠了,好讨厌。

不要着急,我们再来仔细分析分析下,在前面,我们提到过一个名词——领域半径,什么是领域半径呢?很简单,就是可以容纳下自身及其所有孩子节点的最小半径,那么问题就来了,末端节点的领域半径为我们指定的最小半径,那么倒数第二层的领域半径是多少呢?并不是我们前面计算出来的半径,而应该加上末端节点自身的领域半径,因为它们之间存在着包含关系,子节点的领域必须包含于其父亲节点的领域中,那我们在看看上图,是不是感觉末端节点的领域被侵占了。那么我们前面计算出来的半径代表着什么呢?前面计算出来的半径其实代表着孩子节点的布局半径,在布局的时候是通过该半径来布局的。

OK,那我们来总结下,节点的领域半径是其下每层节点的布局半径之和,而布局半径需要根据其孩子节点个数及其领域半径共同决定。

好了,我们现在知道问题的所在了,那么我们的代码该如何去实现呢?接着往下看:

/*** 就按节点领域半径及布局半径* @param {ht.Node} root - 根节点对象* @param {Number} minR - 最小半径*/
function countRadius(root, minR) {minR = (minR == null ? 25 : minR);// 若果是末端节点,则设置其布局半径及领域半径为最小半径if (!root.hasChildren()) {root.a('radius', minR);root.a('totalRadius', minR);return;}// 遍历孩子节点递归计算半径var children = root.getChildren();children.each(function(child) {countRadius(child, minR);});var child0 = root.getChildAt(0);// 获取孩子节点半径var radius = child0.a('radius'),totalRadius = child0.a('totalRadius');// 计算子节点的1/2张角var degree = Math.PI / children.size();// 计算父亲节点的布局半径var pRadius = totalRadius / Math.sin(degree);// 缓存父亲节点的布局半径root.a('radius', pRadius);// 缓存父亲节点的领域半径root.a('totalRadius', pRadius + totalRadius);// 缓存其孩子节点的布局张角root.a('degree', degree * 2);
}

在代码中我们将节点的领域半径缓存起来,从下往上一层一层地叠加上去。接下来我们一起验证其正确性:

搞定,就是这样子了,2D拓扑上面的布局搞定了,那么接下来该出动3D拓扑啦~

 

3. 加入z轴坐标,呈现3D下的树状结构

3D拓扑上面布局无非就是多加了一个坐标系,而且这个坐标系只是控制节点的高度而已,并不会影响到节点之间的重叠,所以接下来我们来改造下我们的程序,让其能够在3D上正常布局。

也不需要太大的改造,我们只需要修改下布局器并且将2D拓扑组件改成3D拓扑组件就可以了。

/*** 布局树* @param {ht.Node} root - 根节点*/
function layout(root) {// 获取到所有的孩子节点对象数组var children = root.getChildren().toArray();// 获取孩子节点个数var len = children.length;// 计算张角var degree = root.a('degree');// 根据三角函数计算绕父亲节点的半径var r = root.a('radius');// 获取父亲节点的位置坐标var rootPosition = root.p3();children.forEach(function(child, index) {// 根据三角函数计算每个节点相对于父亲节点的偏移量var s = Math.sin(degree * index),c = Math.cos(degree * index),x = s * r,z = c * r;// 设置孩子节点的位置坐标child.p3(x + rootPosition[0], rootPosition[1] - 100, z + rootPosition[2]);// 递归调用布局孩子节点layout(child);});
}

上面是改造成3D布局后的布局器代码,你会发现和2D的布局器代码就差一个坐标系的的计算,其他的都一样,看下在3D上布局的效果:

恩,有模有样的了,在文章的开头,我们可以看到每一层的节点都有不同的颜色及大小,这些都是比较简单,在这里我就不做深入的讲解,具体的代码实现如下:

var level = 4,size = (level + 1) * 20;var root = createNode(dataModel);
root.setName('root');
root.p(100, 100);root.s('shape3d', 'sphere');
root.s('shape3d.color', randomColor());
root.s3(size, size, size);var colors = {},sizes = {};
createTreeNodes(dataModel, root, level - 1, 5, function(data, level, num) {if (!colors[level]) {colors[level] = randomColor();sizes[level] = (level + 1) * 20;}size = sizes[level];data.setName('item-' + level + '-' + num);// 设置节点形状为球形data.s('shape3d', 'sphere');data.s('shape3d.color', colors[level]);data.s3(size, size, size);
});

在这里引入了一个随机生成颜色值的方法,对每一层随机生成一种颜色,并将节点的形状改成了球形,让页面看起来美观些(其实很丑)。

提个外话,节点上可以贴上图片,还可以设置文字的朝向,可以根据用户的视角动态调整位置,等等一系列的拓展,这些大家都可以去尝试,相信都可以做出一个很漂亮的3D树出来。

到此,整个Demo的制作就结束了,今天的篇幅有些长,感谢大家的耐心阅读,在设计上或则是表达上有什么建议或意见欢迎大家提出,点击这里可以访问HT for Web官网上的手册

 

这篇关于HTML5的WebGL实现的3D和2D拓扑树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1120736

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo