基於 webGL 的元素周期表 3D 交互展示


前言

之前在網上看到別人寫的有關元素周期表的文章,深深的勾起了一波回憶,記憶里初中時期背的“氫氦鋰鈹硼,碳氮氧氟氖,鈉鎂鋁硅磷,硫氯氬鉀鈣”、“養(氧)龜(硅)鋁鐵蓋(鈣),哪(鈉)家(鉀)沒(鎂)青(氫)菜(鈦)”,高中時期記的質量守恆、元素守恆、原子守恆、電子守恆,時間過的飛快,轉眼我們都已經這么大了。。。

現在我用 HT 來實現它,HT 有 2D 拓撲和 3D 模型場景,兩種形式我都實現了,話不多說,先看效果圖。

界面展示

整個頁面由 HT UI 組件構成,使用 ht.ui.TabLayout 標簽頁布局組件,進行 23D 界面的分別展示。

2D界面:整體是一個 ht.ui.SplitLayout 分割組件(左右分割),左邊使用 ht.ui.HTView 包裝了 GraphView 拓撲圖組件,右邊是一個 ht.ui.Form 表單組件。

3D界面:整體是一個 ht.ui.SplitLayout 分割組件(上下分割),上邊添加了 ht.ui.HBoxLayout 構成的按鈕組,下邊是使用 ht.ui.HTView 包裝了 Graph3dView 場景。

 

demo 地址:http://www.hightopo.com/demo/elementTable/index.html

2D 界面代碼分析

拓撲圖組件

先來說左邊的拓撲圖組件,ht.graph.GraphView 是 HT 框架中 2D 功能最豐富的組件,具有基本圖形的呈現和編輯功能,拓撲節點連線及自動布局功能,電力和電信等行業預定義對象,具有動畫渲染等特效, 因此其應用面很廣泛,可作為監控領域的繪圖工具和人機界面,可作為一般性的圖形化編輯工具,可擴展成工作流和組織圖等企業應用。

拓撲圖中展示的 118 個元素,每一個都是 ht.Node 拓撲節點,默認的節點展示是一個小電腦樣式,在這里我們通過 setImage 設置節點顯示的圖片信息,如下圖:

矢量圖通過點、線和多邊形來描述圖形,因此在無限放大和縮小圖片的情況下依然能保持一致的精確度。上圖就是一張矢量圖,由 1 個矩形和 6 個文字組成,任意縮放不失真,大家可以訪問 demo 地址,通過滾輪來縮放拓撲圖進行體驗,具體矢量圖的繪制請參考矢量手冊

肯定有人會有疑問,118 種元素,是否要繪制 118 張矢量圖,感覺稍微還能接受,如果是成千上萬呢,那么人會累趴下的。不用怕,HT 幫我們解決了這個問題,對繪制的矢量圖進行數據綁定,將繪制內容的屬性綁定到節點的屬性上,應用中通過更新節點對應屬性,圖形界面就會自動刷新,達到實時顯示數據的效果,比如我的這張矢量圖,我將 6 個元素屬性文本內容和字體顏色以及矩形背景色都進行了數據綁定,綁定好之后我只需要通過  node.a('background', '#FEB64D') 就可以修改矩形的背景色(backgrouond 是矩形背景色綁定的屬性),具體數據綁定請參考數據綁定手冊

既然說到了數據綁定,我們就先看下顯示元素分類的功能,如下圖對比,節點樣式的變化不是通過重新 setImage 設置另一張矢量圖,而是修改原矢量中綁定的樣式屬性。根據元素所屬類別,修改矢量圖的矩形背景色、元素中文名文本顏色。切換狀態的按鈕是 ht.ui.ToggleButton 開關按鈕,擁有“0/1”兩種狀態的切換,通過監聽按鈕是否選中,來切換元素周期表樣式。

 1 toggle.on('p:selected', e => {
 2     if (e.newValue) {
 3         this.htView.legend.s('2d.visible', true); // 顯示類別圖例
 4         this.htView.addClassification(); // 展示分類
 5     }
 6     else {
 7         this.htView.legend.s('2d.visible', false); // 隱藏類別圖例
 8         this.htView.initElements(); // 原始樣式
 9     }
10 });

 

