// 建立二維拓撲視圖 this.g2d = new ht.graph.GraphView(); this.g2dDm = this.g2d.dm(); // 建立三維拓撲視圖 this.g3d = new ht.graph3d.Graph3dView(); this.g3dDm = this.g3d.dm(); // 將二維圖紙嵌入到三維場景中 this.g2d.addToDOM(this.g3d.getView()); // 修改左右鍵交互方式 let mapInteractor = new ht.graph3d.MapInteractor(this.g3d); this.g3d.setInteractors([mapInteractor]); // 修改最大仰角爲 PI / 2 mapInteractor.maxPhi = Math.PI / 2; const G = {}; window.G = G; // 事件派發 G.event = new ht.Notifier();
3D 場景加載主視圖爲:html
首先我搭建了一個 3D 的場景用來放置咱們的 json 場景數據,利用 ht.Default.xhrLoad 函數解析 json 場景數據,並經過 deserialize 將反序列化的對象加入DataModel來顯示加載 3D 場景,有興趣的能夠經過<HT的序列化手冊>來了解這一機制的實現。json
ht.Default.xhrLoad('scenes/demo.json', (json) => { if (!json) return; g3dDm.deserialize(json); // 設置三維視圖的中心點和相機位置 g3d.setCenter([-342, -64, 389]); g3d.setEye([-355, 10833, 2642]); // 設置最遠距離 g3d.setFar(1000000); // 獲取球圖標,設置爲天空球 let skybox = g3dDm.getDataByTag('skyBox'); g3d.setSkyBox(skybox); // 模型加載完後執行動畫 const modelList = []; g3dDm.each(d => { const shape3d = d.s('shape3d'); if (!shape3d || !shape3d.endsWith('.json')) return; if (ht.Default.getShape3dModel(shape3d)) return; modelList.push(shape3d); }); ht.Default.handleModelLoaded = (name, model) => { const index = modelList.indexOf(name); if (index < 0) return; modelList.splice(index, 1); if (modelList.length > 91) return; ht.Default.handleModelLoaded = () => { }; // 模型加載完侯,默認執行場景切換動畫 g3d.moveCamera([257, 713, 1485], [7, 40, 144], { duration: 2000, finishFunc: () => { this.load2D(); } }); }; });
2D 面板加載視圖爲:數組
一樣,我搭建了一個 2D 的場景用來放置咱們的 json 矢量圖,利用 ht.Default.xhrLoad 函數將 json 矢量背景圖反序列化顯示在 2D 面板數據。瀏覽器
ht.Default.xhrLoad('displays/demo.json', (json) => { if (!json) return; g2dDm.deserialize(json); // 面板動畫入口 this.tittleAnim(); this.panelTime(); // 2D圖紙加載完後執行事件處理 this.loaded2DHandler(); });
2、3D 動畫效果以及切換漫遊緩存
對於 3D 建模下的樓宇建築,加上場景的全方位漫遊,可以使用戶達到一種沉浸式的體驗,更加直觀地去感覺這個樓宇下各個場景的聯繫,依次地介紹了冷站、智慧末端以及熱站的位置以及功能運做的動畫 。主要運用的方法是經過藉助 HT 提供的 ht.Shape 圖元類型,能夠在 GraphView 和 Graph3dView 組件上展現出各類二維和三維的形狀效果,而漫遊的管道路線就是由其擴展子類 ht.Polyline 去繪製實現一條三維的管道,而後用這條繪製的管道加上漫遊的時間去調用這個漫遊的方法,其本質上是圍繞着中心點,而後根據管道去不斷地改變視角下的 eye 和 center 的數值,達到環視這個建築的總體視角。安全
這裏能夠了解一下關於空間軌道的繪製,詳見<HT的形狀手冊>的空間管線章節。網絡
如下是環視漫遊動畫的僞代碼:運維
polyLineRoam(polyLine, time) { const g3d = this.g3d; const g3dDm = this.g3dDm; this.roamButton.a('active', true); this.roamAnim = ht.Default.startAnim({ duration: time, easing: t => t, action: (v, t) => { let length = this.main.g3d.getLineLength(polyLine), offset = this.main.g3d.getLineOffset(polyLine, length * v), point = offset.point, px = point.x, py = point.y, pz = point.z; g3d.setEye(px, py, pz); g3d.setCenter(7, 40, 144); }, finishFunc: () => { this.roam1(); } }); }
在總體建築的環視漫遊完後,咱們能夠經過拉近各個場景的視角,來依次巡視各個場景所執行的動畫。在根據管道改變 eye 和 center 環視漫遊方法結束後,用動畫的結束回調 finishFunc 去調用下一個動畫的執行,而巡視漫遊就在這裏去調用,如下咱們以巡視冷站的漫遊動畫爲例去介紹實現的方法。ide
巡視漫遊的主要實現方法是經過 HT 核心包的相機移動 moveCamera 來實現的, 經過參數 (eye, center, animation) 來調用這個方法:函數
每次執行完一個場景的視角移動後,再經過相機移動動畫的結束回調 finishFunc 調用下一個相機移動的動畫,達到巡視漫遊的效果。
// 切換到冷站視角 roam1() { const g3d = this.g3d; const g3dDm = this.g3dDm; this.roamAnim = g3d.moveCamera([-291, -8, 283], [148, -400, 171], { duration: 500, easing: t => t * t, finishFunc: () => { this.roam2(); } }); }
在環視漫遊和巡視漫遊的執行下,咱們也能夠觸發 2D 圖紙右面板下的按鈕面板去觀看咱們想要瀏覽的指定場景,這時候就會關閉當前在執行的環視漫遊或者巡視漫遊,再次點擊改按鈕則返回場景的主視角,或者點擊左上角漫遊按鈕又能夠進入環視漫遊,這樣的交互體驗,能夠方便用戶即便地查看想要瀏覽的場景,而不用依靠等待逐一漫遊下去查看,也不會干擾到漫遊的總體體驗。相應地經過介紹冷站按鈕的點擊觸發介紹一下實現的方法。
通常的交互方式存在三種事件交互的方法,包括事件通知管理器 ht.Notifier 類,內置的 Interator 在交互過程會派發出事件和數據綁定的監聽來實現,而這裏使用的是第三種交互方式。
經過數據綁定監聽到 onDown 執行按下的事件後,經過改變按下和再次按下的按鈕狀態 active 來分別執行相機移動去切換視角,主要實現的僞代碼以下:
// 設置圖元可交互 this.coolingCentralStationButton.s('interactive', true); // 經過數據綁定監聽到onDown執行按下的事件 this.coolingCentralStationButton.s('onDown', () => { // 切換到冷站時,2d面板所執行的切換動畫 this.switchToColdStation(); // 按鈕初始化 this.buttonTearDown(); // 按鈕按下效果的狀態 let active = this.coolingCentralStationButton.a('active'); // button爲按鈕集合數組,當按下電梯按鈕,其餘按鈕默認false button.forEach(btn => { btn.a('active', false); }); // 冷站按鈕的狀態切換 this.coolingCentralStationButton.a('active', !active); // 根據冷站按鈕的狀態執行切換到冷站或者切換回主視角 if (active) { // 相機移動切換到主視角 moveCamera(g3d, [257, 713, 1485], [7, 40, 144], { duration: 2000, easing: t => t * t }); } else { // 漫遊動畫對象若是不爲空,則暫停漫遊動畫對象而且設置爲空 if (this.roamAnim !== null) { this.roamAnim.pause(); this.roamAnim = null; } // 相機移動切換到冷站視角 coolingCentralStationAnimation = moveCamera(g3d, [-291, -8, 283], [148, -400, 171], { duration: 2000, easing: t => t * t }); } });
固然,在 3D 場景下還有一些頗有趣的動畫效果,好比車流效果、飛光效果和圓環擴散效果。車流效果主要經過採用了貼圖的 uv 的偏移來實現達到車流穿梭的科技感效果;而飛光效果則是採用調度動畫的方法來間隔設置飛光的高度,達到最高點則消失而後從新輪迴動畫展現;圓環擴散效果則是一樣採用調度動畫的方法來間隔設置圓環的縮放值和透明度,來達到擴散消失的效果。
對於間隔的調度動畫,爲了實現動畫的流暢性,這裏調度使用的 loop 是運用到本身封裝 HT 的動畫 ht.Default.startAnim 的一個方法:
loop(action, interval = 20) { return ht.Default.startAnim({ frames: Infinity, interval: interval, action: action }); }
而後經過調用這個 loop 的間隔動畫方法,咱們來實現車流效果、飛光效果和圓環擴散效果,實現的參考僞代碼以下:
// 車流圖元的初始化 let traffic = g3dDm.getDataByTag('traffic'); // 圓環擴散圖元的初始化 let lightRing = this.lightRing = g3dDm.getDataByTag('lightRing'); // 飛光圖元設置三種透明狀態數組集合flyMap的初始化 [1, 2, 3].forEach(i => { const data = flyMap['fly' + i] = g3dDm.getDataByTag('fly' + i); data.eachChild(d => { d.s({ // 打開透明度 'shape3d.transparent': true, // 根據不一樣的數組集合設置不一樣的透明度 'shape3d.opacity': i === 3 ? 0.5 : 0.7, // 設置沿着y軸自動旋轉 'shape3d.autorotate': 'y' }); }); }); if (this.flyAnim) return; this.flyAnim = loop(() => { // 飛光根據間隔設置高度來達到上升的效果 for (let k in flyMap) { const data = flyMap[k]; let e = data.getElevation() + flyDltMap[k]; if (e >= 500) e = -400; data.setElevation(e); } // 車流根據設置間隔增加uv偏移量來實現穿梭的效果 traffic.eachChild(c => { c.s('all.uv.offset', [location, 0]); }); location -= 0.03; // 旋轉震盪波透明度漸降 let percent = lightRing.a('percent') || 0, scale = 15 * percent + 0.5; lightRing.setScale3d([scale + 1, scale, scale + 1]); lightRing.s('shape3d.opacity', (1 - percent) * 0.5); percent += 0.01; if (percent >= 1) { percent = 0; } lightRing.a('percent', percent); }, 50);
3、冷站場景和熱站場景的動畫實現
場景動畫中機組的風扇、集水器的蓄滿以及水的流動效果:
動畫的實現主要仍是經過 HT 自帶的 ht.Default.startAnim 動畫
函數,支持 Frame-Based 和 Time-Based 兩種方式的動畫。一樣的,咱們這裏使用的是 Frame-Based 來封裝一個 loop 函數來執行每一幀間隔的動畫。
通常來講,動畫可經過自行配置來達到本身想要實現的方法,這裏能夠了解< HT 的入門手冊>關於動畫
函數的介紹。
if (this.stationAnim) return; this.stationAnim = loop(() => { // 冷站水管流動 coldFlow_blue.eachChild(c => { c.s('shape3d.uv.offset', [-location, 0]); }); coldFlow_yellow.eachChild(c => { c.s('shape3d.uv.offset', [location, 0]); }); // 熱站水管流動 heatFlow_blue.eachChild(c => { c.s('shape3d.uv.offset', [-location, 0]); }); heatFlow_yellow.eachChild(c => { c.s('shape3d.uv.offset', [location, 0]); }); location -= 0.03; // 冷站風扇旋轉 cold_fan.eachChild(c => { c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]); }); // 熱站風扇旋轉 heat_fan.eachChild(c => { c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]); }); // 集水器水位變化 HotWaterTankTall += 0.25; if (HotWaterTankTall > 15) { HotWaterTankTall = 0; } coldWaterTankTall1 += 0.25; if (coldWaterTankTall1 > 20) { coldWaterTankTall1 = 0; } coldWaterTankTall2 += 0.25; if (coldWaterTankTall2 > 20) { coldWaterTankTall2 = 0; } hotWaterTank.setTall(HotWaterTankTall); coldWaterTank1.setTall(coldWaterTankTall1); coldWaterTank2.setTall(coldWaterTankTall2); }, 50);
4、中央空調末端智慧羣控系統場景效果
這裏採用了模擬數據的方式來體現末端智能節能控制的效果。應用於真實項目的時候,能夠採用數據接口的方式來實時對接真實數據,能夠達到實時監控的效果。
我使用了本身 mock 的末端羣控的數據參數,格式以下:
var boxData = [ [{ // 設備編號 id: 'box1', // 設備的溫度 temperature: 23.8, // 設備的頻率 frequency: 45.8 }, ...] ... ];
這裏的實現也是經過 loop 循環執行數據的讀取,當數組指標 index 讀取到最後一個數據時,當即關閉循環並清空 loop調度。
boxAnimation = loop(() => { for (let i = 0, l = 16; i <= l-1; i++) { let roomTag, roomBox, tag; tag = i+1; roomTag = 'boxPanel' + tag; roomBox = 'box' + tag; let panel = g3dDm.getDataByTag(roomTag); let box = g3dDm.getDataByTag(roomBox); if (panel) { panel.a('valueT', boxData[index][i].temperature + '℃'); panel.a('valueK', boxData[index][i].frequency + 'Hz'); // 手動更新緩存的面板信息 g3d.invalidateShape3dCachedImage(panel); // 根據溫度判斷設備的顏色 if (box && parseFloat(panel.a('valueT')) < 26) { box.s('shape3d.blend', 'rgb(4,67,176)'); box.s('wf.color', 'rgb(4,67,176)'); } else if (box && parseFloat(panel.a('valueT')) >= 26 && parseFloat(panel.a('valueT')) <= 28) { box.s('shape3d.blend', 'rgb(28,189,87)'); box.s('wf.color', 'rgb(28,189,87)'); } else if (box && parseFloat(panel.a('valueT')) > 28) { box.s('shape3d.blend', 'rgb(181,43,43)'); box.s('wf.color', 'rgb(181,43,43)'); } } } index++; if (index >= 10) { boxAnimation.pause(); boxAnimation = null; } }, 500);
總結