基於 HTML5 Canvas 的 3D 模型貼圖問題

圖撲軟體發表於2017-12-06

之前注意到的一個例子,但是一直沒有沉下心來看這個例子到底有什麼優點,總覺得就是一個 list 列表,也不知道右邊的 3d 場景放兩個節點是要幹嘛,今天突然想起來就仔細地看了一下這個例子的程式碼,實際操作中應該還是有用處的,就跟大家分享一下。

本例地址: http://hightopo.com/guide/guide/core/listview/examples/example_custom.html

實現圖如下,看起來略有點簡陋,但是可以自己天馬心空增加或者更改成你需要的東西:

圖片描述

首先,建立場景,HT 中有一個 BorderPane 皮膚元件是拿來頁面排布的,可以排布 html 標籤,也可以排布 HT 的元件,這裡我們將整個頁面分為三個部分,頂部工具條 toolbar、左側列表 listView 和中間 3d 場景 g3d,再將這個皮膚元件新增進 html body 體中:

borderPane = new ht.widget.BorderPane();//皮膚元件                
toolbar = new ht.widget.Toolbar(); //工具條                    

listView = new ht.widget.ListView(); //列表元件
g3d = new ht.graph3d.Graph3dView();// 3d 元件

borderPane.setTopView(toolbar);//將 toolbar 放置到皮膚中的頂部
borderPane.setLeftView(listView, 350); //將 listView 放置到皮膚中的左側
borderPane.setCenterView(g3d); //將 g3d 放置到皮膚中的中間

borderPane.addToDOM(); //將皮膚元件新增進 body 中

addToDOM 函式是 HT 封裝好的將 HT 元件新增進 body 體中的一個方法,其實現邏輯如下:

addToDOM = function(){   
    var self = this,
          view = self.getView(),//通過 getView 函式獲取元件的底層 div
          style = view.style;
    document.body.appendChild(view); //body 新增孩子 view           
    style.left = '0';
    style.right = '0';
    style.top = '0';
    style.bottom = '0';      
    window.addEventListener('resize', function () { self.iv(); }, false);//視窗大小變化時,立即重新整理元件            
}

我們一個部分一個部分來解析,從最上層的 toolbar 工具條開始,如下:

圖片描述

工具條也是分為三個部分,一是左側的搜尋框,二是中間的分割線,三是右側的點選按鈕。

我們首先向工具條 toolbar 中新增這三個元素,具體新增方法請參考 HT for Web 工具條手冊

toolbar.setItems([//設定工具條元素陣列              
    {
        id: 'text',
        label: 'Search',
        icon: 'images/search.png',
        textField: {
            width: 120
        }
    },
    'separator',
    {
        label: 'Sort by price',
        type: 'toggle',//toggle表示開關按鈕
        selected: true,
        action: function(){
            listView.setSortFunc(this.selected ? sortFunc : null);
        }
    }
]);

接下來向左側的 listView 列表中新增資料,這個資料就是 product.js 中的變數 products,通過遍歷這個陣列變數,將這個陣列中的所有值都填充到 listView 列表中:

圖片描述

products.forEach(function(product){//products 是在product.js 檔案中定義的
    var data = new ht.Data();
    data.a(product);//設定資料 data 的 attr 屬性
    、listView.dm().add(data);//將 data 新增進 listView 的資料容器中
}); 

然後對 listView 列表進行一系列的樣式屬性的設定:行高、背景、icon 圖示、文字提示等等。程式碼如下,解釋都在程式碼中了,還有不懂的請查閱 HT for Web 列表手冊

listView.setRowHeight(50);//設定行高
listView.drawRowBackground = function(g, data, selected, x, y, width, height){//繪製行背景色,預設僅在選中該行時填充選中背景色,可過載自定義
    if(this.isSelected(data)){//選中時
        g.fillStyle = '#87A6CB';
    }
    else if(this.getRowIndex(data) % 2 === 0){//偶數行時
        g.fillStyle = '#F1F4F7';
    }
    else{
        g.fillStyle = '#FAFAFA';
    }
    g.beginPath();
    g.rect(x, y, width, height);
    g.fill();
};
// HT 通過 ht.Default.setImage('name', json) 函式來註冊圖片
ht.Default.setImage('productIcon', {
    width: 50,
    height: 50,
    clip: function(g, width, height) {//clip 用於裁剪繪製區域
        //利用canvas畫筆繪製,實現自定義裁剪任意形狀的效果
        //這裡是將圖片裁剪成圓形
        g.beginPath();
        //x, y, radius, startAngle, endAngle, anticlockwise
        g.arc(width/2, height/2, Math.min(width, height)/2-3, 0, Math.PI * 2, true);
        g.clip();
    },
    comps: [//向量圖形的元件Array陣列,每個陣列物件為一個獨立的元件型別,陣列的順序為元件繪製先後順序
        {
            type: 'image',
            stretch: 'uniform',//圖片始終保持原始寬高比例不變化,並儘量填充滿矩形區域
            rect: [0, 0, 50, 50],//指定元件繪製在向量中的矩形邊界 [x, y, width, height]四個引數方式,分別代表左上角座標x和y,以及寬高width和height
            name: {func: function(data){return data.a('ProductId');}}//圖片的名字為 data.a('ProductId') 返回的值
        }
    ]
});
listView.setIndent(60);//設定indent縮排,該引數一般用於指定圖示的寬度                 
listView.getIcon = function(data){//返回data物件對應的icon圖示,可過載自定義
    return 'productIcon';//這個是前面 ht.Default.setImage 函式註冊過的向量圖形
};  

listView.enableToolTip();//開啟文字提示
listView.getLabel = function(data){//返回data物件顯示的文字,預設返回data.toLabel(),可過載自定義
    return data.a('ProductName') + ' - $' + data.a('UnitPrice').toFixed(2);
};                
listView.getToolTip = function(e){//根據傳入的互動事件,返回文字提示資訊,可過載自定義
    var data = this.getDataAt(e);//傳入邏輯座標點或者互動event事件引數,返回當前點下的資料元素
    if(data){
        return '<span style="color:#3D97D0">ProductId:&nbsp;</span>' + data.a('ProductId') + '<br>' +  
                       '<span style="color:#3D97D0">ProductName:&nbsp;</span>' + data.a('ProductName') + '<br>' +  
                       '<span style="color:#3D97D0">QuantityPerUnit:&nbsp;</span>' + data.a('QuantityPerUnit') + '<br>' + 
                       '<span style="color:#3D97D0">Description:&nbsp;</span>' + data.a('Description');                        
    }
    return null;
};

列表元件中還封裝了一個很方便的函式 setSortFunc,用於設定排序函式,使用者也可以自定義,目前我們希望對這些”商品“進行排序:

sortFunc = function(d1, d2){//自定義排序函式
    return d1.a('UnitPrice') - d2.a('UnitPrice');
};
listView.setSortFunc(sortFunc);//HT 定義的 設定排序函式

因為我們要進行資料的搜尋,就要對資料以及顯示方面進行過濾,因為在資料變化時,HT 無法獲知需要更新,這時候就要我們手動對有顯示變化的部分呼叫更新函式 invalidate 簡寫為 iv。

我們對文字輸入框的鍵盤彈起事件進行事件的監聽,然後判斷我們輸入的值在 listView 列表中是否存在等操作對顯示介面進行過濾:

// 對text文字框進行鍵盤按鍵彈起事件監聽 toolbar.getItemById('text').element.getElement().onkeyup = function(e){
    listView.invalidateModel();//無效模型,最徹底的重新整理方式 “完全重新整理”
};
//如果文字框輸入的值在
listView.setVisibleFunc(function(data){//設定可見過濾器                    
    var text = toolbar.v('text');//getValue(id)根據id獲取對應item元素值,簡寫函式為v(id)
    if(text){              
        return data.a('ProductName').toLowerCase().indexOf(text.toLowerCase()) >= 0;//indexOf()方法返回在型別陣列中可以找到給定元素的第一個索引,如果不存在,則返回-1
    }
    return true;
});

第三個部分,右側 3d 場景,利用的是 HT 的三維元件 ht.graph3d.Graph3dView,然後在 3d 場景上新增兩個節點,作為對照:

//建立兩個節點放到 3d 場景中
var node = new ht.Node();
node.s3(30, 30, 30);//設定三維大小
node.p3(-30, 15, 0);//設定三維座標
node.s('all.color', '#87A6CB');//設定 node 的六個面顏色                
g3d.dm().add(node);//將新建的 node 新增進 3d 場景的資料容器中

var node = new ht.Node();
node.s3(30, 30, 30);
node.p3(30, 15, 0);
node.s('all.color', '#87A6CB');
node.setElevation(15);
g3d.dm().add(node);

g3d.setEye(-100, 100, 80);//設定 3d 場景的眼睛(或Camera)所在位置,預設值為[0, 300, 1000]
g3d.setGridVisible(true);//設定是否顯示網格
g3d.setGridColor('#F1F4F7');//設定網格線顏色
整個場景建立完畢,接下來就是將 listView 中顯示的 icon 圖示拖拽到 3d 中的節點上,作為貼圖。列表元件中封裝了一個拖拽的功能 handleDragAndDrop,這個函式有兩個引數,event 互動事件和 state 當前狀態,我們對拖拽事件的不同狀態進行不同的處理:

listView.handleDragAndDrop = function(e, state){//該函式預設為空,若該函式被過載,則pan平移元件功能將被關閉
    if(state === 'prepare'){//state當前狀態,先後會有prepare-begin-between-end四種過程
        var data = listView.getDataAt(e);//傳入邏輯座標點或者互動event事件引數,返回當前點下的資料元素
        listView.sm().ss(data);//設定選中當前事件所在的資料元素
        if(dragImage && dragImage.parentNode){
            document.body.removeChild(dragImage);
        }
        dragImage = ht.Default.toCanvas('productIcon', 30, 30, 'uniform', data);
        // toCanvas(image, width, height, stretch, data, view, color)將圖片轉換成Canvas物件
        productId = data.a('ProductId');
    }
    else if(state === 'begin'){
        if(dragImage){
            var pagePoint = ht.Default.getPagePoint(e);//返回page屬性座標
            dragImage.style.left = pagePoint.x - dragImage.width/2 + 'px';//實時更新拖拽時的圖示的位置
            dragImage.style.top = pagePoint.y - dragImage.height/2 + 'px';
            document.body.appendChild(dragImage);//在 html body 體中新增這個拖拽的圖片
        }
    }
    else if(state === 'between'){
        if(dragImage){
            var pagePoint = ht.Default.getPagePoint(e);//返回page屬性座標
            dragImage.style.left = pagePoint.x - dragImage.width/2 + 'px';
            dragImage.style.top = pagePoint.y - dragImage.height/2 + 'px';   

            if(ht.Default.containedInView(e, g3d)){//判斷互動事件所處位置是否在View元件之上,一般用於Drog And Drop的拖拽操作判斷
            //這邊做了兩個判斷,一個是滑鼠在拖拽的時候未鬆開,一個是滑鼠拖拽的時候鬆開了。
                if(lastFaceInfo){//滑鼠未鬆開的情況下,貼圖顯示舊值
                //data.face 預設值為front,圖示在3D下的朝向,可取值left|right|top|bottom|front|back|center
                    lastFaceInfo.data.s(lastFaceInfo.face + '.image', lastFaceInfo.oldValue);
                    lastFaceInfo = null;
                }
                //滑鼠鬆開時,將新值賦給這個面
                var faceInfo = g3d.getHitFaceInfo(e);//獲取滑鼠所在面資訊
                if(faceInfo){
                    faceInfo.oldValue = faceInfo.data.s(faceInfo.face + '.image');//獲取面的“老值”
                    faceInfo.data.s(faceInfo.face + '.image', productId);//front/back/top/bottom/left/right.image 設定這些面的貼圖
                    lastFaceInfo = faceInfo;
                }
            }
        }
    }
    else{//拖拽結束之後,所有值都回到初始值
        if(dragImage){//有從列表中拖拽圖片
            if(lastFaceInfo){//有賦“圖片”到 3d 中的節點上
                lastFaceInfo.data.s(lastFaceInfo.face + '.image', lastFaceInfo.oldValue);
               lastFaceInfo = null;
           }                             
           if(ht.Default.containedInView(e, g3d)){                               
               var faceInfo = g3d.getHitFaceInfo(e);
               if(faceInfo){
                   faceInfo.data.s(faceInfo.face + '.image', productId);
               }
           }                                                        
           if(dragImage.parentNode){
               document.body.removeChild(dragImage);
           }
           dragImage = null;  
           productId = null;
       }
   }
}; 

相關文章