用canvas寫出ui想要的儀表盤

心平氣和艾維dei發表於2020-03-13

演示地址(頁面中最下面可以直接下載js檔案)

需求:

  • 最大值12,單位1
  • 漸變
  • 不用數字刻度
  • 有根長指標

觀察設計圖,圖表需要控制的變數如下:

{放哪個元素上,當前資料,最大值,大標題,下面的文字,是否需要動畫,每個刻度間要多少條線}

手把手教程,有canvas基礎建議直接看演示裡的程式碼


衝!

準備工作

新建gauge.js放這個圖表的程式碼

// gauge.js
function Gauge(c) {
    this.el = c.el // canvas的id
    this.maxLineNums = c.maxLineNums // 最大刻度
    this.unitLineNums = c.unitLineNums // 單位刻度內線條數
    this.data = c.data // 資料
    this.title = c.title // 大標題
    this.textBottom = c.textBottom // 下面的文字
    this.isAnimation = c.isAnimation //是否有動畫,預設關閉
}
複製程式碼

建立一個圖表,canvas高寬建議在標籤內指定 引入js檔案,建立一個例項

// gauge.html
<canvas id="aaa" width="200px" height="200px"></canvas>
<script src="../src/gauge/gauge.js"></script>
<script>
        let canvas1 = new Gauge({
            el: 'aaa',  //canvas的id(在標籤內指定高寬)
            maxLineNums: 24, //最大刻度
            unitLineNums: 5, //單位刻度內線條數
            data: 24, //資料
            title: '標題1',
            textBottom: '下面的文字a',
            isAnimation: true //關閉動畫,預設關閉
        })
</script>
複製程式碼

html裡的操作就完事了,回到gauge.js

建立一個canvas

  • 獲取元素尺寸,設定文字居中
  • 設定動畫指標、開始的角度、圓的半徑
  • 生成漸變色陣列,漸變演算法
// function Gauge(c)內部
    let canvas = document.getElementById(this.el),
        ctx = canvas.getContext('2d'),
        cWidth = canvas.width,
        cHeight = canvas.height;
    ctx.textAlign = "center"
    
    let animationData = 0 //動畫,當前指標的資料
    let startAngel = 0.75 * Math.PI // 儀表盤是個圓弧,設定1.5pi。
    let r = 0.4 * canvas.width 
    // 半徑將隨著canvas的尺寸而變化
    
    //生成漸變色組,可更改前兩個引數改變顏色
    let gradientColor = gradientColors('#17deea', '#e97f03', this.maxLineNums + 1)

複製程式碼

開始畫畫

將繪畫過程放在函式裡,方便做動畫

在這個方法裡,animationData是此時畫布的data,通過迴圈0->data達到動畫效果

    let draw = () => {
        // 動畫就是每一幀畫布重繪,所以要先清除畫布
        ctx.clearRect(-200, -200, 400, 400);
        ctx.beginPath();
        // 1.畫標題和文字
        // 2.畫刻度線
    }
    // 3.判斷動畫是否執行
    // 4.畫單位刻度內的小線
複製程式碼

1.畫標題和文字

        if (this.title !== undefined) {
            ctx.fillStyle = '#777';
            ctx.font = "18px serif";
            ctx.fillText(this.title, cWidth / 2, cHeight * 0.4); //大標題的顏色、字號、位置
        }
        if (this.textBottom !== undefined) {
            ctx.font = "12px serif";
            ctx.fillText(this.textBottom, cWidth / 2, cHeight * 0.8); //底部文字的顏色(與大標題一致,可自行增加)、字號、位置
        }

        ctx.font = "28px serif";
        ctx.fillStyle = gradientColor[animationData]; //中間資料的顏色(與指標顏色一致,可自行更改)
        ctx.fillText(animationData, cWidth / 2, cHeight * 0.55); //中間資料的位置

複製程式碼

2.畫刻度線

