基于 HTML5 Canvas 的 3D 热力云图效果
<h2><strong>前言</strong></h2><p><span data-mce-=""> 数据蕴藏价值,但数据的价值需要用<span data-mce-=""> <span data-mce-="">IT 技术去发现、探索,可视化可以帮助人更好的去分析数据,信息的质量很大程度上依赖于其呈现方式。在数据分析上,热力图无疑是一种很好的方式。在很多行业中都有着广泛的应用。</span></span></span></p>
<p><span data-mce-=""> <span data-mce-="">最近刚好项目中需要用到 <span data-mce-="">3D <span data-mce-="">热力图的效果展示。网上搜了相关资料,发现大多<span data-mce-="">数是 <span data-mce-="">2D <span data-mce-="">效果或者伪 <span data-mce-="">3D <span data-mce-="">的,而 3D <span data-mce-="">粒子效果对于性能上的体验不是很好,于是取巧写<span data-mce-="">了个 <span data-mce-="">3D <span data-mce-="">热力图的效果 。</span></span></span></span></span></span></span></span></span></span></span></span></span></p>
<p> <span data-mce-="">Demo : http://www.hightopo.com/demo/heatMap3D/</span></p>
<p><span data-mce-=""> 部分效果图:</span></p>
<p> <img src="https://img2018.cnblogs.com/common/1496396/202001/1496396-20200103115548049-1948363290.gif"></p>
<h2><strong>应用场景</strong></h2>
<p><strong><img src="https://img2018.cnblogs.com/common/1496396/202001/1496396-20200103142832840-996554349.png"></strong></p>
<p><span data-mce-=""> 大楼内的人员分布热力图。我们可以通过观察到一个区域的颜色深浅来判断该区域内实时的人员流动情况,知道哪个区域人多,哪个区域人少。该场景可适用于大楼内的警务监控,在发生突发事件时科学高效地制定分流疏导策略提供有力的帮助和支持,减少损失。亦可用于火险预警,监控区域实时温度。</span></p>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200228103813385-493798460.png"></p>
<p><span data-mce-=""> 室内设备温度热力图。传统的数据中心汇报方式枯燥单调、真实感不强,互动性差等,借助于<span data-mce-=""><span data-mce-=""> 3D 热力图的可视化呈现方式,机房运维管理人员可大大提高工作效率及降低工作失误的可能性。</span></span></span></p>
<h2><strong>整体思路</strong></h2>
<p><span data-mce-=""> 在场景反序列化之后,设置热力图的初始参数,初始化后得到的热力图模型添加进场景中,模拟 <span data-mce-="">3D 热力图效果,最后再添加扫描、换肤、温度提示等功能。</span></span></p>
<h3><strong>1.数据准备</strong></h3>
<p><span data-mce-=""> 在场景中画出热力图的区域,如图</span></p>
<p><span data-mce-=""><img src="https://img2018.cnblogs.com/i-beta/1496396/202001/1496396-20200103114426699-1336302450.png"></span></p>
<p><span data-mce-=""> 首先确定要生成热力图的区域 areaNode <span data-mce-="">,然后随机生成<span data-mce-=""> <span data-mce-="">20 <span data-mce-="">个点的信息,包含坐<span data-mce-="">标 <span data-mce-=""><span data-mce-="">position <span data-mce-="">(坐标是相对红色长方体的某个顶点) 及热力值 <span data-mce-=""><span data-mce-="">temperature 。<span data-mce-=""><br></span></span></span></span></span></span></span></span></span></span></span></span></p>
<p> 以下是该部分的主要代码:</p>
<div class="cnblogs_code">
<pre><span data-mce-="">function<span data-mce-=""> getTemplateList(areaNode, hot, num) {
let heatRect =<span data-mce-=""> areaNode.getRect();
let { width, height } =<span data-mce-=""> heatRect;
let rackTall =<span data-mce-=""> areaNode.getTall();
hot = hot + <span data-mce-="">this.random(20<span data-mce-="">);
let templateList =<span data-mce-=""> [];
<span data-mce-="">for (let i = 0; i < num; i++<span data-mce-="">) {
templateList.push({
position: {
x: 0.2 * width + <span data-mce-="">this.random(0.6 *<span data-mce-=""> width),
y: 0.2 * height + <span data-mce-="">this.random(0.6 *<span data-mce-=""> height),
z: 0.1 * rackTall + <span data-mce-="">this.random(0.8 *<span data-mce-=""> rackTall)
},
temperature: hot
});
}
<span data-mce-="">return<span data-mce-=""> templateList;
}
let heatMapArea_1 = dm.getDataByTag('heatMapArea_1'<span data-mce-="">);
let templateList_1 = <span data-mce-="">this<span data-mce-="">.getTemplateList(
heatMapArea_1,
70<span data-mce-="">,
20<span data-mce-="">
);</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<h3><strong>2.初始化</strong></h3>
<p><span data-mce-=""> 使用<span data-mce-=""> <span data-mce-=""><span data-mce-="">ht-thermodynamic.js 插件来生成热力图。</span></span></span></span></p>
<p><span data-mce-=""><span data-mce-=""> 发热点的数据准备好后,接着配置热力图的参数,参数说明如下。</span></span></p>
<div class="cnblogs_code">
<pre><span data-mce-="">//<span data-mce-=""> 默认配置
let config =<span data-mce-=""> {
hot: 45<span data-mce-="">,
min: 20<span data-mce-="">,
max: 55<span data-mce-="">,
size: 50<span data-mce-="">,
pointNum: 20<span data-mce-="">,
radius: 150<span data-mce-="">,
opacity: 0.05<span data-mce-="">,
colorConfig: {
0: 'rgba(0,162,255,0.14)'<span data-mce-="">,
0.2: 'rgba(48,255,183,0.60)'<span data-mce-="">,
0.4: 'rgba(255,245,48,0.70)'<span data-mce-="">,
0.6: 'rgba(255,73,18,0.90)'<span data-mce-="">,
0.8: 'rgba(217,22,0,0.95)'<span data-mce-="">,
1: 'rgb(179,0,0)'<span data-mce-="">
},
colorStopFn: <span data-mce-="">function (v, step) { <span data-mce-="">return v * step *<span data-mce-=""> step },
};
<span data-mce-="">//<span data-mce-=""> 获取区域数据
let rackTall =<span data-mce-=""> areaNode.getTall();
let heatRect =<span data-mce-=""> areaNode.getRect();
let { width, height } =<span data-mce-=""> heatRect;
<span data-mce-="">if (width === 0 || height === 0) <span data-mce-="">return<span data-mce-="">;
<span data-mce-="">//<span data-mce-=""> 热力图初始化
let thd = <span data-mce-="">this.thd = <span data-mce-="">new<span data-mce-=""> ht.thermodynamic.Thermodynamic3d(g3d, {
<span data-mce-="">//<span data-mce-=""> 热力图所占用的空间
box: <span data-mce-="">new<span data-mce-=""> ht.Math.Vector3(width, height, rackTall),
<span data-mce-="">//<span data-mce-=""> 配置温度的最小值和最大值
<span data-mce-=""> min: config.min,
max: config.max,
<span data-mce-="">//<span data-mce-=""> 每一片的渲染间隔
interval: 40<span data-mce-="">,
<span data-mce-="">//<span data-mce-=""> 为false时,温度区域交集时值不累加,取最高温度
remainMax: <span data-mce-="">false<span data-mce-="">,
<span data-mce-="">//<span data-mce-=""> 每一片的透明度
<span data-mce-=""> opacity: config.opacity,
<span data-mce-="">//<span data-mce-=""> 颜色步进
<span data-mce-=""> colorStopFn: config.colorStopFn,
<span data-mce-="">//<span data-mce-=""> 颜色范围
<span data-mce-=""> gradient: config.colorConfig
}); </span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<h3><strong>3.加载热力图</strong></h3>
<p> 将第一步生成的发热点,设置<span data-mce-=""> thd 的数据对象,调用<span data-mce-=""> thd.createThermodynamicNode() 来生成热力图的<span data-mce-=""> 3D 图元。设置其相关信息,<span data-mce-="">将该图元添加进<span data-mce-=""> 3D 场景中。这样一个简单的<span data-mce-=""> 3D 热力图就算完成了。</span></span></span></span></span></span></p>
<div class="cnblogs_code">
<pre><span data-mce-="">//<span data-mce-=""> 加载热力图
<span data-mce-="">function<span data-mce-=""> loadThermodynamic(thd, areaNode, templateList, config) {
thd.setData(templateList);
<span data-mce-="">//<span data-mce-=""> x,y,z面数
let node = <span data-mce-="">this.heatNode =<span data-mce-=""> thd.createThermodynamicNode(config.size, config.size, config.size);
let p3 =<span data-mce-=""> areaNode.p3();
node.setAnchorElevation(0<span data-mce-="">);
node.p3(p3);
node.s({
'interactive': <span data-mce-="">true<span data-mce-="">,
'preventDefaultWhenInteractive': <span data-mce-="">false<span data-mce-="">,
'3d.movable': <span data-mce-="">false<span data-mce-="">,
"wf.visible": <span data-mce-="">false<span data-mce-="">
});
g3d.dm().add(node);
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<p> 主体介绍完了,现在开始讲讲该<span data-mce-=""> demo 的几个功能。</span></p>
<h3><strong>4.温度提示</strong></h3>
<p><img src="https://img2018.cnblogs.com/common/1496396/202001/1496396-20200116120151531-920265217.gif"></p>
<p class="brush:javascript;gutter:true;"> 因为在<span data-mce-=""> 3D 场景中,我不好判断当前鼠标坐标<span data-mce-="">(x,y,z),所以我将<span data-mce-=""> tip 面板放在了 <span data-mce-="">2D 图纸上,将<span data-mce-=""> 2D 图纸嵌在<span data-mce-=""> 3D 场景的上层。通过监听<span data-mce-=""> 3D 场景中的<span data-mce-=""> onMove 事件来控制<span data-mce-=""> tip 面板的显隐及值的变化。<br><span data-mce-=""> tip 显隐控制:当鼠标移入进热力图区域时,<span data-mce-="">tip 显示,反之则隐藏。在这我遇到了个问题,因为我把除了热力图区块以外的设置成不可交互的,当鼠标移出区域后,无法监听到<span data-mce-=""> onMove 事件,导致<span data-mce-=""> bug,<span data-mce-="">tip 面板始终存在着。我使用了<span data-mce-=""> setTimeout 来解决这问题,延时1s后自动隐藏,但后来发现完全没必要滥用 setTimeout ,只要监听<span data-mce-=""> onLeave 时隐藏<span data-mce-=""> tip 就行了。<br><span data-mce-=""> tip 值控制:调用<span data-mce-=""> ht-thermodynamic.js 的方法可以获取到当前鼠标相对热力图区域的温度值<span data-mce-=""> thd.getHeatMapValue(e.event,'middle'),实时改变<span data-mce-=""> tip 面板的<span data-mce-=""> value 属性 。<br> 代码如下:</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></p>
<div class="cnblogs_code">
<pre><span data-mce-="">//<span data-mce-=""> 交互效果
g3d.mi(e =><span data-mce-=""> {
<span data-mce-="">if (e.kind === 'onMove'<span data-mce-="">) {
let { clientX, clientY } =<span data-mce-=""> e.event;
<span data-mce-="">if (<span data-mce-="">this<span data-mce-="">.templateTip) {
let value1 = <span data-mce-="">this.thd1.getHeatMapValue(e.event, 'middle'<span data-mce-="">);
let value2 = <span data-mce-="">this.thd2.getHeatMapValue(e.event, 'middle'<span data-mce-="">);
<span data-mce-="">if (value1 || value1 === 0 || value2 || value2 === 0<span data-mce-="">) {
let position =<span data-mce-=""> g2d.getLogicalPoint({ x: clientX, y: clientY })
<span data-mce-="">this.templateTip.a('value', value1 || value2 || 0<span data-mce-="">)
let { width, height } = <span data-mce-="">this<span data-mce-="">.templateTip.getRect()
<span data-mce-="">this.templateTip.setPosition({ x: position.x + width / 2, y: position.y - height / 2<span data-mce-=""> })
}
}
} <span data-mce-="">else <span data-mce-="">if (kind === 'onLeave'<span data-mce-="">) {
let tag =<span data-mce-=""> data.getTag()
<span data-mce-="">if (tag && tag.hasOwnProperty('hoverBlock') > -1<span data-mce-="">) {
<span data-mce-="">this.g2d.getView().style.cursor = 'default'<span data-mce-="">;
}
<span data-mce-="">this.templateTip && <span data-mce-="">this.setVisible(<span data-mce-="">this.templateTip, <span data-mce-="">false<span data-mce-="">)
}
})</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<h3><strong>5.扫描</strong></h3>
<p><strong><img src="https://img2018.cnblogs.com/common/1496396/202001/1496396-20200116120736324-2136384117.gif"></strong></p>
<p><span data-mce-=""> <span data-mce-="">将第三步中的 thd.createThermodynamicNode() 替换。在生成热力图对象时,不直接返回一个模型,而是选择某一个方向进行“切割”,将这一方向的长度均分为 n 份,通过 thd.getHeatMap() 方法来获取每一片的热成像。n 的值理论上可以取任意值,但为了渲染效果更好一点,这里我取的是 50,不至于太多而导致首次渲染时间过长。每切出一个面,我们就在热力区域的相对位置上动态创建一个 ht.Node ,接着使用 ht.Default.setImage() 将切出来的面注册成图片,去设置成该 node 的贴图(只需设置切割方向上的两个面就行)。最后将所有的 node 添加进 dataModel ( ht 中承载 <code>Data </code>数据的模型)。</span></span></p>
<p> 扫描功能,有两种方案。第一种是在步骤<span data-mce-=""> 3 切割贴片时,不去创建<span data-mce-=""> n 个<span data-mce-=""> node ,而是只创建一个,然后动态去设置该<span data-mce-=""> node 的贴图及坐标,模拟扫描效果;第二种依旧创建<span data-mce-=""> n 个<span data-mce-=""> node,然后全部隐藏,通过不同时刻来控制让其中某一个节点显示,模拟扫描功能。这里我采用了第二种,因为第一种要去频繁的修改多种属性才能达到效果,第二种的话只要控制其 '3d.visible'。</span></span></span></span></span></span></p>
<p> 主要代码如下:</p>
<div class="cnblogs_code">
<pre><span data-mce-="">let length;
<span data-mce-="">if (dir === 'z'<span data-mce-="">) {
length =<span data-mce-=""> rackTall;
}
<span data-mce-="">else <span data-mce-="">if (dir === 'x'<span data-mce-="">) {
length =<span data-mce-=""> width;
}
<span data-mce-="">else <span data-mce-="">if (dir === 'y'<span data-mce-="">) {
length =<span data-mce-=""> height;
}
let size =<span data-mce-=""> config.size;
<span data-mce-="">for (let index = 0; index < size; index++<span data-mce-="">) {
<span data-mce-="">//<span data-mce-=""> 热力切图间隔
const offset = length /<span data-mce-=""> size;
let timer = setTimeout(() =><span data-mce-=""> {
let ctx = thd.getHeatMap(index *<span data-mce-=""> offset, dir, colorConfig);
let floor = <span data-mce-="">this<span data-mce-="">.getHeatFloor(
areaNode,
dir,
ctx,
index,
size,
config
);
<span data-mce-="">this<span data-mce-="">.floors.push(floor);
dm.add(floor);
}, 0<span data-mce-="">);
<span data-mce-="">this<span data-mce-="">.timers.push(timer);
}
<span data-mce-="">function<span data-mce-=""> start() {
<span data-mce-="">this<span data-mce-="">.hide();
<span data-mce-="">this.anim = <span data-mce-="">true<span data-mce-="">;
<span data-mce-="">this.count = 0<span data-mce-="">;
let frames = <span data-mce-="">this<span data-mce-="">.floors.length;
let params =<span data-mce-=""> {
frames, <span data-mce-="">//<span data-mce-=""> 动画帧数
interval: 50, <span data-mce-="">//<span data-mce-=""> 动画帧间隔毫秒数
easing: t =><span data-mce-=""> {
<span data-mce-="">return<span data-mce-=""> t;
},
finishFunc: () =><span data-mce-=""> {
<span data-mce-="">if (<span data-mce-="">this<span data-mce-="">.anim) {
<span data-mce-="">this<span data-mce-="">.start();
}
},
action: (v, t) =><span data-mce-=""> {
<span data-mce-="">this.count++<span data-mce-="">;
<span data-mce-="">this.show(<span data-mce-="">this<span data-mce-="">.count);
}
};
<span data-mce-="">this.scanning =<span data-mce-=""> ht.Default.startAnim(params);
}
<span data-mce-="">function<span data-mce-=""> hide(index) {
<span data-mce-="">if (index || index === 0<span data-mce-="">) {
<span data-mce-="">this.floors.forEach((i, j) =><span data-mce-=""> {
<span data-mce-="">if (index ===<span data-mce-=""> j) {
i.s('3d.visible', <span data-mce-="">false<span data-mce-="">);
}
<span data-mce-="">else<span data-mce-=""> {
i.s('3d.visible', <span data-mce-="">true<span data-mce-="">);
}
});
}
<span data-mce-="">else<span data-mce-=""> {
<span data-mce-="">this.floors.forEach(i =><span data-mce-=""> {
i.s('3d.visible', <span data-mce-="">false<span data-mce-="">);
});
}
}
<span data-mce-="">function<span data-mce-=""> show(index) {
<span data-mce-="">if (index || index === 0<span data-mce-="">) {
<span data-mce-="">this.floors.forEach((i, j) =><span data-mce-=""> {
<span data-mce-="">if (index ===<span data-mce-=""> j) {
i.s('3d.visible', <span data-mce-="">true<span data-mce-="">);
}
<span data-mce-="">else<span data-mce-=""> {
i.s('3d.visible', <span data-mce-="">false<span data-mce-="">);
}
});
}
<span data-mce-="">else<span data-mce-=""> {
<span data-mce-="">this.floors.forEach(i =><span data-mce-=""> {
i.s('3d.visible', <span data-mce-="">true<span data-mce-="">);
});
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<p>第一种方式实现主要代码:</p>
<div class="cnblogs_code">
<pre><span data-mce-="">getHeatFloor(node, dir, config) {
let { width, height } =<span data-mce-=""> node.getRect();
let rackTall =<span data-mce-=""> node.getTall();
let s3 = ;
let floor = <span data-mce-="">new<span data-mce-=""> ht.Node();
floor.setTag('hotspot'<span data-mce-="">);
floor.setAnchor3d({
x: 0.5<span data-mce-="">,
y: 0.5<span data-mce-="">,
z: 0.5<span data-mce-="">
});
floor.s3(s3);
floor.s({
interactive: <span data-mce-="">true<span data-mce-="">,
preventDefaultWhenInteractive: <span data-mce-="">false<span data-mce-="">,
'3d.selectable': <span data-mce-="">true<span data-mce-="">,
'3d.movable': <span data-mce-="">false<span data-mce-="">,
'all.visible': <span data-mce-="">false<span data-mce-="">,
: <span data-mce-="">true<span data-mce-="">,
: config.opacity,
: <span data-mce-="">true<span data-mce-="">,
: <span data-mce-="">true<span data-mce-="">,
: 'rgba(51,255,231,0.10)'<span data-mce-="">
});
<span data-mce-="">return<span data-mce-=""> floor
}
getHeatFloorInfo(node, dir, ctx, index, size, config) {
let { width, height } =<span data-mce-=""> node.getRect();
let rackTall =<span data-mce-=""> node.getTall();
let point =<span data-mce-=""> node.getPosition3d();
let part = 0<span data-mce-="">;
let p3, s3;
let Top = 'top'<span data-mce-="">;
<span data-mce-="">if (!<span data-mce-="">dir) {
dir = 'z'<span data-mce-="">;
}
<span data-mce-="">//<span data-mce-=""> 热力图的yz方向与ht的yz方向相反 dir=z代表的是竖直方向
<span data-mce-="">if (dir === 'x'<span data-mce-="">) {
Top = 'left'<span data-mce-="">;
part = (width / size) *<span data-mce-=""> index;
p3 =<span data-mce-=""> [
point - width / 2 +<span data-mce-=""> part,
point + rackTall / 2<span data-mce-="">,
point
];
<span data-mce-="">//<span data-mce-=""> p3 = + part, point, point];
s3 = ;
}
<span data-mce-="">else <span data-mce-="">if (dir === 'y'<span data-mce-="">) {
Top = 'front'<span data-mce-="">;
part = (height / size) *<span data-mce-=""> index;
p3 =<span data-mce-=""> [
point,
point + rackTall / 2<span data-mce-="">,
point - height / 2 +<span data-mce-=""> part
];
s3 = ;
}
<span data-mce-="">else <span data-mce-="">if (dir === 'z'<span data-mce-="">) {
Top = 'top'<span data-mce-="">;
part = (rackTall / size) *<span data-mce-=""> index;
p3 = , point + part, point];
s3 = ;
}
let heatName = <span data-mce-="">this<span data-mce-="">.generateUUID();
ht.Default.setImage('heatMap' +<span data-mce-=""> heatName, ctx);
<span data-mce-="">this<span data-mce-="">.heatFloorInfo.push(
{
img: 'heatMap' +<span data-mce-=""> heatName,
p3
}
)
}
show(index){
let info = <span data-mce-="">this<span data-mce-="">.heatFloorInfo
<span data-mce-="">this<span data-mce-="">.floor.p3(info.p3)
<span data-mce-="">this.floor.s('3d.visible', <span data-mce-="">true<span data-mce-="">);
<span data-mce-="">this.floor.s('top.image'<span data-mce-="">, info.img);
<span data-mce-="">//<span data-mce-=""> 手动刷新
<span data-mce-="">this<span data-mce-="">.floor.iv();
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<h3><strong>6.换肤</strong></h3>
<p><strong><img src="https://img2018.cnblogs.com/common/1496396/202001/1496396-20200116121110643-48267756.gif"></strong></p>
<p> 换肤的实现原理:根据不同的场景值去动态修改<span data-mce-=""> ht.graph3d.Graph3dView 的背景色及墙的颜色等。</span></p>
<p> 代码:</p>
<div class="cnblogs_code">
<pre><span data-mce-="">function<span data-mce-=""> changeSkin() {
let backgroundColor = <span data-mce-="">this<span data-mce-="">.g3d.dm().getBackground(),
dark_bg = <span data-mce-="">this.g3d.dm().getDataByTag('dark_skin'<span data-mce-="">),
light_bg = <span data-mce-="">this.g3d.dm().getDataByTag('light_skin'<span data-mce-="">);
<span data-mce-="">if (backgroundColor !== 'rgb(255,255,255)'<span data-mce-="">) {
<span data-mce-="">this.g3d.dm().setBackground('rgb(255,255,255)'<span data-mce-="">);
} <span data-mce-="">else<span data-mce-=""> {
<span data-mce-="">this.g3d.dm().setBackground('rgb(0,0,0)'<span data-mce-="">);
}
dark_bg.s('2d.visible', !dark_bg.s('2d.visible'<span data-mce-="">));
dark_bg.s('3d.visible', !dark_bg.s('3d.visible'<span data-mce-="">));
light_bg.s('2d.visible', !light_bg.s('2d.visible'<span data-mce-="">));
light_bg.s('3d.visible', !light_bg.s('3d.visible'<span data-mce-="">));
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
</div>
<p> 本篇就介绍到了,目前<span data-mce-=""> ht-thermodynamic.js 还处于测试阶段,待到相对成熟后再更新该 <span data-mce-="">demo ,有兴趣了解更多关于<span data-mce-=""> 2D/3D 可视化的构建,可翻阅其他文章的例子,<span data-mce-="">HT 会给你很多不可思议的东西。</span></span></span></span></p><br><br>
来源:https://www.cnblogs.com/xhload3d/p/12374499.html
頁:
[1]