今天咱们来使用canvas绘制一个带动画效果的圆形进度条
知识点包括
- canvas的基本知识点
- 常用的小技巧 缓动公式; 圆弧上某个点的xy坐标计算 ; requestAnimationFrame 的使用
具体的在代码里细说
<canvas id="cav" width="400" height="400"></canvas>
<script>
class circleProgress {
constructor(params) {
/*
需要传入的参数
el canvas 元素
progress 真实的进度比例 0 - 1 之间 100% 就是1;
number 当前进度表示的数额
*/
const { el, progress, number } = params;
//获取canvas 元素 及其 2d context
const cav = document.querySelector(el);
this.ctx = cav.getContext('2d');
// 获取canvas 元素的宽高信息 用来计算圆形进度的 圆点坐标
const { width, height } = cav.getBoundingClientRect();
this.w = width;
this.h = height;
this.x = width / 2;
this.y = height / 2;
this.r = 60;
// Math.PI / 180 是很直观的度数换算公式 -> 度数 * Math.PI / 180;
this.angle = Math.PI / 180;
// 圆形进度的开始 和 结束 路径角度度数
this.beginDeg = 140;
this.endDeg = 40;
// 计算出真实进度条的结束 路径 角度度数
this.progressDeg = this.beginDeg + (360 - this.beginDeg + this.endDeg) * progress;
this.aniDeg = this.beginDeg;
this.number = number;
this.move();
}
/*
获取圆形上某个点的xy坐标
公式:
x = 圆心x坐标 + 半径 * Math.cos(点的角度*Math.PI/180)
y = 圆心y坐标 + 半径 * Math.sin(点的角度*Math.PI/180)
*/
getPointPos(deg) {
const { ctx, x, y, r, angle } = this;
return {
xPos: x + r * Math.cos(deg * angle),
yPos: y + r * Math.sin(deg * angle)
}
}
// 绘制文字
drawFont(font) {
const { ctx, w, h } = this;
// 获取文字的宽度 用已计算文字的x坐标 居中显示
ctx.font = 'bold 30px arial';
const fontW = ctx.measureText(font).width;
ctx.fillStyle = '#00aeef';
ctx.fillText(font, (w - fontW) / 2, h / 2 + 12)
}
// 绘制圆形
drawCircle(color, endDeg) {
const { ctx, x, y, r, angle } = this;
ctx.beginPath();
ctx.lineCap = "round";
ctx.lineWidth = 8;
ctx.strokeStyle = color;
ctx.arc(x, y, r, 140 * angle, endDeg * angle);
ctx.stroke();
ctx.closePath();
}
// 绘制圆点
drawPoint() {
const { ctx, x, y, r, angle, aniDeg } = this;
// 绘制圆形
ctx.beginPath();
ctx.fillStyle = "#00aeef";
const { xPos, yPos } = this.getPointPos(aniDeg)
ctx.arc(xPos, yPos, 8, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
// 动画绘制
move() {
const { ctx, x, y, r, angle, w, h, progressDeg, number } = this;
let font = 0;
let last = false;
const draw = () => {
// canvas 动画美帧绘制前 都需要把上一帧清除掉
ctx.clearRect(0, 0, w, h);
// 绘制背景圆弧
this.drawCircle('#ccc', this.endDeg);
// 绘制真实进度圆弧
this.drawCircle('#00aeef', this.aniDeg);
// 绘制真实进度圆点
this.drawPoint();
// 缓动公式 A = A + (B - A) / 速率;
// 需要注意的是公式会无穷接近B 所以需要加一个临界判断;
this.aniDeg = this.aniDeg + (progressDeg - this.aniDeg) / 20;
font = font + (number - font) / 20;
this.drawFont(Math.floor(font))
if (last) { return; }
if ((progressDeg - this.aniDeg > 1)) {
//使用requestAnimationFrame来实现动画 效果会更加细腻
requestAnimationFrame(draw)
} else {
// 临界判断 最后一次绘制 达到目标进度;
last = true;
this.aniDeg = progressDeg;
font = number;
draw();
}
}
draw();
}
}
const goProgress = new circleProgress({
el: '#cav',
progress: 0.4,
number: 4000
})
</script>
最终效果