vue造轮子之圆形进度条

在github上看到一个圆形进度条组件,比较适合于我现在的业务需求,然后就拷贝下来放进我的项目。但是cv程序员并不是我想要。所以就研究了一下它的源码,没想到的是源码竟然是如此简单和通俗易懂,先附上原作者的github地址和效果图。
https://github.com/cumt-robin/vue-awesome-progress
vue造轮子之圆形进度条_第1张图片
一.canvas基本用法
由于该组件是基于canvas完成的,所以我们需要知道canvas的基本用法,学习canvas地址为
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial
在这里我们需要知道,canvas如何画圆和写文字,下面有几个简单的demo
canvs画圆
根据mdn文档的介绍
画圆需要借助arc(x,y,r,startAngle,endAngle,anticlockwise)方法。

该方法有六个参数:x,y为绘制圆弧所在圆上的圆心坐标。radius为半径。startAngle以及endAngle参数用弧度定义了开始以及结束的弧度。这些都是以x轴为基准。参数anticlockwise为一个布尔值。为true时,是逆时针方向,否则顺时针方向。
*注意:arc()函数中表示角的单位是弧度,不是角度。角度与弧度的js表达式:
弧度=(Math.PI/180)角度。

 

这个时候你就可以在页面上看到这个0-270deg的圆环
vue造轮子之圆形进度条_第2张图片
需要注意的一点是,圆弧它的起始角度为
vue造轮子之圆形进度条_第3张图片
关于如何给圆弧上颜色,大家可以自行去mdn上查找资料,下面是链接
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes

二.绘制文本
canvas绘制文字主要使用了strokeText(text,x,y)方法,该方法主要接受3个参数,第一个为绘制文本内容,后两个分别对应着x和y。


export default {
  mounted () {
    let canvas = this.$refs.canvasDemo;
    let ctx = canvas.getContext("2d");
    ctx.font = "48px serif";
    ctx.textBaseline = "hanging";
    ctx.strokeText("Hello world", 0, 100);
  }
};

以上这两种方法是我们绘制canvas圆形进度条主要用到的方法,那么就开始在vue实现圆形的进度条。
在components下建立awesomeProgress文件夹和awesomeProgress.vue文件。
首先我们先来分析一下,画一个这样的圆环需要什么?
vue造轮子之圆形进度条_第4张图片
其实在这个圆弧中,它其实是分成了两个圆,一个是底圆,一个是进度的圆,那么,我们在这里就可以确定参数了。
1.底圆的半径r,我们可以通过它底圆的半径来确定这个canvas的宽度和高度,但是我们需要注意一点,**底圆的半径和进度圆的半径是一样的,唯一不同的地方就是进度圆是有线条的宽度的,而且这个进度圆也分了有进度圆点和没有进度圆点的情况,如果它是有进度圆点的情况,这个时候canvas的宽高应为(底圆的半径+小球的半径(默认为6))2。如果没有进度圆点,则进度圆点的半径为0的情况,canvas的宽高应为(底圆的半径+线条宽度/2(默认为8))2

vue造轮子之圆形进度条_第5张图片

三.确定圆形canvas的宽高
在awesomeProgress.vue文件中的template,编写一个canvas,

<template>
  <canvas ref="canvasDemo"
          :width="canvasSize"
          :height="canvasSize" />
</template>
export default {
  name: "awesomeProgress",
  props: {
    // 底部圆的半径
    circleRadius: {
      type: Number,
      default: 40
    },
    // 底部圆的线条宽度
    circleWidth: {
      type: Number,
      default: 2
    },
    // 底部圆的颜色
    circleColor: {
      type: String,
      default: '#e5e5e5'
    },
    // 进度圆的线条宽度
    lineWidth: {
      type: Number,
      default: 8
    },
    // 进度圆小球上的半径
    pointRadius: {
      type: Number,
      default: 6
    }
  },
  computed: {
    // 求出进度圆和底部圆半径之和
    outerRadius () {
      return this.pointRadius > 0 ? (this.circleRadius + this.pointRadius) : (this.circleRadius + this.lineWidth / 2);
    },
    // canvas的宽度和高度需*2
    canvasSize () {
      return this.outerRadius * 2
    }
  },
  methods: {
    initCanvas () {
      let canvas = this.$refs.canvasDemo; // 获取canvas的dom节点
      let ctx = canvas.getContext("2d");
      // 画圆环
      ctx.strokeStyle = this.circleColor;
      ctx.lineWidth = this.circleWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));
      ctx.stroke();
    },
    // 角度转换弧度
    deg2Arc (deg) {
      return deg / 180 * Math.PI
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.initCanvas()
    })
  }
}

