基于 HTML5 Canvas 的 3D 热力云图效果
前言
數(shù)據(jù)蘊(yùn)藏價(jià)值,但數(shù)據(jù)的價(jià)值需要用?IT?技術(shù)去發(fā)現(xiàn)、探索,可視化可以幫助人更好的去分析數(shù)據(jù),信息的質(zhì)量很大程度上依賴于其呈現(xiàn)方式。在數(shù)據(jù)分析上,熱力圖無(wú)疑是一種很好的方式。在很多行業(yè)中都有著廣泛的應(yīng)用。
最近剛好項(xiàng)目中需要用到?3D?熱力圖的效果展示。網(wǎng)上搜了相關(guān)資料,發(fā)現(xiàn)大多數(shù)是?2D?效果或者偽?3D?的,而?3D?粒子效果對(duì)于性能上的體驗(yàn)不是很好,于是取巧寫(xiě)了個(gè)?3D?熱力圖的效果?。
Demo?:?http://www.hightopo.com/demo/heatMap3D/
部分效果圖:
?
應(yīng)用場(chǎng)景
大樓內(nèi)的人員分布熱力圖。我們可以通過(guò)觀察到一個(gè)區(qū)域的顏色深淺來(lái)判斷該區(qū)域內(nèi)實(shí)時(shí)的人員流動(dòng)情況,知道哪個(gè)區(qū)域人多,哪個(gè)區(qū)域人少。該場(chǎng)景可適用于大樓內(nèi)的警務(wù)監(jiān)控,在發(fā)生突發(fā)事件時(shí)科學(xué)高效地制定分流疏導(dǎo)策略提供有力的幫助和支持,減少損失。亦可用于火險(xiǎn)預(yù)警,監(jiān)控區(qū)域?qū)崟r(shí)溫度。
室內(nèi)設(shè)備溫度熱力圖。傳統(tǒng)的數(shù)據(jù)中心匯報(bào)方式枯燥單調(diào)、真實(shí)感不強(qiáng),互動(dòng)性差等,借助于?3D?熱力圖的可視化呈現(xiàn)方式,機(jī)房運(yùn)維管理人員可大大提高工作效率及降低工作失誤的可能性。
整體思路
在場(chǎng)景反序列化之后,設(shè)置熱力圖的初始參數(shù),初始化后得到的熱力圖模型添加進(jìn)場(chǎng)景中,模擬?3D?熱力圖效果,最后再添加掃描、換膚、溫度提示等功能。
1.數(shù)據(jù)準(zhǔn)備
在場(chǎng)景中畫(huà)出熱力圖的區(qū)域,如圖
首先確定要生成熱力圖的區(qū)域 areaNode?,然后隨機(jī)生成?20??個(gè)點(diǎn)的信息,包含坐標(biāo)?position?(坐標(biāo)是相對(duì)紅色長(zhǎng)方體的某個(gè)頂點(diǎn)) 及熱力值?temperature?。
以下是該部分的主要代碼:
function getTemplateList(areaNode, hot, num) {let heatRect = areaNode.getRect();let { width, height } = heatRect;let rackTall = areaNode.getTall();hot = hot + this.random(20);let templateList = [];for (let i = 0; i < num; i++) {templateList.push({position: {x: 0.2 * width + this.random(0.6 * width),y: 0.2 * height + this.random(0.6 * height),z: 0.1 * rackTall + this.random(0.8 * rackTall)},temperature: hot});}return templateList; } let heatMapArea_1 = dm.getDataByTag('heatMapArea_1'); let templateList_1 = this.getTemplateList(heatMapArea_1,70,20 );2.初始化
使用?ht-thermodynamic.js??插件來(lái)生成熱力圖。
? 發(fā)熱點(diǎn)的數(shù)據(jù)準(zhǔn)備好后,接著配置熱力圖的參數(shù),參數(shù)說(shuō)明如下。
// 默認(rèn)配置 let config = {hot: 45,min: 20,max: 55,size: 50,pointNum: 20,radius: 150,opacity: 0.05,colorConfig: {0: 'rgba(0,162,255,0.14)',0.2: 'rgba(48,255,183,0.60)',0.4: 'rgba(255,245,48,0.70)',0.6: 'rgba(255,73,18,0.90)',0.8: 'rgba(217,22,0,0.95)',1: 'rgb(179,0,0)'},colorStopFn: function (v, step) { return v * step * step }, }; // 獲取區(qū)域數(shù)據(jù) let rackTall = areaNode.getTall(); let heatRect = areaNode.getRect(); let { width, height } = heatRect; if (width === 0 || height === 0) return; // 熱力圖初始化 let thd = this.thd = new ht.thermodynamic.Thermodynamic3d(g3d, {// 熱力圖所占用的空間box: new ht.Math.Vector3(width, height, rackTall),// 配置溫度的最小值和最大值 min: config.min,max: config.max,// 每一片的渲染間隔interval: 40,// 為false時(shí),溫度區(qū)域交集時(shí)值不累加,取最高溫度remainMax: false,// 每一片的透明度opacity: config.opacity,// 顏色步進(jìn)colorStopFn: config.colorStopFn,// 顏色范圍 gradient: config.colorConfig });3.加載熱力圖
將第一步生成的發(fā)熱點(diǎn),設(shè)置?thd?的數(shù)據(jù)對(duì)象,調(diào)用?thd.createThermodynamicNode()?來(lái)生成熱力圖的?3D?圖元。設(shè)置其相關(guān)信息,將該圖元添加進(jìn)?3D?場(chǎng)景中。這樣一個(gè)簡(jiǎn)單的?3D?熱力圖就算完成了。
// 加載熱力圖 function loadThermodynamic(thd, areaNode, templateList, config) {thd.setData(templateList);// x,y,z面數(shù)let node = this.heatNode = thd.createThermodynamicNode(config.size, config.size, config.size);let p3 = areaNode.p3();node.setAnchorElevation(0);node.p3(p3);node.s({'interactive': true,'preventDefaultWhenInteractive': false,'3d.movable': false,"wf.visible": false});g3d.dm().add(node); }主體介紹完了,現(xiàn)在開(kāi)始講講該?demo?的幾個(gè)功能。
4.溫度提示
因?yàn)樵?3D?場(chǎng)景中,我不好判斷當(dāng)前鼠標(biāo)坐標(biāo)(x,y,z),所以我將?tip?面板放在了?2D?圖紙上,將?2D?圖紙嵌在?3D?場(chǎng)景的上層。通過(guò)監(jiān)聽(tīng)?3D?場(chǎng)景中的?onMove?事件來(lái)控制?tip?面板的顯隱及值的變化。
tip?顯隱控制:當(dāng)鼠標(biāo)移入進(jìn)熱力圖區(qū)域時(shí),tip?顯示,反之則隱藏。在這我遇到了個(gè)問(wèn)題,因?yàn)槲野殉藷崃D區(qū)塊以外的設(shè)置成不可交互的,當(dāng)鼠標(biāo)移出區(qū)域后,無(wú)法監(jiān)聽(tīng)到?onMove?事件,導(dǎo)致?bug,tip?面板始終存在著。我使用了?setTimeout?來(lái)解決這問(wèn)題,延時(shí)1s后自動(dòng)隱藏,但后來(lái)發(fā)現(xiàn)完全沒(méi)必要濫用 setTimeout ,只要監(jiān)聽(tīng)?onLeave?時(shí)隱藏?tip?就行了。
tip?值控制:調(diào)用?ht-thermodynamic.js?的方法可以獲取到當(dāng)前鼠標(biāo)相對(duì)熱力圖區(qū)域的溫度值?thd.getHeatMapValue(e.event,'middle'),實(shí)時(shí)改變?tip?面板的?value?屬性 。
代碼如下:
5.掃描
將第三步中的?thd.createThermodynamicNode() 替換。在生成熱力圖對(duì)象時(shí),不直接返回一個(gè)模型,而是選擇某一個(gè)方向進(jìn)行“切割”,將這一方向的長(zhǎng)度均分為 n 份,通過(guò) thd.getHeatMap()??方法來(lái)獲取每一片的熱成像。n 的值理論上可以取任意值,但為了渲染效果更好一點(diǎn),這里我取的是?50,不至于太多而導(dǎo)致首次渲染時(shí)間過(guò)長(zhǎng)。每切出一個(gè)面,我們就在熱力區(qū)域的相對(duì)位置上動(dòng)態(tài)創(chuàng)建一個(gè) ht.Node ,接著使用?ht.Default.setImage() 將切出來(lái)的面注冊(cè)成圖片,去設(shè)置成該 node 的貼圖(只需設(shè)置切割方向上的兩個(gè)面就行)。最后將所有的 node 添加進(jìn)?dataModel?(?ht 中承載?Data?數(shù)據(jù)的模型)。
掃描功能,有兩種方案。第一種是在步驟?3?切割貼片時(shí),不去創(chuàng)建?n?個(gè)? node?,而是只創(chuàng)建一個(gè),然后動(dòng)態(tài)去設(shè)置該?node?的貼圖及坐標(biāo),模擬掃描效果;第二種依舊創(chuàng)建?n?個(gè)?node,然后全部隱藏,通過(guò)不同時(shí)刻來(lái)控制讓其中某一個(gè)節(jié)點(diǎn)顯示,模擬掃描功能。這里我采用了第二種,因?yàn)榈谝环N要去頻繁的修改多種屬性才能達(dá)到效果,第二種的話只要控制其 '3d.visible'。
主要代碼如下:
let length; if (dir === 'z') {length = rackTall; } else if (dir === 'x') {length = width; } else if (dir === 'y') {length = height; } let size = config.size; for (let index = 0; index < size; index++) {// 熱力切圖間隔const offset = length / size;let timer = setTimeout(() => {let ctx = thd.getHeatMap(index * offset, dir, colorConfig);let floor = this.getHeatFloor(areaNode,dir,ctx,index,size,config);this.floors.push(floor);dm.add(floor);}, 0);this.timers.push(timer); } function start() {this.hide();this.anim = true;this.count = 0;let frames = this.floors.length;let params = {frames, // 動(dòng)畫(huà)幀數(shù)interval: 50, // 動(dòng)畫(huà)幀間隔毫秒數(shù)easing: t => {return t;},finishFunc: () => {if (this.anim) {this.start();}},action: (v, t) => {this.count++;this.show(this.count);}};this.scanning = ht.Default.startAnim(params); } function hide(index) {if (index || index === 0) {this.floors.forEach((i, j) => {if (index === j) {i.s('3d.visible', false);}else {i.s('3d.visible', true);}});}else {this.floors.forEach(i => {i.s('3d.visible', false);});} } function show(index) {if (index || index === 0) {this.floors.forEach((i, j) => {if (index === j) {i.s('3d.visible', true);}else {i.s('3d.visible', false);}});}else {this.floors.forEach(i => {i.s('3d.visible', true);});} }第一種方式實(shí)現(xiàn)主要代碼:
getHeatFloor(node, dir, config) {let { width, height } = node.getRect();let rackTall = node.getTall();let s3 = [1, rackTall, height];let floor = new ht.Node();floor.setTag('hotspot');floor.setAnchor3d({x: 0.5,y: 0.5,z: 0.5});floor.s3(s3);floor.s({interactive: true,preventDefaultWhenInteractive: false,'3d.selectable': true,'3d.movable': false,'all.visible': false,[Top + '.visible']: true,[Top + '.opacity']: config.opacity,[Top + '.transparent']: true,[Top + '.reverse.flip']: true,[Top + '.color']: 'rgba(51,255,231,0.10)'});return floor } getHeatFloorInfo(node, dir, ctx, index, size, config) {let { width, height } = node.getRect();let rackTall = node.getTall();let point = node.getPosition3d();let part = 0;let p3, s3;let Top = 'top';if (!dir) {dir = 'z';}// 熱力圖的yz方向與ht的yz方向相反 dir=z代表的是豎直方向if (dir === 'x') {Top = 'left';part = (width / size) * index;p3 = [point[0] - width / 2 + part,point[1] + rackTall / 2,point[2]];// p3 = [point[0] + part, point[1], point[2]];s3 = [1, rackTall, height];}else if (dir === 'y') {Top = 'front';part = (height / size) * index;p3 = [point[0],point[1] + rackTall / 2,point[2] - height / 2 + part];s3 = [width, rackTall, 1];}else if (dir === 'z') {Top = 'top';part = (rackTall / size) * index;p3 = [point[0], point[1] + part, point[2]];s3 = [width, 1, height];}let heatName = this.generateUUID();ht.Default.setImage('heatMap' + heatName, ctx);this.heatFloorInfo.push({img: 'heatMap' + heatName,p3}) } show(index){let info = this.heatFloorInfo[index]this.floor.p3(info.p3)this.floor.s('3d.visible', true);this.floor.s('top.image', info.img);// 手動(dòng)刷新this.floor.iv(); }6.換膚
換膚的實(shí)現(xiàn)原理:根據(jù)不同的場(chǎng)景值去動(dòng)態(tài)修改?ht.graph3d.Graph3dView?的背景色及墻的顏色等。
代碼:
function changeSkin() {let backgroundColor = this.g3d.dm().getBackground(),dark_bg = this.g3d.dm().getDataByTag('dark_skin'),light_bg = this.g3d.dm().getDataByTag('light_skin');if (backgroundColor !== 'rgb(255,255,255)') {this.g3d.dm().setBackground('rgb(255,255,255)');} else {this.g3d.dm().setBackground('rgb(0,0,0)');}dark_bg.s('2d.visible', !dark_bg.s('2d.visible'));dark_bg.s('3d.visible', !dark_bg.s('3d.visible'));light_bg.s('2d.visible', !light_bg.s('2d.visible'));light_bg.s('3d.visible', !light_bg.s('3d.visible'));}? 本篇就介紹到了,目前?ht-thermodynamic.js?還處于測(cè)試階段,待到相對(duì)成熟后再更新該?demo?,有興趣了解更多關(guān)于?2D/3D?可視化的構(gòu)建,可翻閱其他文章的例子,HT?會(huì)給你很多不可思議的東西。
總結(jié)
以上是生活随笔為你收集整理的基于 HTML5 Canvas 的 3D 热力云图效果的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: link引入和@import的区别
- 下一篇: 手把手教你搭建springboot程序