前些日子出差,在飞机上看到头顶的监控面板,除了播放电视剧和广告以外,还会时不时的切换到一个飞机航行的监控系统,不过整个监控系统让人感到有一点点的简陋,因此我就突发奇想制做了一个采用 HT for Web 的升级版监控系统,demo 的效果还行,发出来你们相互学习下。程序员
demo(https://www.hightopo.com/demo/flight-monitor/)svg
实现过程
云中穿行效果
为了达到飞机云中穿行的效果,最开始我遇到的问题是飞机飞行的层次感,也就一般所说的透视效果,这里我采用的是云通道和云背景以不一样的速度流动,制造一种飞行的透视效果。学习
云我采用的是贴图的方式呈现的,可是仅仅是贴图会遮挡天空和飞机,很是影响飞机飞行的观感,因此我开启了相应图元的 transparent
和 opacity
,云背景和云通道设置不一样的透明度,不只增长了层次感,还会让人产生云朵从眼前飘过的错觉。大数据
云通道采用的是 ht.Polyline
类型,通道缩放拉大了 Y 轴的比例,使云通道有更大的纵向空间,设置 reverse.flip
背拷贝使云通道内部也显示出贴图,仿佛让飞机置身于云海中穿梭;云背景采用 ht.Node
类型,只设置一个面显示充当云背景。动画
总体的云流动效果采用 offset
偏移实现,改变相应图元或相应图元面的贴图偏移量来达到飞机云中穿行的效果, 代码以下:this
var i = 1, p = 0; setInterval(() => { i -= 0.1; p += 0.005; clouds.s('shape3d.uv.offset', [i, 0]); cloudBackground.s('all.uv.offset', [p, 0]); }, 100);
升降颠簸效果
虽然达到了飞机云中穿行的效果,可是若是飞机只是直直的飞行,那也会下降飞行的实感,相信坐过飞机的朋友确定都遇到过因气流产生的颠簸,也常常感觉到飞机飞行途中的爬升和降低,这实际上是由于飞机的航线并非一直固定在一个高度上,有时会爬升有时会降低,因此我就用 ht-animation.js HT 动画扩展插件去实现飞机颠簸效果,代码以下:url
dm.enableAnimation(20); plane.setAnimation({ back1: { from: 0, to: 160, easing: 'Cubic.easeInOut', duration: 8000, next: "up1", onUpdate: function (value) { value = parseInt(value); var p3 = this.p3(); this.p3(value, p3[1], p3[2]); } }, //...省略类似 start: ["back1"] });
球扇形视角限制
飞行效果完善以后,这时我就遇到了一个比较棘手的问题,由于实际上虽然看着飞机是在云海中穿梭,可是仅仅是在通道中飞行,背景其实也只是平面贴图,因此当视角到达某种程度的时候就会有强烈的违和感和不真实感,就须要一个视角限制,使视角的调整刚恰好在一个范围内。spa
视角限制的话通常是限制 g3d 的 eye
和 center
,不太了解的朋友能够去看 hightopo 官网中的 3d 手册,里面有详细的说明,这里我就再也不赘述了;由于视角范围的关系,因此我决定固定 center
的位置,代码以下:.net
g3d.addPropertyChangeListener(e => { // 固定中心点 if (e.property === 'center') { e.newValue[0] = center[0]; e.newValue[1] = center[1]; e.newValue[2] = center[2]; } }
而后再把 eye
限制在某一个范围内就大功告成了,然而这里却并非那么简单,最开始我把 eye
限制在一个立方体的空间内,但交互效果很不理想,考虑到 g3d 默认交互中,鼠标拖拽平移视角变换时,实际上 eye
是在一个以 center
为球心的球面上运动的,因此我决定从这个球中挖出来一块做为 eye
的限制空间,也就是球扇形,不太理解的朋友能够参考这个图:插件
球扇形视角限制,一共须要三个参数,分别是中心参考轴、中心轴和外边所成角度、所在球限制半径,其中中心参考轴可根据初始 eye
和 center
的链接延长线肯定,所在球限制半径又分最大限制和最小限制,代码以下:
function limitEye(g3d, eye, center, options) { var limitMaxL = options.limitMaxL, limitMinL = options.limitMinL, limitA = options.limitA; g3d.addPropertyChangeListener(e => { // 固定中心点 if (e.property === 'center') { e.newValue[0] = center[0]; e.newValue[1] = center[1]; e.newValue[2] = center[2]; } // 限制视角 if (e.property === 'eye') { var newEyeV = new ht.Math.Vector3(e.newValue), centerV = new ht.Math.Vector3(center), refEyeV = new ht.Math.Vector3(eye), refVector = refEyeV.clone().sub(centerV), newVector = newEyeV.clone().sub(centerV); if (centerV.distanceTo(newEyeV) > limitMaxL) { newVector.setLength(limitMaxL); e.newValue[0] = newVector.x; e.newValue[1] = newVector.y; e.newValue[2] = newVector.z; } if (centerV.distanceTo(newEyeV) < limitMinL) { newVector.setLength(limitMinL); e.newValue[0] = newVector.x; e.newValue[1] = newVector.y; e.newValue[2] = newVector.z; } if (newVector.angleTo(refVector) > limitA) { var oldLength = newVector.length(), oldAngle = newVector.angleTo(refVector), refLength = oldLength * Math.cos(oldAngle), vertVector, realVector, realEye; refVector.setLength(refLength); newEyeV = newVector.clone().add(centerV); refEyeV = refVector.clone().add(centerV); vertVector = newEyeV.clone().sub(refEyeV); vertLength = refLength * Math.tan(limitA); vertVector.setLength(vertLength); realVector = vertVector.clone().add(refEyeV).sub(centerV); realVector.setLength(oldLength); realEye = realVector.clone().add(centerV); // 防止移动角度大于 180 度,视角反转 if (oldAngle > Math.PI / 2) { realEye.negate(); } e.newValue[0] = realEye.x; e.newValue[1] = realEye.y; e.newValue[2] = realEye.z; } } }) }
飞机监控系统
固然做为监控系统,天然要有监控了,增长右下角的小地图,并提供三种模式,分别是聚焦飞机,聚焦飞行轨迹和聚焦地图,并根据飞机的飞行方向控制飞行轨迹的流动效果,其中聚焦飞机会跟随飞机移动进行 fitData
,使飞机一直处于小地图的中心,代码以下:
var fitFlowP = function (e) { if (e.property === 'position' && e.data === plane) { mapGV.fitData(plane, false); } }; buttonP.s({ 'interactive': true, 'onClick': function (event, data, view, point, width, height) { map.a('fitDataTag', 'plane2D'); mapGV.fitData(plane, false); mapDM.md(fitFlowP); } }); buttonL.s({ 'interactive': true, 'onClick': function (event, data, view, point, width, height) { mapDM.umd(fitFlowP); map.a('fitDataTag', 'flyLine'); mapGV.fitData(flyLine, false); } }); // ...省略
增长鼠标移到飞机相应位置进行名称的提示、双击后显示飞机相应位置的信息面板并将视角聚焦到面板上、点击飞机任意地方切换回飞机飞行模式等效果。

左侧增长监控面板替代上面提到的双击相应位置这步操做直接聚焦到相应位置的信息面板上,这里按钮开启了交互并添加了相应的交互逻辑,代码以下:
button_JC.s({ 'interactive': true, 'onClick': function (event, data, view, point, width, height) { event.preventDefault(); let g3d = G.g3d, g3dDM = G.g3d.dm(); g3d.fireInteractorEvent({ kind: 'doubleClickData', data: g3dDM.getDataByTag(data.getTag()) }) } }); //...省略
天空渲染效果
既然是监控系统确定是 24 小时无差异的监控,这就涉及到一个问题,我总不可能半夜的时候飞机也从瓦蓝瓦蓝的天空上飞过,这就很欠缺真实性了,因此要有一个天空从亮到暗再从暗到亮的过程,这个过程我暂定到 06:00-06:30 和19:00-19:30 这两个时间段。
天空采用的是 shape3d : 'sphere'
球形,包裹整个场景,而后使用 reverse.flip
背拷贝 和 blend
染色,以后天空就能够渲染成我想要的颜色,若是按照时间改变天空明暗只要改变染色值就能够了。
可是因为白天和晚上光照状况的不一样,云反射光的强度也不一样,就致使了白天和晚上云的差别,因此也要调整云道和云背景的贴图的 opacity
透明度,晚间更为透明度,代码以下:
if ((hour > 6 && hour < 19) || (hour == 6 && minutes >= 30)) { timePane && timePane.a({ 'morning.visible': false, 'day.visible': true, 'dusk.visible': false, 'night.visible': false, 'day.opacity': 1 }) skyBox.s({ "shape3d.blend": 'rgb(127, 200, 240)', }) cloudBackground.s({ "back.opacity": 0.7, }) clouds.s({ "shape3d.opacity": 0.7, }) } else if ((hour < 6 || hour > 19) || (hour == 19 && minutes >= 30)) { //...省略 } else if (hour == 6 && minutes < 15 ) { //...省略 } else if (hour == 6 && minutes >= 15 && minutes < 30) { //...省略 } else if (hour == 19 && minutes < 15) { //...省略 } else if (hour == 19 && minutes >= 15 && minutes < 30) { //...省略 }
这里我还增长了对右上角时间面板时间状态图标的支持,并增长了图标切换时的渐隐渐显效果,同时给时间面板状态图标位置增长了点击切换到下一时间状态的功能。
为了演示效果我增长了时间倍速按钮,下图是 500 倍时间流速下的变化状况:
总结
经过这个 demo ,我发现生活中有不少没有被人所注意到的细节都存在数据可视化的可能,在这个大数据的时代更多的可能性值得被人发掘出来,不要错个身边每个值得数据可视化的细节,这样不只能够更好的挖掘 HT for Web 的潜力,也能够增强自身身为一个程序员的综合素质。