那么此时页面上就会渲染出以下的图形,
vue造轮子之圆形进度条_第6张图片
四.绘制进度圆和进度圆文字
既然是进度圆,则必须告知,进度圆在那个地方结束,在这里就假定一个参数percent,这个参数代表的是进度完成了百分之多少。那么这个百分之多少则必须映射到我们这个进度圆上面去。其实我们是可以通过它的百分比来求出这个进度圆结束的角度。

export default {
  name: "awesomeProgress",
  props: {
    // 底部圆的半径
    circleRadius: {
      type: Number,
      default: 40
    },
    // 底部圆的线条宽度
    circleWidth: {
      type: Number,
      default: 2
    },
    // 底部圆的颜色
    circleColor: {
      type: String,
      default: '#e5e5e5'
    },
    // 进度圆的线条宽度
    lineWidth: {
      type: Number,
      default: 8
    },
    // 进度圆小球上的半径
    pointRadius: {
      type: Number,
      default: 6
    },
    // 进度圆的百分比
    percentage: {
      type: Number,
      default: 0,
      validator: function (value) {
        return value >= 0 && value <= 100
      }
    },
    // 进度圆的起始角度
    startDeg: {
      type: Number,
      default: 270,
      validator: function (value) {
        return value >= 0 && value < 360
      }
    },
    fontSize: {
      type: Number,
      default: 14
    },
    fontColor: {
      type: String,
      default: '#3B77E3'
    },
    // 是否显示文字
    showText: {
      type: Boolean,
      default: true
    }
  },
  computed: {
    // 求出进度圆和底部圆半径之和
    outerRadius () {
      return this.pointRadius > 0 ? (this.circleRadius + this.pointRadius) : (this.circleRadius + this.lineWidth / 2);
    },
    // canvas的宽度和高度需*2
    canvasSize () {
      return this.outerRadius * 2
    }
  },
  methods: {
    initCanvas () {
      let canvas = this.$refs.canvasDemo; // 获取canvas的dom节点
      let ctx = canvas.getContext("2d");
      // // 画圆环
      // 画圆环
      ctx.strokeStyle = this.circleColor;
      ctx.lineWidth = this.circleWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));
      ctx.stroke();

      const endDeg = this.getTargetDegByPercentage(this.startDeg, this.percentage); // 求出进度圆结束的角度
      this.animateDrawArc(canvas, ctx, this.startDeg, endDeg);
    },
    // 画进度圆,文字
    animateDrawArc (canvas, ctx, startDeg, endDeg) {
      // 画文字
      if (this.showText) {
        this.drawText(ctx, canvas)
      }
      const startArc = this.deg2Arc(startDeg);
      ctx.strokeStyle = "red";
      ctx.lineWidth = this.lineWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, startArc, this.deg2Arc(endDeg));
      ctx.stroke();
    },
    // 画文字的方法
    drawText (ctx, canvas) {
      ctx.font = `${this.fontSize}px Arial,"Microsoft YaHei"`
      ctx.fillStyle = this.fontColor;
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      let label = Math.round(this.percentage) + "%";
      ctx.fillText(label, canvas.clientWidth / 2, canvas.clientWidth / 2);
    },
    // 求出进度圆结束的角度的方法
    getTargetDegByPercentage (startDeg, percentage) {
      if (percentage === 100) {
       	 return startDeg + 360;
      }
      return (startDeg + 360 * percentage / 100) % 360
    },
    // 角度转换弧度
    deg2Arc (deg) {
      return deg / 180 * Math.PI
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.initCanvas()
    })
  }
}

