在現在前端圈大行其道的 React 和 Vue 中,可復用的組件可能是他們大受歡迎的原因之一,
在 HT 的產品中也有組件的概念,不過在 HT 中組件的開發是依托於 HTML5 Canvas 的技術去實現的,
也就是說如果你有過使用 Canvas 的開發經驗你就可以來封裝自己的組件。
下面我以一個進度環為例,來探究一下如何使用ht.js
封裝出一個拓撲組件。
效果圖
代碼實現
前置知識
除了
HT
預定義的組件類型外,用戶還可以自定義擴展類型,自定義有兩種方式:
- 直接將
type
值設置成繪制函數:function(g, rect, comp, data, view){}
- 通過
ht.Default.setCompType(name, funtion(g, rect, comp, data, view){})
注冊組件類型,矢量type
值設置成相應的注冊名
在這里我選用第一種通過形如
這樣的方式完成組件的聲明,那么 function(g, rect, comp, data, view) { }
中的內容就是我們接下來需要關注的了
准備工作
-
抽象並聲明出幾個 Coding 中需要的變量
- 進度百分比
progressPercentage {百分比}
- 圓環漸變色
linearOuter {顏色數組}
- 內圓漸變色
linearInner {顏色數組}
- 字體縮放比例
fontScale {數字}
- 顯示原始值
showOrigin {布爾}
- 進度條樣式
progressLineCap {線帽樣式}
- 進度百分比
-
變量的聲明和賦值了
var x = rect.x; var y = rect.y; var rectWidth = rect.width; var rectHeight = rect.height; var width = rectWidth < rectHeight ? rectWidth : rectHeight; var progressPercentage = parseFloat((data.a('progressPercentage') * 100).toFixed(10)); var fontScale = data.a('fontScale'); var showOrigin = data.a('showOrigin'); var backgroundColor = data.a('backgroundColor'); var progressLineCap = data.a('progressLineCap'); var fontSize = 16; // 字體大小 var posX = x + rectWidth / 2; // 圓心 x 坐標 var posY = y + rectHeight / 2; // 圓心 y 坐標 var circleLineWidth = width / 10; // 圓環線寬 var circleRadius = (width - circleLineWidth) / 2; // 圓環半徑 var circleAngle = {sAngle: 0, eAngle: 2 * Math.PI}; // 繪制背景圓和圓環內圓所需的角度 var proStartAngel = Math.PI; // 進度環起始角度 var proEndAngel = proStartAngel + ((Math.PI * 2) / 100) * progressPercentage; // 進度環結束角度
-
創建漸變色樣式
var grd = context.createLinearGradient(x1, y1, x2, y2); grd.addColorStop(0, 'red'); grd.addColorStop(1, 'blue');
在 Canvas 中的漸變色是按照如上方式來創建的,但是在一個組件中去如果一個一個去添加顯然是去組件的理念是背道而馳的,所以我選擇封裝一個函數根據顏色數組中的各個顏色來生成漸變色樣式
// 創建漸變色樣式函數 function addCreateLinear(colorsArr) { var linear = rectWidth < rectHeight ? g.createLinearGradient(x, posY - width / 2, width, posY + width / 2) : g.createLinearGradient(posX - width / 2, y, posX + width / 2, width); var len = colorsArr.length; for (var key in colorsArr) { linear.addColorStop((+key + 1) / len, colorsArr[key]); } return linear; } // 創建漸變填充顏色 var linearOuter = addCreateLinear(data.a('linearOuter')); var linearInner = addCreateLinear(data.a('linearInner'));
開始 Coding
准備工作結束后下面就是 Canvas 的時間了
-
繪制背景圓
g.beginPath(); g.arc(posX, posY, circleRadius, circleAngle.sAngle, circleAngle.eAngle); g.closePath(); g.fillStyle = backgroundColor; g.fill(); g.lineWidth = circleLineWidth; g.strokeStyle = backgroundColor; g.stroke();
-
繪制進度環
g.beginPath(); g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel); g.strokeStyle = linearOuter; g.lineWidth = circleLineWidth; g.lineCap = progressLineCap; if (progressPercentage !== 0) g.stroke();
-
繪制中心圓
g.beginPath(); g.fillStyle = linearInner; g.arc(posX, posY, circleRadius - circleLineWidth / 2 - 1, 0, Math.PI * 2, false); g.strokeStyle = '#0A2E44'; g.fill(); g.lineWidth = 2; g.stroke();
-
繪制文字
g.fillStyle = 'white'; g.textAlign = 'center'; g.font = fontSize + 'px Arial'; g.translate(posX * (1 - fontScale), posY * (1 - fontScale)); g.scale(fontScale, fontScale); showOrigin ? g.fillText(progressPercentage / 100, posX, posY + fontSize / 3) : g.fillText(progressPercentage + '%', posX, posY + fontSize / 3);
最后通過簡單的配置就可以在網頁上呈現出這個進度環了
var dataModel = new ht.DataModel(); var graphView = new ht.graph.GraphView(dataModel); var circle1 = new ht.Node(); circle1.setPosition(150, 150); circle1.setSize(200, 200); circle1.setImage('circle-progress-bar'); circle1.a({ progressPercentage: 0.48, linearOuter: ['#26a67b', '#0474d6'], linearInner: ['#004e92', '#000000'], fontScale: 1, showOrigin: true, progressLineCap: 'butt', backgroundColor: 'rgb(61,61,61)' }); dataModel.add(circle1); // 這次多生成幾個 不過代碼相似 在此就不贅述了
完整代碼如下
ht.Default.setImage('circle-progress-bar', { width: 100, height: 100, comps: [ { type: function(g, rect, comp, data, view) { // 獲取屬性值 var x = rect.x; var y = rect.y; var rectWidth = rect.width; var rectHeight = rect.height; var width = rectWidth < rectHeight ? rectWidth : rectHeight; var progressPercentage = parseFloat((data.a('progressPercentage') * 100).toFixed(10)); var fontScale = data.a('fontScale'); var showOrigin = data.a('showOrigin'); var backgroundColor = data.a('backgroundColor'); var progressLineCap = data.a('progressLineCap'); var fontSize = 16; // 定義屬性值 var posX = x + rectWidth / 2; var posY = y + rectHeight / 2; var circleLineWidth = width / 10; var circleRadius = (width - circleLineWidth) / 2; var circleAngle = { sAngle: 0, eAngle: 2 * Math.PI }; var proStartAngel = Math.PI; var proEndAngel = proStartAngel + ((Math.PI * 2) / 100) * progressPercentage; // 創建漸變背景色 function addCreateLinear(colorsArr) { var linear = rectWidth < rectHeight ? g.createLinearGradient(x, posY - width / 2, width, posY + width / 2) : g.createLinearGradient(posX - width / 2, y, posX + width / 2, width); var len = colorsArr.length; colorsArr.forEach(function(item, index) { linear.addColorStop((index + 1) / len, item); }); return linear; } // 創建漸變填充顏色 var linearOuter = addCreateLinear(data.a('linearOuter')); var linearInner = addCreateLinear(data.a('linearInner')); // 0.保存繪制前狀態 g.save(); // 1.背景圓 g.beginPath(); g.arc(posX, posY, circleRadius, circleAngle.sAngle, circleAngle.eAngle); g.closePath(); g.fillStyle = backgroundColor; g.fill(); g.lineWidth = circleLineWidth; g.strokeStyle = backgroundColor; g.stroke(); // 2.進度環 g.beginPath(); g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel); g.strokeStyle = linearOuter; g.lineWidth = circleLineWidth; g.lineCap = progressLineCap; if (progressPercentage !== 0) g.stroke(); // 3.繪制中心圓 g.beginPath(); g.fillStyle = linearInner; g.arc(posX, posY, circleRadius - circleLineWidth / 2 - 1, 0, Math.PI * 2, false); g.strokeStyle = '#0A2E44'; g.fill(); g.lineWidth = 2; g.stroke(); // 4.繪制文字 g.fillStyle = 'white'; g.textAlign = 'center'; g.font = fontSize + 'px Arial'; g.translate(posX * (1 - fontScale), posY * (1 - fontScale)); g.scale(fontScale, fontScale); showOrigin ? g.fillText(progressPercentage / 100, posX, posY + fontSize / 3) : g.fillText(progressPercentage + '%', posX, posY + fontSize / 3); // 5.恢復繪制前狀態 g.restore(); } } ] });
幾點心得
聲明屬性
在這個部分有幾點可供參考
-
使用小駝峰對屬性進行命名,並且少用縮寫盡量語義化
舉個栗子:
fontScale
字體縮放比例progressPercentage
進度百分比
-
屬性值類型的選擇也要盡量貼合屬性的含義
舉個栗子:
- 一個存儲着幾個顏色值字符串的數組,用顏色數組就比單純的數組更為貼切
- 一個表示畫筆線帽種類的字符串,用線帽樣式就比字符轉更為貼切
使用屬性
由於進度環是一個圓形的組件,那么在這里有兩點供參考
-
當組件的
rect.width
和rect.height
不相等的時候我們需要自己來設定一個 width,讓圓在這個以 width 為邊的正方形中繪制,而 width 的值就是
rect.width
和rect.height
中較短的一邊,而這么做的理由是這樣繪制圓自適應性能力會更好,並且圓心也直會在
(rect.width/2, rect.height/2)
這一點上。var rectWidth = rect.width; var rectHeight = rect.height; var width = rectWidth < rectHeight ? rectWidth : rectHeight;
-
由於我們自己設定了一個 width,那么在設置漸變顏色的參數上就需要注意一下了。
當 rect.width 不等於 rect.height 的時候。
如果按照
g.createLinearGradient(0, 0, rect.width, rect.height)
設置漸變色就會出現下面的效果,右下方的藍色不見了。不過如果按照如下代碼的方式設置漸變色就會出現下面的效果就會出現預期的效果了。
var posX = rectWidth / 2; var posY = rectHeight / 2; var linear = rectWidth < rectHeight ? g.createLinearGradient(0, posY - width / 2, width, posY + width / 2) : g.createLinearGradient(posX - width / 2, 0, posX + width / 2, width);
原因其實很簡單,就是漸變顏色方向的起點和終點並沒有隨着 width 的改變而改變。
如圖所示以
rectWidth > rectHeight
為例
繪制組件
在繪制組件的過程中,我們需要把一些邊界條件和特殊情況考慮到,來保持組件的擴展性和穩定性
下面就是一些我的心得
-
在做了 g 操作的頭尾分別使用
save
和restore
,以此來保障 g 操作的不影響后續的擴展開發。g.save() // g 操作 // ... // ... g.restore()
設想一下,我們正在用 10 像素寬,顏色為紅色的筆畫圖,然后把畫筆設置成1像素寬,顏色變成綠色。綠色畫完之后呢,我們想接着用10像素的紅色來畫,如果沒有 save 與 restore,那我們就不得不重新設置一遍畫筆——如果畫筆狀態過多,那我們的代碼就會大量增加;而且,這些設置過程是重復而乏味的。
最后保存的最先還原!restore 總是還原離他最近的 save 點(已經還原的不能第2次還原到他)。
另外 save 和 restore 一般是改變了 transform 或 clip 才需要,大部分情況下不需要,例如你設置了顏色、寬度等等參數,下次要繪制這些的人會自己再設置這些,所以能盡量不用 save/restore 的地方可以盡量不用,那也是有代價的
-
當進度值為 0 且 線帽樣式為圓角的時候進度環會變成一個圓點,正確的做法使需要對進度值為 0 的時候進行特殊處理。
// 進度環 g.beginPath(); g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel); g.strokeStyle = linearOuter; g.lineCap = progressLineCap; if (progressPercentage !== 0) g.stroke();
-
由於 Chrome 瀏覽器的限制(Chrome 顯示最小字體為 12 px),所以不能通過 12px這樣的數值設定文字大小,只能通過縮放來控制文字的大小了。
當你高高興興的的使用 scale 對文字進行縮放的時候
var fontScale = 0.75 g.fillStyle = 'white'; g.textAlign = 'center'; g.font = fontSize + 'px Arial'; g.scale(fontScale, fontScale); g.fillText(progressPercentage + '%', posX, posY + fontSize / 3);
你會得到這樣的結果
造成這個結果的原因是 scale 操作的參考點位置不對
下面我們使用矩形的例子詳細解釋一下
// 原矩形 ctx.save(); ctx.beginPath(); ctx.strokeRect(0, 0, 400, 400); ctx.restore(); // 縮放后的矩形 ctx.save(); ctx.beginPath(); ctx.scale(0.75, 0.75); ctx.strokeRect(0, 0, 400, 400); ctx.restore();
這時 scale 的參考點是
(0,0)
所以,中心縮放沒有按照我們預期的進行當修改參考點的坐標為
(50,50)
之后,中心縮放就正常了那么這個
(50,50)
是怎么得來的?根據上圖我們不難看出這個距離其實就是
(縮放前的邊長 - 縮放后的邊長) / 2
得到得公式就是
width * (1 - scale) / 2
在這個例子中套用一下就是
400 * (1 - 0.75) / 2 = 50
// 原矩形 ctx.save(); ctx.beginPath(); ctx.strokeRect(0, 0, 400, 400); ctx.restore(); // 縮放后的矩形 ctx.save(); ctx.beginPath(); ctx.translate(50, 50) ctx.scale(0.75, 0.75); ctx.strokeRect(0, 0, 400, 400); ctx.restore();
我們把上面得公式在做進一步的擴展,讓它的適用性更強
width * (1 - scale) / 2 -> width / 2 * (1 - scale) -> posX * (1 - scale) height * (1 - scale) / 2 -> height / 2 * (1 - scale) -> posY * (1 - scale)
在這里也需要明確一點
posX = x + (width / 2)
posY = y + (height / 2)
在進一步抽象成函數
function centerScale(ctx, posX, posY, scaleX, scaleY) { ctx.translate(posX * (1 - scaleX), posY * (1 - scaleY)); ctx.scale(scaleX, scaleY); }
那么其中的文字縮放也是如出一轍
var fontScale = 0.75 g.fillStyle = 'white'; g.textAlign = 'center'; g.font = fontSize + 'px Arial'; g.translate(posX * (1 - fontScale), posY * (1 - fontScale)); g.scale(fontScale, fontScale); g.fillText(progressPercentage + '%', posX, posY + fontSize / 3);
當然結果也是很不錯的😉,文字的縮放功能實現了
在實現上如果大家有什么問題可以直接留言或者私信或者直接去官網hightopo上查閱相關的資料
結語
這個進度環組件的開發就到此結束了,相信小伙伴們通過我的這篇學習筆記也是可以通過ht.js
獨立開發一個拓撲組件了。后續我還會不定期的分享我的學習心得,希望小伙伴們也能給出自己的建議。