在github上看到一个圆形进度条组件,比较适合于我现在的业务需求,然后就拷贝下来放进我的项目。但是cv程序员并不是我想要。所以就研究了一下它的源码,没想到的是源码竟然是如此简单和通俗易懂,先附上原作者的github地址和效果图。
https://github.com/cumt-robin/vue-awesome-progress
一.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的圆环
需要注意的一点是,圆弧它的起始角度为
关于如何给圆弧上颜色,大家可以自行去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文件。
首先我们先来分析一下,画一个这样的圆环需要什么?
其实在这个圆弧中,它其实是分成了两个圆,一个是底圆,一个是进度的圆,那么,我们在这里就可以确定参数了。
1.底圆的半径r,我们可以通过它底圆的半径来确定这个canvas的宽度和高度,但是我们需要注意一点,**底圆的半径和进度圆的半径是一样的,唯一不同的地方就是进度圆是有线条的宽度的,而且这个进度圆也分了有进度圆点和没有进度圆点的情况,如果它是有进度圆点的情况,这个时候canvas的宽高应为(底圆的半径+小球的半径(默认为6))2。如果没有进度圆点,则进度圆点的半径为0的情况,canvas的宽高应为(底圆的半径+线条宽度/2(默认为8))2
三.确定圆形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()
})
}
}
那么此时页面上就会渲染出以下的图形,
四.绘制进度圆和进度圆文字
既然是进度圆,则必须告知,进度圆在那个地方结束,在这里就假定一个参数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" />
此时的效果为:
这个时候已经能出现半圆弧的效果,而且能够根据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>
这时候你就可以看到很漂亮的渐变色出现了,具体效果如下
六.给进度圆弧添加动画效果
添加动画效果,我们借助第三方库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