元素類別圖例也是一個 ht.Node 節點,同樣是繪制的矢量,它從一開始就在圖紙中, node.s('2d.visible', false) 設置為不可見,要展示分類時,再設置為 true 顯示。

表單面板

右邊的表單面板有 5 行,第 2 行就是上邊提到的顯示分類功能,第 3 行是一個文本輸入框,用來獲取元素序數,限制了只能輸入數字,還增加了輸入數的驗證,只能輸入 1~118 。

代碼如下:

 1 let textField = new ht.ui.TextField();
 2 textField.setFormDataName('textField'); // 設置在表單中的名稱
 3 textField.setPlaceholder('請輸入查詢的元素序數!');
 4 textField.setMaskRe(/\d/); // 限制只能輸入數字
 5 textField.setInstant(true); // 開啟即時模式,值改變就派發屬性改變事件
 6 textField.on('p:value', (e) => { // 監聽值改變事件
 7     let value = e.newValue;
 8     if (value > 118) {
 9         textField.setErrorMessage('只有 1 ~ 118 號元素喲!', {
10             placements: ['top']
11         });
12     }
13     else { textField.setErrorMessage(null); }
14 });

第 4 行是一個文本區域 ht.ui.TextArea,用來展示查詢的元素信息。

第 5 行是一組按鈕,用來提交查詢數據和重置表單信息。

3D 界面代碼分析

按鈕組

上邊是一個 ht.ui.HBoxLayout 橫向布局器,hbox 中添加了 4 個按鈕,來進行 3D 形態轉換。

按鈕支持圖標和文字,提供 normal、hover、active、disabled 四種狀態,按鈕生成代碼:

 1 createButton(text) {
 2     let button = new ht.ui.Button();
 3     button.setBorder(null);
 4     button.setHoverBorder(null);
 5     button.setActiveBorder(null);
 6     button.setBackground(new ht.ui.drawable.ColorDrawable('rgba(37,115,194,0.6)', 4)); // normal 背景
 7     button.setHoverBackground(new ht.ui.drawable.ColorDrawable('rgba(10,92,173,0.50)', 4)); // hover 背景
 8     button.setActiveBackground(new ht.ui.drawable.ColorDrawable('rgba(15,132,250,0.6)', 4)); // active 背景
 9     button.setText(text);
10     button.setTextColor('rgb(0, 211, 255)');
11     button.setHoverTextColor('rgb(0, 211, 255)');
12     button.setActiveTextColor('rgb(0, 211, 255)');
13 
14     return button;
15 }

