在如今前端圈大行其道的 React 和 Vue 中,可複用的組件多是他們大受歡迎的緣由之一,javascript
在 HT 的產品中也有組件的概念,不過在 HT 中組件的開發是依託於 HTML5 Canvas 的技術去實現的,html
也就是說若是你有過使用 Canvas 的開發經驗你就能夠來封裝本身的組件。 前端
下面我以一個進度環爲例,來探究一下如何使用ht.js
封裝出一個拓撲組件。html5
自定義組件java
除了
HT
預約義的組件類型外,用戶還能夠自定義擴展類型,自定義有兩種方式:canvas
- 直接將
type
值設置成繪製函數:function(g, rect, comp, data, view){}
- 經過
ht.Default.setCompType(name, funtion(g, rect, comp, data, view){})
註冊組件類型,矢量type
值設置成相應的註冊名
在這裏我選用第一種經過形如數組
ht.Default.setImage('circle-progress-bar', { width: 100, height: 100, comps: [ { type: function(g, rect, comp, data, view) { // ... } } ] });
這樣的方式完成組件的聲明,那麼 function(g, rect, comp, data, view) { }
中的內容就是咱們接下來須要關注的了瀏覽器
抽象並聲明出幾個 Coding 中須要的變量ide
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'));
準備工做結束後下面就是 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;
當 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()
save/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
獨立開發一個拓撲組件了。後續我還會不按期的分享個人學習心得,但願小夥伴們也能給出本身的建議。