通過迴圈畫出單位刻度線。有兩種方法畫出圓:旋轉畫布、用三角函式算出線條的起點和終點。第一種在動畫時因為要不停迴圈會導致我角度算不出來,所以採用三角函式,推薦第二種。畫線的步驟如下:

  • 計算當前刻度的角度 = 開始角度 + (第幾根線)*(1.5pi/刻度數)
  • 計算上一個單位刻度的角度
  • 線條顏色通過gradientColor()漸變演算法得到,若當前刻度>animationData,顏色是#ccc
  • 迴圈繪製上一個刻度到當前刻度內的線(條數this.unitLineNums)
    • 將上一個刻度傳入drawSmallLine(previousAngle)
    • 計算刻度
  • 設定單位刻度的線寬
  • 移動畫筆起點
  • 畫單位刻度線,如果當前刻度===animationData,那這條線要長一點,通過let rL = 1.2 * r調整長度
        for (let i = 1; i <= this.maxLineNums; i++) {
            let currentAngle = startAngel + i * (1.5 * Math.PI / this.maxLineNums)
            let previousAngle = startAngel + (i - 1) * (1.5 * Math.PI / this.maxLineNums)
            for (let j = 0; j < this.unitLineNums; j++) {
                ctx.strokeStyle = (i > animationData) ? '#cccccc' : gradientColor[i];
                drawSmallLine(previousAngle)
                previousAngle = previousAngle + 1.5 * Math.PI / (this.maxLineNums * this.unitLineNums)
            }
            ctx.save();
            ctx.beginPath();
            ctx.lineWidth = 1; //單位刻度線條寬度,推薦1~2

            ctx.strokeStyle = (i > animationData) ? '#cccccc' : gradientColor[i];
            ctx.moveTo(Math.cos(currentAngle) * r * 0.75 + cWidth / 2, Math.sin(currentAngle) * r * 0.75 + cHeight / 2);
            if (i === animationData) {
                let rL = 1.2 * r
                ctx.lineTo(Math.cos(currentAngle) * rL + cWidth / 2, Math.sin(currentAngle) * rL + cHeight / 2);
            } else {
                ctx.lineTo(Math.cos(currentAngle) * r + cWidth / 2, Math.sin(currentAngle) * r + cHeight / 2);
            }
            ctx.stroke();
        }
複製程式碼

3.判斷動畫是否執行

  • 如果isAnimation真,動畫執行,將呼叫requestAnimationFrame(animation),這個api可以順滑執行動畫,但是時間不可控制,也可以用別的方法在這裡寫一個迴圈替代
  • 如果不執行動畫, animationData = this.data ,只繪製一次
    if (this.isAnimation) {
        let animation = () => {
            draw()
            animationData++

            if (animationData <= c.data) {
                requestAnimationFrame(animation)
            }
        }
        requestAnimationFrame(animation);
    } else {
        animationData = this.data
        draw()
    }

複製程式碼

4.畫單位刻度內的小線function drawSmallLine

    function drawSmallLine(currentAngle) {
        ctx.save();
        ctx.beginPath();
        ctx.lineWidth = 1; //單位刻度內線條寬度,推薦1~2
        ctx.moveTo(Math.cos(currentAngle) * r * 0.75 + cWidth / 2, Math.sin(currentAngle) * r * 0.75 + cHeight / 2);
        ctx.lineTo(Math.cos(currentAngle) * r + cWidth / 2, Math.sin(currentAngle) * r + cHeight / 2);
        ctx.stroke();
    }
複製程式碼

漸變色演算法

這個演算法是cv來的,但是忘了在哪複製的了,先謝謝這位大佬

let gradientColors = function (start, end, steps, gamma) {
    // 顏色漸變演算法
    // convert #hex notation to rgb array
    let parseColor = function (hexStr) {
        return hexStr.length === 4 ? hexStr.substr(1).split('').map(function (s) {
            return 0x11 * parseInt(s, 16);
        }) : [hexStr.substr(1, 2), hexStr.substr(3, 2), hexStr.substr(5, 2)].map(function (s) {
            return parseInt(s, 16);
        })
    };

    // zero-pad 1 digit to 2
    let pad = function (s) {
        return (s.length === 1) ? '0' + s : s;
    };

    let i, j, ms, me, output = [],
        so = [];
    gamma = gamma || 1;
    let normalize = function (channel) {
        return Math.pow(channel / 255, gamma);
    };
    start = parseColor(start).map(normalize);
    end = parseColor(end).map(normalize);
    for (i = 0; i < steps; i++) {
        ms = i / (steps - 1);
        me = 1 - ms;
        for (j = 0; j < 3; j++) {
            so[j] = pad(Math.round(Math.pow(start[j] * me + end[j] * ms, 1 / gamma) * 255).toString(16));
        }
        output.push('#' + so.join(''));
    }
    return output;
};
複製程式碼

完事

相關文章