通過 button.on('click', e => { // 切換函數 }) 來監聽點擊事件。

3D 場景

下邊是 ht.graph3d.Graph3dView,通過對 WebGL 底層技術的封裝,與 HT 其他組件一樣, 基於 HT 統一的 DataModel 數據模型來驅動圖形顯示,極大降低了 3D 圖形技術開發的門檻,在熟悉 HT 數據模型基礎上, 一般程序員只需要 1 個小時的學習即可上手 3D 圖形開發。

元素在 3D 場景顯示為一個面片,對面片進行 2D 時做好的矢量貼圖,同樣通過修改節點屬性,來控制顯示樣式。

1 node.s({
2     'shape3d': 'billboard', // 設置節點類型為‘billboard’公告板
3     'shape3d.image': 'symbols/元素2.json', // 設置面片貼圖
4     'shape3d.reverse.flip': true, // 設置反面是否顯示正面內容
5     'shape3d.image.cache': true, // 進行貼圖緩存
6     'shape3d.fixSizeOnScreen': false, // 設置是否固定保持屏幕大小,不隨縮放而變化
7     'select.brightness': 1 // 設置選中亮度為 1
8 });

接下來說幾種旋轉變化,dm 是 DataModel 即綁定的數據容器,datasMap 用來存放元素變化前后的位置信息,用於動畫驅動時使用。

1. 隨機打亂:設置一組空間范圍值,生成范圍內的(x,y,z)隨機值,用以設置節點位置。

 1 let dm = this.dm,
 2     datasMap = {};
 3 
 4 dm.each(data => {
 5     let x = Math.random() * 2000 - 1000; // 獲取隨機 x
 6     let y = Math.random() * 2000 - 1000; // 獲取隨機 y
 7     let z = Math.random() * 500 - 250; // 獲取隨機 z
 8     
 9     let position = data.getPosition3d(),
10         px = position[0],
11         py = position[1],
12         pz = position[2];
13 
14     datasMap[data] = {
15         x: x,
16         y: y,
17         z: z,
18         px: px,
19         py: py,
20         pz: pz
21     };
22 });

 

2. 球形環繞:繞球面螺旋線生成點坐標。

 1 let dm = this.dm,
 2     datas = dm.getDatas(),
 3     datasMap = {};
 4 
 5 let r = 400,
 6     theta, phi;
 7 
 8 for (let i = 0; i < 118; i++) { 
 9     let data = datas.get(i);
10     theta = (i + 1) / 118 * 180; // 獲取球系坐標
11     phi = (i + 1) / 118 * 360 * 10; // 獲取球系坐標
12    // 球系坐標轉換為 HT 三維坐標
13     let z = r * Math.sin(theta * Math.PI / 180) * Math.cos(phi * Math.PI / 180),
14         x = r * Math.sin(theta * Math.PI / 180) * Math.sin(phi * Math.PI / 180),
15         y = r * Math.cos(theta * Math.PI / 180);
16 
17     let position = data.getPosition3d(),
18     px = position[0],
19     py = position[1],
20     pz = position[2];
21 
22     datasMap[data] = {
23         x: x,
24         y: y,
25         z: z,
26         px: px,
27         py: py,
28         pz: pz
29     };
30 }

3. 環形圍繞:設置一個環繞半徑、起始高度,以固定角度旋轉,每次降低節點的設置高度。

 1 let dm = this.dm,
 2     datasMap = {},
 3     datas = dm.getDatas(),
 4     radius = 400,
 5     angle = 18,
 6     num = 360 / angle;
 7 
 8 let y = 300, 
 9     count = 0;
10 for (let i = 0; i < 6; i++) {
11     for (let j = 0; j < num; j++) {
12         let data = datas.get(count),
13             radian = Math.PI / 180 * j * angle;
14 
15         if (!data) break;
16         count++;
17 
18         let x = radius * Math.cos(radian),
19             z = radius * Math.sin(radian); 
20         
21         let position = data.p3(),
22             px = position[0],
23             py = position[1],
24             pz = position[2];
25 
26         datasMap[data] = {
27             x: x,
28             y: y,
29             z: z,
30             px: px,
31             py: py,
32             pz: pz
33         };                
34         y -= 6;
35     }
36 }

4. 復原:根據記錄的元素的行數和列數,計算元素節點的 xy 值,z 值固定。

 1 let dm = this.dm,
 2     datasMap = {};
 3 
 4 dm.each(data => {
 5     let index = data.a('index'),
 6         row = data.a('row'),
 7         col = data.a('col');
 8 
 9     let position = data.getPosition3d(),
10         px = position[0],
11         py = position[1],
12         pz = position[2];
13 
14     datasMap[data] = {
15         index: index,
16         row: row,
17         col: col,
18         px: px,
19         py: py,
20         pz: pz
21     };
22 });

 

5. 元素切換狀態時的動畫:詳情了解入門手冊動畫

 1 ht.Default.startAnim({
 2     duration: 1500,
 3     easing: function(t) { 
 4         return t * t;
 5     },
 6     action: function(v, t) {
 7         dm.each(data => {
 8             let info = datasMap[data],
 9                 x = info.x,
10                 y = info.y,
11                 z = info.z,
12                 px = info.px,
13                 py = info.py,
14                 pz = info.pz;
15 
16             data.p3(px + v * (x - px), py + v * (y - py), pz + v * (z - pz)); // 移動元素位置
17             data.lookAt([0, y, 0], 'back'); // 調整元素朝向
18         });
19     }
20 });

 

總結

再次看過元素周期表,你是否想起化學課上滿黑板的化學方程式,是否想起了化學實驗課酒精燈的燃燒,是否還記得實驗操作流程、儀器的正確擺放。

再來操作一次:http://www.hightopo.com/demo/chemistry/index.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM