不管在任何領域,只要能讓非程序員能通過拖拽來實現 2D 和 3D 的設計圖就是很牛的,今天我們不需要 3dMaxs 等設計軟體,直接用 HT 就能自己寫出一個 2D 3D 編輯器,實現這個功能我覺得成就感還是爆棚的,哈哈!只要你會想,能做,就能根據這個編輯器延展成 big thing!

本例地址:hightopo.com/demo/drag-

下面是實現效果圖:

我們首先將所有需要用到的 json 文件作為矢量圖輸出,矢量圖的好處是組件上的圖元縮放都不會失真,並且不再需要為 Retina 顯示屏提供不同尺寸的圖片, 在 devicePixelRatio 多樣化的移動時代, 要實現完美的跨平台,矢量可能是的最低成本的解決方案。

HT 通過 ht.Default.setImage 函數來註冊圖片,可以是 base64、jpg、 png 以及 json 格式的圖片:

ht.Default.setImage(edit, images/default.json);
ht.Default.setImage(shape, images/edit.json);
ht.Default.setImage(edge, images/edge.json);
ht.Default.setImage(circle, images/circle.json);
ht.Default.setImage(roundRect, images/roundRect.json);
ht.Default.setImage(rect, images/rect.json);

這邊我註冊的是頂部工具條的 6 個圖片,分別為「編輯」、「不規則圖形」、「圓」、「圓角矩形」、「矩形」以及「連線」,功能如其名。主要操作:點擊工具條的任意一個圖標,在工具條下的空白處拖動滑鼠,即可實現繪圖。

那麼接下來的步驟就是創建「工具條」,HT 封裝了工具條的組件 ht.widget.Toolbar 在這個函數的參數中填入工具條中的元素,具體操作方法請看 HT for Web 工具條手冊,這邊值得注意的一個點是,groupId 是將一個類型的元素分組,分組的好處是在我們選中這個組中的任意一個元素的時候,其他的元素都不選中,就能造成「單選」的效果:

toolbar = new ht.widget.Toolbar();
toolbar.addItem(createItem(edit, edit, 編輯, [ new ht.graph.EditInteractor(graphView)])); // 這邊最後一個參數數組可放置多個交互器,具體定義請參見 HT for Web 入門手冊
// addItem(item, index)可在指定 index 位置插入新元素,index 為空代表插入到最後。
toolbar.addItem(createItem(shape, shape, 不規則圖形, [ new CreateShapeInteractor(graphView)]));
toolbar.addItem(createItem(circle, circle, 圓, [ new CreateNodeInteractor(graphView)]));
toolbar.addItem(createItem(roundRect, roundRect, 圓角矩形, [ new CreateNodeInteractor(graphView)]));
toolbar.addItem(createItem(rect, rect, 矩形, [ new CreateNodeInteractor(graphView)]));
toolbar.addItem(createItem(edge, edge, 連線, [ new CreateEdgeInteractor(graphView)]));
toolbar.getItemById(edit).selected = true; // 默認選中「編輯」
toolbar.getSelectBackground = function(){ // 重載自定義選中背景顏色
return #eee;
}

在上面用到的 addItem 函數是向 ht.widget.Toolbar 工具條中添加元素,添加的元素是從 createItem 函數中傳回來的元素,我們在這個函數中利用了 vector 矢量創造了一個矩形和一張圖片的結合體,我們將之前註冊好的矢量圖傳給這個結合體中的「圖片」,然後通過控制這個圖片的「渲染顏色」,來過濾工具條選中和非選中狀態的顏色:

function createItem(id, iconName, toolTip, interactorsArr){
var item = {
id: id, // 工具條元素的唯一標示,如果設置可通過 getItemById 獲取
unfocusable: true, // 工具條元素是否不可獲取焦點,默認滑鼠滑過時會顯示一個矩形邊框,可設置為 true 關閉此效果
icon: iconName, // 工具條元素的圖標
toolTip: toolTip, // 工具條元素的文字提示
groupId: bar // 對工具條元素進行分組,同一個分組內的元素選中會自動出現互斥效果
};
var json = ht.Default.getImage(iconName);
var width = json ? json.width : 16;
var height = json ? json.height : 16;

item.icon = {
width: width + 8,
height: height + 8,
fitSize: json ? json.fitSize : false,
comps: [
{
type: rect,//組件類型
rect: [0, 0, width + 8, height + 8] // 指定組件繪製在矢量中的矩形邊界
},
{
type: image,
name: iconName,
color: { // 渲染顏色,HT系統會自動採用該顏色對圖片內容進行渲染
func: (data, view) => {
return #000
}
}
}
]
};

item.action = function(){ // 函數類型,工具條元素被點擊時調用
for(var i = 0; i < toolbar.getItems().length; i++){
toolbar.getItems()[i].icon.comps[1].color = #000;
}
item.icon.comps[1].color = #1E90FF;
graphView.setInteractors(interactorsArr); // 組合多個交互器
graphView.sm().clearSelection(); // 每次工具條有點擊的時候就清空所有的選中
}

return item;
}

接著利用 HT 封裝的面板組件 [ht.widget.BorderPane](邊框面板手冊 - HT for Web) 將整個界面分成兩個部分:頂部和底部。我們又利用 HT 封裝的 [ht.widget.SplitView](分割組件手冊 - HT for Web) 分割組件將底部分為上下兩個部分,最後將這個外邊框 borderPane 添加進 body 體中:

splitView = new ht.widget.SplitView(graphView, g3d, v, 0.5);
borderPane = new ht.widget.BorderPane();
borderPane.setTopView(toolbar);
borderPane.setCenterView(splitView);
borderPane.addToDOM();

整個場景都製作完畢,接著就是功能部分。我們把製作「不規則圖形」作為一個單獨的部分放到 CreateShapeInteractor.js 中,製作「圓」、「圓角矩形」以及「矩形」三個部分分為一個部分放到 CreateNodeInteractor.js 中,將「連線」分為一個部分放到 CreateEdgeInteractor.js 中,接下來我們將對這三個 js 文件一個個解析。

這三個 js 文件的共同點是通過 HT 封裝的繼承函數 ht.Default.def 繼承並創建新的類,這三個類我們在前面的代碼中是有提到的: CreateShapeInteractor、CreateNodeInteractor 以及 CreateEdgeInteractor 類,都大同小異,我們這裡重點解析 CreateNodeInteractor.js 文件。

首先就是要聲明定義一個 CreateNodeInteractor 類,就是這三個部分:

var CreateNodeInteractor = function (graphView) {
CreateNodeInteractor.superClass.constructor.call(this, graphView);
};
ht.Default.def(CreateNodeInteractor, ht.graph.Interactor, { // 自定義類,第一個參數為類名,第二個參數為繼承的類,第三個參數為此類的方法
// 這邊重新繪製這個類的方法
}

接著就是向這個類中添加我們需要的功能,主要的功能是「滑鼠點擊事件的觸發」以及「觸摸屏幕事件的觸發」,我們通過對事件的監聽來繪製圖形,首先就是判斷滑鼠左鍵或者觸屏是否點擊:

handle_touchstart: function (e) {//觸屏 開始點擊
ht.Default.preventDefault(e);//阻止所有的默認交互事件
if (ht.Default.isLeftButton(e)) {//滑鼠左鍵是否點擊
this._graphView.setFocus(e);//設置焦點
this.p1 = this._graphView.lp(e);//獲取當前邏輯坐標點
this.startDragging(e);//調用 startDragging 開始拖拽函數
}
}

然後對滑鼠彈起或者觸屏是否結束進行事件的判斷,並直接生成一個 ht.Node 節點。HT 把單純的點擊事件和拖拽事件分為兩種命名格式,單純的點擊事件為 handle_* 方法,拖拽事件是 handleWindow* 方法。上面的代碼就是從點擊工具條的能觸發 CreateNodeInteractor 類的元素開始,到放到界面中生成圖元結束。並沒有拖拽的過程,會有一個默認的大小:

HT 默認調用 ht.graph.DefaultInteractor 事件,裡面有一系列的操作,我們現在要做的拖拽跟這個有衝突,所以在前面我們先將這個默認的事件阻止,獲取滑鼠點下的第一個點的邏輯坐標和第二個點的邏輯坐標,根據這兩個坐標的點生成一個矩形,然後開始繪製節點:

handleWindowTouchMove: function(e) {
ht.Default.preventDefault(e); // 阻止事件的默認行為,常用於屏蔽觸屏上默認 DoubleTap 縮放等行為
if (!this.p1) {
return;
}
this.p2 = this._graphView.lp(e);
const rect = ht.Default.unionPoint(this.p1, this.p2); // 將 p1 和 p2 兩個點組合成一個矩形
if (this.node) {
this.node.setRect(rect);
}
else {
if (!rect.width || !rect.height) {
return;
}
this._graphView.dm().beginTransaction(); // 類似資料庫里開啟事務,從 beginTransaction()到 endTransaction()之間所有的修改可被一次性撤銷或重做
this.createNode(rect, false); // 調用 createNode 函數
}
}

然後在拖拽結束的時候做結束繪製圖形的操作,這裡我是直接設置繪製結束後就將工具條選中「編輯」的元素:

handleWindowTouchEnd: function(e) {
ht.Default.preventDefault(e);
this._graphView.dm().endTransaction(); // 類似資料庫里結束事務,從beginTransaction()到endTransaction()之間所有的修改可被一次性撤銷或重做
if (!this.node && this.p1) {
this.createNode({
x: this.p1.x - 25,
y: this.p1.y - 25,
width: 50,
height: 50
}, true);
}
var continuousCreating = false;
if (!continuousCreating) {
for(var i = 0; i < toolbar.getItems().length; i++){
toolbar.getItems()[i].selected = false;
toolbar.getItems()[i].icon.comps[1].color = #000;
}
toolbar.getItemById(edit).selected = true;
toolbar.getItemById(edit).icon.comps[1].color = #1E90FF;
borderPane.iv();
this._graphView.setEditable(true);
this._graphView.sm().ss(this.node);
}
else {
this.node = this.p1 = this.p2 = null;
}
}

最後,我們只要知道如何繪製圖元就好了,在 HT 中,基礎的圖元都可以通過設置樣式中的 shape 或者 shape3d 來生成不同的圖元,我們這邊就是通過這種途徑,如果想要在界面中生成複雜圖形,如:機櫃模型,可以參考這篇文章:cnblogs.com/xhload3d/p/

createNode: function(rect, click) {
// create instance
if (ht.Default.isFunction(this.type)) {
this.node = this.type(rect, click);
}
else {
this.node = new ht.Node();
}
this.node.setTall(50); //為 3D 圖形做準備,設置其厚度,才會有立體感
if(toolbar.getItemById(circle).selected){//如果工具條的 『circle』 被選中
this.node.s({ // 設置 style 樣式
"shape": "oval", // 橢圓形,為空時顯示為圖片,可設置多邊形類型參見入門手冊
"shape.background": "#D8D8D8", // 多邊形類型圖元背景
"shape.border.width": 1, // 多邊形類型圖元邊框寬度
"shape.border.color": "#979797", // 多邊形類型圖元邊框顏色
"shape3d": "sphere" // 為空時顯示為六面立方體,其他可選值為box|sphere|cylinder|cone|torus|star|rect|roundRect|triangle|rightTriangle|parallelogram|trapezoid
});
}else if(toolbar.getItemById(roundRect).selected){
this.node.s({
"shape": "roundRect", // 圓角矩形
"shape.background": "#D8D8D8",
"shape.border.width": 1,
"shape.border.color": "#979797",
"shape3d": "roundRect"
});
}else if(toolbar.getItemById(rect).selected){
this.node.s({
"shape": "rect", // 矩形
"shape.background": "#D8D8D8",
"shape.border.width": 1,
"shape.border.color": "#979797",
"shape3d": "rect"
});
}
// set bounds
if (click) {
this.node.setPosition(rect.x + rect.width / 2, rect.y + rect.height / 2); // 設置 node 的坐標點
}
else {
this.node.setRect(rect); // 設置 node 的外矩形邊框大小
}
// add to data model
this._graphView.dm().add(this.node); // 將這個 node 添加進數據容器 DataModel 中
}

到此,創建 ht.Node 節點的聲明全部結束,大家可以根據自己的想像創建你想要的編輯器!


推薦閱讀:
相关文章