1. 程式人生 > >基於 HTML5 Canvas 的拓撲元件開發

基於 HTML5 Canvas 的拓撲元件開發

在現在前端圈大行其道的 React 和 Vue 中,可複用的元件可能是他們大受歡迎的原因之一,

在 HT 的產品中也有元件的概念,不過在 HT 中元件的開發是依託於 HTML5 Canvas 的技術去實現的,

也就是說如果你有過使用 Canvas 的開發經驗你就可以來封裝自己的元件。

下面我以一個進度環為例,來探究一下如何使用ht.js封裝出一個拓撲元件。

效果圖

1

程式碼實現

前置知識

自定義元件

除了HT預定義的元件型別外,使用者還可以自定義擴充套件型別,自定義有兩種方式:

  • 直接將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) { }中的內容就是我們接下來需要關注的了

準備工作

  1. 抽象並宣告出幾個 Coding 中需要的變數

    • 進度百分比 progressPercentage {百分比}
    • 圓環漸變色 linearOuter {顏色陣列}
    • 內圓漸變色 linearInner {顏色陣列}
    • 字型縮放比例 fontScale {數字}
    • 顯示原始值 showOrigin {布林}
    • 進度條樣式 progressLineCap {線帽樣式}
  2. 變數的宣告和賦值了

    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; // 進度環結束角度
  3. 建立漸變色樣式

    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 的時間了

  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

  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

  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

  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

    最後通過簡單的配置就可以在網頁上呈現出這個進度環了

    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);
    // 這次多生成幾個 不過程式碼相似 在此就不贅述了

    6

    完整程式碼如下

    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();
                }
            }
        ]
    });
    View Code

幾點心得

宣告屬性

在這個部分有幾點可供參考

  • 使用小駝峰對屬性進行命名,並且少用縮寫儘量語義化

    舉個栗子:

    • fontScale 字型縮放比例
    • progressPercentage 進度百分比
  • 屬性值型別的選擇也要儘量貼合屬性的含義

    舉個栗子:

    • 一個儲存著幾個顏色值字串的陣列,用顏色陣列就比單純的陣列更為貼切
    • 一個表示畫筆線帽種類的字串,用線帽樣式就比字元轉更為貼切

使用屬性

由於進度環是一個圓形的元件,那麼在這裡有兩點供參考

  • 當元件的 rect.widthrect.height 不相等的時候我們需要自己來設定一個 width,

    讓圓在這個以 width 為邊的正方形中繪製,而 width 的值就是 rect.widthrect.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) 設定漸變色就會出現下面的效果,右下方的藍色不見了。

    7

    不過如果按照如下程式碼的方式設定漸變色就會出現下面的效果就會出現預期的效果了。

      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);

    8

    原因其實很簡單,就是漸變顏色方向的起點和終點並沒有隨著 width 的改變而改變。

    如圖所示以rectWidth > rectHeight 為例

    10

繪製元件

在繪製元件的過程中,我們需要把一些邊界條件和特殊情況考慮到,來保持元件的擴充套件性和穩定性

下面就是一些我的心得

  • 在做了 g 操作的頭尾分別使用 saverestore ,以此來保障 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);

    你會得到這樣的結果

    10

    造成這個結果的原因是 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();

    11

    這時 scale 的參考點是(0,0)所以,中心縮放沒有按照我們預期的進行

    當修改參考點的座標為(50,50)之後,中心縮放就正常了

    12

    那麼這個(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);

    當然結果也是很不錯的?,文字的縮放功能實現了

    14

    在實現上如果大家有什麼問題可以直接留言或者私信或者直接去官網hightopo上查閱相關的資料

結語

這個進度環元件的開發就到此結束了,相信小夥伴們通過我的這篇學習筆記也是可以通過ht.js獨立開發一個拓撲元件了。後續我還會不定期的分享我的學習心得,希望小夥伴們也能給出自己的建議。