在其他组件中引用的方式为:

<AwesomeProgress circle-color="#e5e9f2"
                     :circle-radius="60"
                     :startDeg="0"
                     :percentage="50" />

此时的效果为:
vue造轮子之圆形进度条_第7张图片
这个时候已经能出现半圆弧的效果,而且能够根据percentage的值(0-100)不同进行了变化。但是这中展示效果过于生硬,我们期望能够出现动画效果。

五.添加进度圆渐变色
在做动画之前,我们先给进度圆添加颜色,毕竟红色实在是太辣眼睛了…
关于给canvas添加渐变颜色在mdn上也说的很清楚
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/createLinearGradient
按照文档描述:

使用 createLinearGradient 方法创建一个指定了开始和结束点的CanvasGradient 对象。创建成功后, 你就可以使用 CanvasGradient.addColorStop() 方法,根据指定的偏移和颜色定义一个新的终止。 如例子所示,渐变允许赋值给当前的fillStyle ,使用fillRect() 方法时,在 canvas 上绘制出效果。


<script>
// 参考于https://github.com/cumt-robin/vue-awesome-progress/blob/master/src/components/index.vue
// import BezierEasing from "bezier-easing";
export default {
  name: "awesomeProgress",
  props: {
    // 渐变色的颜色数组
    lineColorStops: {
      type: Array,
      default: function () {
        return [
          { percent: 0, color: '#13CDE3' },
          { percent: 1, color: '#3B77E3' }
        ]
      }
    },
    // 是否使用渐变色
    useGradient: {
      type: Boolean,
      default: true
    },
    lineColor: {
      type: String,
      default: '#3B77E3'
    }
  },
  methods: {
    initCanvas () {
      let canvas = this.$refs.canvasDemo; // 获取canvas的dom节点
      let ctx = canvas.getContext("2d");
      // // 画圆环
      ctx.strokeStyle = this.circleColor;
      ctx.lineWidth = this.circleWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));
      ctx.stroke();

      const endDeg = this.getTargetDegByPercentage(this.startDeg, this.percentage); // 求出进度圆结束的角度
      this.animateDrawArc(canvas, ctx, this.startDeg, endDeg);
    },
    // 画进度圆,文字和圆点的方法
    animateDrawArc (canvas, ctx, startDeg, endDeg) {
      // 画文字
      if (this.showText) {
        this.drawText(ctx, canvas)
      }
      if (this.useGradient) {
        this.gradient = ctx.createLinearGradient(this.circleRadius, 0, this.circleRadius, this.circleRadius * 2);
        this.lineColorStops.forEach(item => {
          this.gradient.addColorStop(item.percent, item.color);
        });
      }
      const startArc = this.deg2Arc(startDeg);
      ctx.strokeStyle = this.useGradient ? this.gradient : this.lineColor;
      ctx.lineWidth = this.lineWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, startArc, this.deg2Arc(endDeg));
      ctx.stroke();
    }
  }
}
</script>

<style>
</style>

这时候你就可以看到很漂亮的渐变色出现了,具体效果如下
vue造轮子之圆形进度条_第8张图片
六.给进度圆弧添加动画效果
添加动画效果,我们借助第三方库bezier-easing,只需npm i bezier-easing --save即可,然后在组件顶部把它引入:

import BezierEasing from "bezier-easing";

其实我们都知道,如果是希望元素产生动画效果,我们传统的方式是采用setInterval时间的方式来堆积动画效果,而这里更希望能够以帧的方式来实现,那么就有个api是window.requestAnimationFrame,这个api的具体用法在mdn文档也有介绍
https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame

其实求他的帧数,要分成三种情况,都是比较它的startDeg和endDeg,求出它的差值,再用差值去乘以这个运动函数即可,

