用canvas写出ui想要的仪表盘

1,951 阅读4分钟

演示地址(页面中最下面可以直接下载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;
};

完事