使用canvas绘制圆环进度条
技术要求
需要一点点数学基础
需要对canvas
的常见的方法熟悉
一点点数学基础
canvas 常见的方法
扬帆起航
- 首先创建一个canvas ,并将这个dom添加在html中
方法名称:
createCanvas
// 需要申明两个全局变量
let isIconLoadSuccess = false;
let iconLoading = null;
// 以下是一个方法
const canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
canvas.style.border = '1px #ccc solid';
// 提前加载图标数据
const imgObj = new Image();
imgObj.src = './success-filling.png';
imgObj.onload = function () {
isIconLoadSuccess = true;
iconLoading = this;
};
return canvas;
- 定义一个绘制圆环的方法
方法名称:
drawCircle(ctx, config)
绘制圆环的思路
使用arc
绘制一个圆形,在其内部填充一个样式,将圆的边框lineWidth
设置的宽度大一点,这个宽度就是圆环的大小,设置strokeStyle
的颜色,该颜色就是圆环的颜色,最后设置连接处样式lineCap
,这里建议将lineCap
设置成round
因为最终想要的是圆环,所以角度应该是(0,360)
最后调用stroke
和closePath
上色和关闭路径
const {
x, y, radius, startAngle, endAngle, color, lineWidth,
} = config;
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle, false);
// 设定曲线粗细度
ctx.lineWidth = lineWidth;
// 给曲线着色
ctx.strokeStyle = color;
// 连接处样式
ctx.lineCap = 'round';
// 给环着色
ctx.stroke();
ctx.closePath();
- 绘制圆环
方法名称:
circle(percent = '0.0')
const { width, height } = canvas;
const ctx = canvas.getContext('2d');
// 清除画布
ctx.clearRect(0, 0, width, height);
// 保存
ctx.save();
/* 填充文字 */
ctx.font = '24px Microsoft YaHei';
/* 文字颜色 */
ctx.fillStyle = '#999';
/* 文字内容 */
const insertContent = '本月任务进度';
// 拿到文本内容的像素相关信息 单位长度(px)
const measureText = ctx.measureText(insertContent);
/* 插入文字,后面两个参数为文字在画布中的坐标点 */
/* 此处注意:text.width获得文字的宽度,然后就能计算出文字居中需要的x值 */
ctx.fillText(insertContent, (width - measureText.width) / 2, (height / 2) + 45);
/* 填充百分比 */
ctx.font = '60px Microsoft YaHei';
ctx.fillStyle = '#222';
const ratioStr = `${(parseFloat(percent) * 100).toFixed(0)} %`;
const text = ctx.measureText(ratioStr);
ctx.fillText(ratioStr, (width - text.width) / 2, (height / 2) + 10);
/* 开始圆环 */
const circleConfig = {
/* 圆心坐标 */
x: width / 2,
y: height / 2,
/* 半径,下方出现的150都是半径 */
radius: (width / 2) - 30,
/* 环的宽度 */
lineWidth: 24,
/* 开始的度数-从上一个结束的位置开始 */
startAngle: 0, // 注意这里的0是3点钟方向,而非12点方向,和数学里的不一样
/* 结束的度数 */
endAngle: 360,
color: '#E7EFF4',
};
/* 灰色的圆环 */
drawCircle(ctx, circleConfig);
/* 有色的圆环 */
const holeCicle = 2 * Math.PI;
const angle = percent * 360; // 圆弧的角度
// 圆心坐标:(x0, y0)
// 半径:r
// 弧度:a => 圆弧计算公式:(角度/180)*Math/.PI
// 则圆上任一点为:(x1, y1)
// x1 = x0 + r * cos(a)
// y1 = y0 + r * sin(a)
const x1 = circleConfig.x + circleConfig.radius * Math.cos(((angle - 90) / 180) * Math.PI) - 25;
const y1 = circleConfig.y + circleConfig.radius * Math.sin(((angle - 90) / 180) * Math.PI) - 25;
// 处理渐变色
const gnt1 = ctx.createLinearGradient(circleConfig.radius * 2, circleConfig.radius * 2, 0, 0);
gnt1.addColorStop(0, '#FF8941');
gnt1.addColorStop(0.3, '#FF8935');
gnt1.addColorStop(1, '#FFC255');
drawCircle(ctx, {
...circleConfig,
/* 从-90度的地方开始画 */
startAngle: -0.5 * Math.PI, // 把起始点改成数学里的12点方向
endAngle: -0.5 * Math.PI + percent * holeCicle,
color: gnt1,
});
// 填充小图标
if (isIconLoadSuccess) {
// 这里的this指的是imgObj,第二三个参数是它的坐标,四五个是长款
ctx.drawImage(iconLoading, x1, y1, 50, 50);
}
- 来一点动画
方法名称:
drawFrame(percent, callback)
// 在该方法之外声明一个全局变量
let speed = 0;
// 以下是 drawFrame 方法体
const id = window.requestAnimationFrame(() => { drawFrame(percent, callback); });
circle(speed.toString());
if (speed >= percent) {
window.cancelAnimationFrame(id);
speed = 0;
if (callback) {
callback();
}
return;
}
speed += 0.01;
- 再来一个快照功能
方法名称:
createImage(src)
const image = new Image();
image.src = src;
return image;
- 调用声明好的方法
const app = document.getElementById('app');
const canvas = createCanvas();
let i = 0;
setInterval(() => {
if (i > 1) {
i = 0;
}
i = Math.random();
drawFrame(i.toString(), () => {
const image = createImage(canvas.toDataURL());
app.appendChild(image);
window.scrollTo(0, document.body.scrollHeight);
});
}, 3000);
app.appendChild(canvas);
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>进度条圆环</title>
<style>
body {
width: 100%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
<script>
// 图标是否加载成功
let isIconLoadSuccess = false;
let iconLoading = null;
function createCanvas() {
const canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
canvas.style.border = '1px #ccc solid';
// 提前加载图标数据
const imgObj = new Image();
imgObj.src = './success-filling.png';
imgObj.onload = function () {
isIconLoadSuccess = true;
iconLoading = this;
};
return canvas;
}
const app = document.getElementById('app');
const canvas = createCanvas();
/* 画曲线 */
function drawCircle(ctx, config) {
const {
x, y, radius, startAngle, endAngle, color, lineWidth,
} = config;
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle, false);
// 设定曲线粗细度
ctx.lineWidth = lineWidth;
// 给曲线着色
ctx.strokeStyle = color;
// 连接处样式
ctx.lineCap = 'round';
// 给环着色
ctx.stroke();
ctx.closePath();
}
function circle(percent = '0.0') {
const { width, height } = canvas;
const ctx = canvas.getContext('2d');
// 清除画布
ctx.clearRect(0, 0, width, height);
// 保存
ctx.save();
/* 填充文字 */
ctx.font = '24px Microsoft YaHei';
/* 文字颜色 */
ctx.fillStyle = '#999';
/* 文字内容 */
const insertContent = '本月任务进度';
// 拿到文本内容的像素相关信息 单位长度(px)
const measureText = ctx.measureText(insertContent);
/* 插入文字,后面两个参数为文字在画布中的坐标点 */
/* 此处注意:text.width获得文字的宽度,然后就能计算出文字居中需要的x值 */
ctx.fillText(insertContent, (width - measureText.width) / 2, (height / 2) + 45);
/* 填充百分比 */
ctx.font = '60px Microsoft YaHei';
ctx.fillStyle = '#222';
const ratioStr = `${(parseFloat(percent) * 100).toFixed(0)} %`;
const text = ctx.measureText(ratioStr);
ctx.fillText(ratioStr, (width - text.width) / 2, (height / 2) + 10);
/* 开始圆环 */
const circleConfig = {
/* 圆心坐标 */
x: width / 2,
y: height / 2,
/* 半径,下方出现的150都是半径 */
radius: (width / 2) - 30,
/* 环的宽度 */
lineWidth: 24,
/* 开始的度数-从上一个结束的位置开始 */
startAngle: 0, // 注意这里的0是3点钟方向,而非12点方向,和数学里的不一样
/* 结束的度数 */
endAngle: 360,
color: '#E7EFF4',
};
/* 灰色的圆环 */
drawCircle(ctx, circleConfig);
/* 有色的圆环 */
const holeCicle = 2 * Math.PI;
const angle = percent * 360; // 圆弧的角度
// 圆心坐标:(x0, y0)
// 半径:r
// 弧度:a => 圆弧计算公式:(角度/180)*Math/.PI
// 则圆上任一点为:(x1, y1)
// x1 = x0 + r * cos(a)
// y1 = y0 + r * sin(a)
const x1 = circleConfig.x + circleConfig.radius * Math.cos(((angle - 90) / 180) * Math.PI) - 25;
const y1 = circleConfig.y + circleConfig.radius * Math.sin(((angle - 90) / 180) * Math.PI) - 25;
// 处理渐变色
const gnt1 = ctx.createLinearGradient(circleConfig.radius * 2, circleConfig.radius * 2, 0, 0);
gnt1.addColorStop(0, '#FF8941');
gnt1.addColorStop(0.3, '#FF8935');
gnt1.addColorStop(1, '#FFC255');
drawCircle(ctx, {
...circleConfig,
/* 从-90度的地方开始画 */
startAngle: -0.5 * Math.PI, // 把起始点改成数学里的12点方向
endAngle: -0.5 * Math.PI + percent * holeCicle,
color: gnt1,
});
// 填充小图标
if (isIconLoadSuccess) {
// 这里的this指的是imgObj,第二三个参数是它的坐标,四五个是长款
ctx.drawImage(iconLoading, x1, y1, 50, 50);
}
}
function createImage(src) {
const image = new Image();
image.src = src;
return image;
}
// 动画函数
let speed = 0;
function drawFrame(percent, callback) {
const id = window.requestAnimationFrame(() => { drawFrame(percent, callback); });
circle(speed.toString());
if (speed >= percent) {
window.cancelAnimationFrame(id);
speed = 0;
if (callback) {
callback();
}
return;
}
speed += 0.01;
}
let i = 0;
setInterval(() => {
if (i > 1) {
i = 0;
}
i = Math.random();
drawFrame(i.toString(), () => {
const image = createImage(canvas.toDataURL());
app.appendChild(image);
window.scrollTo(0, document.body.scrollHeight);
});
}, 3000);
app.appendChild(canvas);
</script>
</html>
使用到的小图标
最终效果图