<template>
  <canvas ref="canvasDemo"
          :width="canvasSize"
          :height="canvasSize" />
</template>

<script>
// 参考于https://github.com/cumt-robin/vue-awesome-progress/blob/master/src/components/index.vue
import BezierEasing from "bezier-easing";
export default {
  name: "awesomeProgress",
  props: {
    // 底部圆的半径
    circleRadius: {
      type: Number,
      default: 40
    },
    // 底部圆的线条宽度
    circleWidth: {
      type: Number,
      default: 2
    },
    // 底部圆的颜色
    circleColor: {
      type: String,
      default: '#e5e5e5'
    },
    // 进度圆的线条宽度
    lineWidth: {
      type: Number,
      default: 8
    },
    // 进度圆小球上的半径
    pointRadius: {
      type: Number,
      default: 6
    },
    // 进度圆的百分比
    percentage: {
      type: Number,
      default: 0,
      validator: function (value) {
        return value >= 0 && value <= 100
      }
    },
    // 进度圆的起始角度
    startDeg: {
      type: Number,
      default: 270,
      validator: function (value) {
        return value >= 0 && value < 360
      }
    },
    fontSize: {
      type: Number,
      default: 14
    },
    fontColor: {
      type: String,
      default: '#3B77E3'
    },
    // 是否显示文字
    showText: {
      type: Boolean,
      default: true
    },
    // 渐变色的颜色数组
    lineColorStops: {
      type: Array,
      default: function () {
        return [
          { percent: 0, color: '#13CDE3' },
          { percent: 1, color: '#3B77E3' }
        ]
      }
    },
    // 是否使用渐变色
    useGradient: {
      type: Boolean,
      default: true
    },
    // 线条颜色
    lineColor: {
      type: String,
      default: '#3B77E3'
    },
    // 是否需要动画
    animated: {
      type: Boolean,
      default: true
    },
    // 动画函数
    easing: {
      type: String,
      // ease-in
      default: '0.42,0,1,1',
      validator: function (value) {
        return /^(\d+(\.\d+)?,){3}\d+(\.\d+)?$/.test(value)
      }
    },
    duration: {
      type: Number,
      // 浏览器大约是60FPS,因此1s大约执行60次requestAnimationFrame
      default: 1
    },
  },
  computed: {
    // 求出进度圆和底部圆半径之和
    outerRadius () {
      return this.pointRadius > 0 ? (this.circleRadius + this.pointRadius) : (this.circleRadius + this.lineWidth / 2);
    },
    // canvas的宽度和高度需*2
    canvasSize () {
      return this.outerRadius * 2
    },
    steps () {
      return this.duration * 60
    }
  },
  data () {
    return {
      gradient: null, // 渐变色对象
      easingFunc: null
    }
  },
  methods: {
    initCanvas () {
      let canvas = this.$refs.canvasDemo; // 获取canvas的dom节点
      let ctx = canvas.getContext("2d");
      // // 画圆环
      // 画圆环
      ctx.strokeStyle = this.circleColor;
      ctx.lineWidth = this.circleWidth;
      ctx.beginPath();
      ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));
      ctx.stroke();

      const endDeg = this.getTargetDegByPercentage(this.startDeg, this.percentage); // 求出进度圆结束的角度
      // this.animateDrawArc(canvas, ctx, this.startDeg, endDeg);
      if (this.percentage === 0) {
        this.animateDrawArc(canvas, ctx, this.startDeg, endDeg, 0, 0);
      } else {
        if (this.animated) {
          this.animateDrawArc(canvas, ctx, this.startDeg, endDeg, 1, this.steps);
        } else {
          this.animateDrawArc(canvas, ctx, this.startDeg, endDeg, this.steps, this.steps);
        }
      }
    },
    // 画进度圆,文字和圆点的方法
    animateDrawArc (canvas, ctx, startDeg, endDeg, stepNo, stepTotal) {
      // 画文字
      window.requestAnimationFrame(() => {
        ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
        if (this.showText) {
          this.drawText(ctx, canvas, stepNo, stepTotal)
        }
        if (this.useGradient) {
          this.gradient = ctx.createLinearGradient(this.circleRadius, 0, this.circleRadius, this.circleRadius * 2);
          this.lineColorStops.forEach(item => {
            this.gradient.addColorStop(item.percent, item.color);
          });
        }
        // 画进度圆
        if (stepTotal > 0) {
          const nextDeg = this.getTargetDeg(startDeg, endDeg, stepNo, stepTotal); // 求出下一帧的角度
          const nextArc = this.deg2Arc(nextDeg);
          const startArc = this.deg2Arc(startDeg);
          ctx.strokeStyle = this.useGradient ? this.gradient : this.lineColor;
          ctx.lineWidth = this.lineWidth;
          ctx.beginPath();
          ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, startArc, nextArc);
          ctx.stroke();
        }
        if (stepNo !== stepTotal) {
          stepNo++;
          this.animateDrawArc(canvas, ctx, startDeg, endDeg, stepNo, stepTotal)
        }
      })
    },
    // 画文字的方法
    drawText (ctx, canvas, stepNo, stepTotal) {
      ctx.font = `${this.fontSize}px Arial,"Microsoft YaHei"`
      ctx.fillStyle = this.fontColor;
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      let label = Math.round(this.easingFunc(stepNo / stepTotal) * this.percentage) + "%";
      ctx.fillText(label, canvas.clientWidth / 2, canvas.clientWidth / 2);
    },
    // 求出下一帧角度
    getTargetDeg (startDeg, endDeg, stepNo, stepTotal) {
      if (stepTotal === 0) {
        return startDeg;
      }
      startDeg = startDeg % 360; // 180=> 180 361=>1
      endDeg = endDeg % 360;
      console.log(startDeg, endDeg)
      if (startDeg > endDeg) {
        const diff = endDeg + 360 - startDeg
        let nextDeg = startDeg + diff * this.easingFunc(stepNo / stepTotal)
        console.log(nextDeg);
        if (nextDeg > 360) {
          nextDeg = nextDeg - 360
          return nextDeg > endDeg ? endDeg : nextDeg
        }
        return nextDeg
      } else if (startDeg < endDeg) {
        const diff = endDeg - startDeg
        let nextDeg = startDeg + diff * this.easingFunc(stepNo / stepTotal)
        console.log(nextDeg);
        if (nextDeg > endDeg) {
          return endDeg
        } else if (nextDeg > 360) {
          return nextDeg - 360
        }
        return nextDeg
      } else {
        return startDeg + 360 * this.easingFunc(stepNo / stepTotal)
      }
    },
    // 求出进度圆结束的角度的方法
    getTargetDegByPercentage (startDeg, percentage) {
      if (percentage === 100) {
        return startDeg + 360;
      }
      return (startDeg + 360 * percentage / 100) % 360
    },
    // 角度转换弧度
    deg2Arc (deg) {
      return deg / 180 * Math.PI
    }
  },
  mounted () {
    const easingParams = this.easing.split(',').map(item => Number(item))
    this.easingFunc = BezierEasing(...easingParams);
    console.log(this.easingFunc);
    this.$nextTick(() => {
      this.initCanvas()
    })
  }
}
</script>

<style>
</style>

最后还有一个小圆点的位置计算,实现起来也比较简单,解决思路:根据三角形的正玄、余弦来得值;
假设一个圆的圆心坐标是(a,b),半径为r,则圆上每个点的X坐标=a + Math.sin(2Math.PI / 360) * r ;Y坐标=b + Math.cos(2Math.PI / 360) * r ;
。如果想要获得更加详细的代码可以参考原作者github地址,https://github.com/cumt-robin/vue-awesome-progress
而这个组件也给我收录了日常开发的组件库中,欢迎各位朋友star。
https://github.com/whenTheMorningDark/workinteresting/blob/master/src/components/awesomeProgress/awesomeProgress.vue

你可能感兴趣的:(vue,es6)