用canvas画一个进度盘

4,330 阅读3分钟

工作的时候收到了UI的设计图,有一个类似进度盘(话说我也不大清楚官方叫法是啥)的效果。。就想起了荒废了好久的canvas。确认过效果,就用canvas来玩一下吧。。

UI分析

UI图如图:

image

那么,按层级分析,里面有几个部分:

  1. 空心圆(进度条的背影)
  2. 空心圆(进度条)
  3. 空心圆(实心圆的边框)
  4. 实心圆
  5. 文字(进度)
  6. 文字(最二行)

确认了需要绘制的部分起码有6块。

开始绘制

canvas了解

根据上面的各层级需求,大概可能会使用上的api如下

// 线宽
ctx.lineWidth
// 用于指定结束线帽的样式
ctx.lineCap
// 绘制渐变色
ctx.createLinearGradient
// 指定线条绘图颜色
ctx.strokeStyle
// 指定填充绘图颜色
ctx.fillStyle
// 创建一个圆
ctx.arc()
// 绘制文字
ctx.fillText()
// 指定文字字样
ctx.font
// 指定文字位置
ctx.textAlign

canvas画圆说明

就是ctx.arc(x, y, redius, startAngle, endAngle, counterclockwise)这个方法可以创建一个圆形,接收6个参数,分别为 圆心x坐标,圆心y坐标, 圆半径,起始弧度,结束弧度,是否逆时针绘制

角度=180°×弧度÷π ,弧度=角度×π÷180°

一个完整圆为: 360° × π ÷ 180° = 2 × π

综上,从0开始到 Math.PI*2 为一个圆

圆的绘制路径如图所示

image

更详细可参考: W3school

先声明一个canvas

// HTML部分
<canvas id="canvas"></canvas>
// JS部分
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = Math.ceil(300 / 1920 * window.innerWidth);
canvas.height = Math.ceil(300 / 1920 * window.innerWidth);

创建一个类

class Annulus {

    constructor(obj={}){
        this.size = 100;
        this.lineWidth = 10;
        this.location = obj.location || {x: canvas.width/2, y: canvas.height/2};
    }

    // 绘制进度条底层
    drawBg(){}
    // 绘制进度条
    drawCircleLay(){}
    // 绘制中心的大圆
    drawCenterCircle(){}
    // 绘制大圆边缘的边
    drawCenterBorderCircle(){}
    // 绘制进度文字
    drawTextPercent(){}
    // 绘制进度下面的文字 
    drawTextName(){}
    // 定义动画
    animate(){
        this.drawBg();
        this.drawCircleLay();
        this.drawCenterCircle();
        this.drawCenterBorderCircle();
        this.drawTextPercent();
        this.drawTextName();
    }
    // 执行
    run(){
        this.animate();
    }
}

进度条的背景

drawBg(){
    // 绘制背景圈
    ctx.beginPath();
    ctx.strokeStyle = '#2d4264';
    ctx.lineWidth = 10;
    ctx.lineCap = "round";
    ctx.arc(this.location.x, this.location.y, 100, Math.PI*0.75, Math.PI*2.25, false);
    ctx.stroke();
}

进度条

drawCircleLay(){
    // 绘制进度条
    ctx.beginPath();
    var gradient = ctx.createLinearGradient(0, this.linearLocation().start, 0, this.linearLocation().end);
    gradient.addColorStop(0, '#0f6cd9');
    gradient.addColorStop(1, '#05a6da');
    ctx.strokeStyle = gradient;
    ctx.lineWidth = this.lineWidth;
    ctx.lineCap = "round";
    ctx.arc(this.location.x, this.location.y, this.size, Math.PI*0.75, Math.PI*2.25, false);
    ctx.stroke();
}

linearLocation(){
    // 设定渐变背影的起始结束点
    let start = this.location.y - ((this.size-15)*2 + this.lineWidth)/2;
    let end = start + (this.size-15)*2 + this.lineWidth
    return {start: start, end: end}   
}

目前基本上跟背景圈一样,不同的是这里用了渐变色,先绘制出来,后再给加上变量变化

中心圆

drawCenterCircle(){
    // 绘制中心圆
    ctx.beginPath();
    var gradient = ctx.createLinearGradient(0, this.linearLocation().start, 0, this.linearLocation().end);
    gradient.addColorStop(0, '#39a8ce');
    gradient.addColorStop(1, '#5647c9');
    ctx.fillStyle = gradient;
    ctx.arc(this.location.x, this.location.y, this.size-15, 0, Math.PI*2, false);
    ctx.fill();
}

中心圆的边

drawCenterBorderCircle(){
    // 绘制中心圆周边的那圈
    ctx.beginPath();
    ctx.strokeStyle = 'rgba(0,0,0,0.3)';
    ctx.lineWidth = 10;
    ctx.arc(this.location.x, this.location.y, this.size-20, 0, Math.PI*2, false);
    ctx.stroke();
}

绘制进度

drawTextPercent(percent){
    // 绘制进度文字
    ctx.beginPath();
    ctx.font = '31px Arial';
    ctx.textAlign="center";
    ctx.fillStyle="#192f47";
    ctx.fillText(`100%`, this.location.x, this.location.y);
    ctx.stroke();
}

绘制进度下面的文字

drawTextName(){
    // 绘制二级文字
    ctx.beginPath();
    ctx.font = '14px "Microsoft YaHei"';
    ctx.textAlign="center";
    ctx.fillStyle="#192f47";
    ctx.fillText('text', this.location.x, this.location.y+25);
    ctx.stroke();
}

让它运动

接下来我们可以让进度条动起来。
运动通用的有几个点:

  1. 速度speed
  2. 经过的路程,在这就是角度degree,因为是进度条,总量就是100, 这里的圈的角度是 Math.PI*2.25 - Math.PI*0.75 = Math.PI * 1.75。那么1%的角度就是 Math.PI*.1.5 / 100
  3. 我们还需要有一个变量来记录当前运动到的百分比 tol

综上,我们把 类的 constructor 改造一下,如下:

constructor(obj = {}){
    /*
    * speed -- 速度
    * color -- 颜色
    * size -- 大小
    * lineWidth -- 线宽
    * location -- 圆心位置
    * text -- 文字
    * value -- 圆环滚动的值 ,这里指是百分比
    */
    this.speed = obj.speed || 0.1;
    this.color = obj.color || '#ffeedd';
    // 180 是UI的大体尺寸是在180px,以1920为基准
    this.size = Math.ceil((obj.size || 80) * (canvas.width / 180));
    this.lineWidth = obj.lineWidth || 10;
    this.location = obj.location || {x: canvas.width/2, y: canvas.height/2};
    this.textName = obj.text || '第二行文字title';
    this.value = obj.value || 0;
    // 这里是圆的终点减去圆的起点
    this.degree = Math.PI*1.5/100;
    this.animate = this.animate.bind(this);
    this.tol = 0;
}

drawCircleLay()方法的绘制结束点使用变量来控制

drawCircleLay(){
    // 绘制进度条
    if (this.value == 0) return;
    ctx.beginPath();
    var gradient = ctx.createLinearGradient(0, this.linearLocation().start, 0, this.linearLocation().end);
    gradient.addColorStop(0, '#0f6cd9');
    gradient.addColorStop(1, '#05a6da');
    ctx.strokeStyle = gradient;
    ctx.lineWidth = this.lineWidth;
    ctx.lineCap = "round";
    //这里改成用变量来控制 
    ctx.arc(this.location.x, this.location.y, this.size, Math.PI * 0.75, Math.PI*0.75+this.tol * this.degree, false);
    ctx.stroke();
}

drawTextPercent() 可以把变量tol传进来作用当前显示的进度数

drawTextPercent(percent){
    // 绘制进度文字
    ctx.beginPath();
    ctx.font = `${this.size / 2.5}px Arial`;
    ctx.textAlign="center";
    ctx.fillStyle="#192f47";
    ctx.fillText(`${parseInt(percent)}%`, this.location.x, this.location.y);
    ctx.stroke();
}

animate() 方法加上 RAF, 让其运动

animate(){
    window.requestAnimationFrame(this.animate);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    this.drawBg();
    this.drawCircleLay();
    this.drawCenterCircle();
    this.drawCenterBorderCircle();
    this.drawTextPercent(this.tol);
    this.drawTextName(this.textName);
    if (this.tol < this.value) { this.tol += this.speed }
}

至此,一个进度盘已完成。其实还有很多改进空间,比如更合理的封装,监听window.resize的时候进行大小的改变等。。

实例地址: codepen