您的位置:首页 > 其它

Canvas绘制一个类似老版支付宝信用分仪表盘效果

2019-03-30 18:07 183 查看
版权声明:版权所有:CRPER(crper@outlook.com); 掘金|Github:CRPER; https://blog.csdn.net/bomess/article/details/88916448

前言

使用了

ESM+TS
的风格来写一个类似老版本支付宝信用分的效果(会动!!!);

一开始用的是普通的

ES5+
的风格来写,这两版的代码都会展示,

模块的版本增加了一些细节的考虑,有兴趣的看官可以看看

效果图及Demo

具体的效果图可以在

Codesanbox
上看

Codesanbox : https://codesandbox.io/s/4rvo5mwxjw

具体的亮点可以看

README

Github: https://github.com/crper/canvas-credit-score

能收获什么?

代码写了一大堆注释。

我的实现思路及编码姿势,以及一些

typescript
的用法

代码

版本1: 非
ESM
的风格

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
canvas {
display: block;
margin: 0 auto;
background-image: linear-gradient(to top, #a3bded 0%, #6991c7 100%);
}

#test-action {
width: 200px;
height: 50px;
font-weight: 700;
background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
color: #333;
font-size: 16px;
line-height: 50px;
border-radius: 50px;
text-align: center;
cursor: pointer;
margin: 50px auto;
}
</style>
</head>

<body>
<div id="test-action">点击我看随机效果</div>
<script>
window.addEventListener('DOMContentLoaded', function () {
/** @type {HTMLCanvasElement} */
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let ratio = window.devicePixelRatio; // 像素比
canvas.id = "credit-score";
canvas.width = 375;
canvas.height = 375;
canvas.style.width = "375px";
canvas.style.height = "375px";
document.body.appendChild(canvas);

// 初始化的一些值
// canvas 默认是逆时针行走,开始的角度到结束角度
const initParams = {
w: canvas.width * ratio,  // 画布的宽度
h: canvas.height * ratio, // 画布的高度
x: canvas.width * ratio / 2, // 圆心坐标x,y
y: canvas.width * ratio / 2, //  圆心坐标x,y
startAngle: 165, // 画布的起点
endAngle: 375, // 画布的结束点
currentAngle: 165, // 当前的角度
scoreStart: 450, // 起步分
scoreTarget: 770, // 目标分
scoreMin: 450, // 最低分
scoreMax: 850, // 最高分
scoreEvaDate: "评估日期:2019-04-01", // 评估日期
segAngle: 84, // 总角度分成几等分
stepAngle() { // 每次走多少度
return (this.endAngle - this.startAngle) / this.segAngle;
},
outerTextSeg: 10,  // 数字范围等分
outerText() {
let textGap = Math.ceil(this.scoreRange() / (this.outerTextSeg - 1));
let textArr = Array.from(new Array(this.outerTextSeg), ((item, index) => textGap * index + this.scoreMin));
let textStepAngel = this.angelRange() / (textArr.length - 1);
let textAngelArr = textArr.map((item, index) => this.startAngle + textStepAngel * index);
return {
textArr,
textLenght: textArr.length,
textStepAngel,
textAngelArr
};
},
scoreRange() { // 分数的范围
return this.scoreMax - this.scoreMin;
},
angelRange() {  // 角度范围
return this.endAngle - this.startAngle
},
scoreLevelText(text) {// 信用评级
let threeRangeScore = Math.ceil(this.scoreRange() / 3);
if (text) {
return text;
} else {
if (this.scoreStart <= this.scoreMin + threeRangeScore) {
return '有待提高'
}
if (this.scoreStart > threeRangeScore && this.scoreStart <= this.scoreMin + threeRangeScore * 2) {
return '信用良好'
}
if (this.scoreStart > this.scoreMin + threeRangeScore * 2 && this.scoreStart <= this.scoreMax) {
return '信用极好'
}
}

},
style: {
line: { // 线条颜色控制
initColor: "rgba(255, 191, 150, 0.5)",  // 初始化颜色
activeColor: "#fff", // 高亮的颜色
width: 1  // 线条的出息
},
dashLine: {
initColor: "rgba(255, 191, 150, 0.5)",  // 初始化颜色
activeColor: "#fff", // 高亮的颜色
width: 1  // 线条的粗细
},
text: {  // 文本颜色
outerText: {   // 外环文本
fontSize: 12,
color: "#fff",
},
innerText: {
score: {
fontSize: 36,
color: "#fff",
},
level: {
fontSize: 18,
color: "#fff",
},
date: {
fontSize: 12,
color: "#f2f2f2",
fontWeight: "normal"
}
}
}
}
}

let pointImg = new Image();
pointImg.src = "data:image/svg+xml;base64,PHN2ZyAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMjRweCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiAgZmlsbD0icmdiKDI1NSwgMjU1LCAyNTUpIiBkPSJNOC4wMDAsMjQuMDA1IEMzLjU4MiwyNC4wMDUgLTAuMDAwLDIwLjUyNSAtMC4wMDAsMTYuMjMwIEMtMC4wMDAsMTEuOTM2IDguMDAwLC0wLjAwNSA4LjAwMCwtMC4wMDUgQzguMDAwLC0wLjAwNSAxNS45OTksMTEuOTM2IDE1Ljk5OSwxNi4yMzAgQzE1Ljk5OSwyMC41MjUgMTIuNDE4LDI0LjAwNSA4LjAwMCwyNC4wMDUgWk04LjAwMCwxMi4wNjUgQzUuNjI1LDEyLjA2NSAzLjcwMCwxMy45MzQgMy43MDAsMTYuMjM5IEMzLjcwMCwxOC41NDQgNS42MjUsMjAuNDEzIDguMDAwLDIwLjQxMyBDMTAuMzc1LDIwLjQxMyAxMi4zMDAsMTguNTQ0IDEyLjMwMCwxNi4yMzkgQzEyLjMwMCwxMy45MzQgMTAuMzc1LDEyLjA2NSA4LjAwMCwxMi4wNjUgWiIvPjwvc3ZnPg=="

ctx.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);

// 获取弧度
function getRadian(degrees) {
return Math.PI / 180 * degrees;
}

// 获取度数
function getDegrees(radian) {
return 180 / Math.PI * radian
}

// 获取圆边上点的坐标
function getRadiusPoint(x, y, radius, degrees) {
return {
x1: x + radius * Math.cos(degrees),
y1: y + radius * Math.sin(degrees)
}
}

// 绘制外围文本
function drawOuterText(x, y, text, fontSize = 28, color = "#fff", fontWeight = "normal") {
ctx.beginPath();
ctx.fillStyle = color;
ctx.font = `${fontWeight} ${fontSize * ratio}px Microsoft yahei`;
ctx.textBaseline = 'ideographic';
ctx.textAlign = "left";
ctx.fillText(text, x - ctx.measureText(text).width / 2, y);
}

// 绘制中心文本
function drawInnerText(x, y, text, fontSize = 30, color = "#fff", fontWeight = "bold") {

ctx.save();
ctx.fillStyle = color;
ctx.font = `${fontWeight} ${fontSize * ratio}px Microsoft yahei`;
ctx.textAlign = "center";
ctx.fillText(`${text}`, x, y);
ctx.textBaseline = 'ideographic';
ctx.restore();
}

// 画小圆点
function drawCircle(x, y, fillColor, mode = false) {
ctx.beginPath();
ctx.arc(x, y, initParams.style.line.width * ratio, 0, 2 * Math.PI, mode);
ctx.fillStyle = fillColor;
ctx.fill();
}

// 画水滴
function waterDrop(x = 60, y = 180, rotate = -120) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(getRadian(rotate));
ctx.drawImage(pointImg, 0, 0, 8 * ratio, 12 * ratio);
ctx.restore();

}

// 移动水滴
function moveWaterDrop(x, y, radius, angle) {
const {x1, y1} = getRadiusPoint(x, y, radius, getRadian(angle));
// 为什么要加90°,是因为图片默认是垂直的,我们要扭正他,从坐标系的初始值开始
waterDrop(x1, y1, angle + 90);
}

// 画虚线圆弧
function drawCircleDashLine({color}, {x, y, radius}) {
ctx.beginPath();
for (let i = 1; i <= initParams.segAngle; i++) {
const {x1, y1} = getRadiusPoint(x, y, radius,  getRadian( initParams.startAngle + initParams.stepAngle() * i));
drawCircle(x1, y1, color);
}
}

// 画实线弧
function drawCircleLine({color}, {x, y, radius}) {
ctx.beginPath()
ctx.arc(x, y, radius, getRadian(initParams.startAngle), getRadian(initParams.endAngle));
ctx.strokeStyle = color;
ctx.lineCap = "round";
ctx.lineWidth = 1 * ratio;
ctx.stroke();
}

// 画外围文本线
function drawOuterTextLine({outerText: {textArr, textAngelArr, textStepAngel, textLenght}, color}, {x, y, radius}) {
// 最外围文字层
for (let index = 0; index < textLenght; index++) {
let angle = getRadian(textAngelArr[index]);
const {x1, y1} = getRadiusPoint(x, y,radius, angle);
drawOuterText(x1, y1, textArr[index], initParams.style.text.outerText.fontSize, initParams.style.text.outerText.color)
}
}

// 清除画布内容
function clearCanvas() {
ctx.clearRect(0, 0, initParams.w, initParams.h);
}

// 画底图,也就是初始化的图
function drawBaseMap() {
const {score, level, date} = initParams.style.text.innerText;
clearCanvas();
drawOuterTextLine({
outerText: initParams.outerText(),
}, {
x: initParams.x,
y: initParams.y,
radius: initParams.x * 0.82
});

drawCircleLine(
{
color: initParams.style.line.initColor
},
{
x: initParams.x,
y: initParams.y,
radius: initParams.x * 0.7
});
drawCircleDashLine(
{
color: initParams.style.dashLine.initColor,
}
,
{
x: initParams.x,
y: initParams.y,
radius: initParams.x * 0.6666666
}
);

// 文字位置()
drawInnerText(initParams.x, initParams.y - 10 * ratio, initParams.scoreStart, score.fontSize, score.color);
drawInnerText(initParams.x, initParams.y + 20 * ratio, initParams.scoreLevelText(), level.fontSize, level.color);
drawInnerText(initParams.x, initParams.y + 40 * ratio, initParams.scoreEvaDate, date.fontSize, date.color, date.fontWeight);

// 覆盖实线
drawCircleLine(
{
color: initParams.style.line.activeColor,
},
{
x: initParams.x,
y: initParams.y,
radius: initParams.x * 0.7
});

// 覆盖虚线
drawCircleDashLine(
{
color: initParams.style.dashLine.activeColor,
}
,
{
x: initParams.x,
y: initParams.y,
radius: initParams.x * 0.6666666
}
);

// 移动小水滴
moveWaterDrop(initParams.x, initParams.y, initParams.x * 0.6, initParams.currentAngle);

// 范围内无限render
if (initParams.scoreStart < initParams.scoreTarget) {
// 每次移动角度的度数范围
initParams.currentAngle += initParams.stepAngle();

// 文字变化
// 求出每次移动角度的度数范围要走多少分数
let stepScore = (initParams.scoreRange() * initParams.stepAngle()) / initParams.angelRange();
initParams.scoreStart = initParams.scoreStart + Math.round(stepScore);
if (initParams.scoreStart >= initParams.scoreTarget) {
initParams.scoreStart = initParams.scoreTarget
}
// 求当前角度与分数角度的比较,当前累计的角度小于需要移动到目的地的角度就继续渲染
let stepAngle = initParams.startAngle + (initParams.angelRange() * ((initParams.scoreTarget - initParams.scoreMin)) / initParams.scoreRange());
if (initParams.currentAngle >= stepAngle) return false;
window.requestAnimationFrame(drawBaseMap);
}
}

let st = setTimeout(() => {
clearTimeout(st);
drawBaseMap();
}, 1000);

function randomHexColor() { //随机生成十六进制颜色
var hex = Math.floor(Math.random() * 16777216).toString(16); //生成ffffff以内16进制数
while (hex.length < 6) { //while循环判断hex位数,少于6位前面加0凑够6位
hex = '0' + hex;
}
return '#' + hex; //返回‘#’开头16进制颜色
}

// 重置随机玩玩
document.getElementById('test-action').addEventListener('click', function () {
// 点击重置
initParams.scoreStart = 450;
initParams.scoreTarget = Math.round(Math.random() * 400) + 450;
initParams.currentAngle = 165;
initParams.segAngle = [42, 84, 168, 336][Math.ceil(Math.random() * 3)];
initParams.outerTextSeg = [5, 10, 15, 20][Math.ceil(Math.random() * 3)];
let randomColor = randomHexColor();

initParams.style = {
line: { // 线条颜色控制
initColor: "rgba(255, 191, 150, 0.5)",  // 初始化颜色
activeColor: randomColor, // 高亮的颜色
width: Math.random() * 1 + 1  // 线条的出息
},
dashLine: {
initColor: "rgba(255, 191, 150, 0.5)",  // 初始化颜色
activeColor: randomColor, // 高亮的颜色
width: Math.random() * 1 + 1   // 线条的出息
},
text: {  // 文本颜色
outerText: {   // 外环文本
fontSize: 12,
color: randomColor,
},
innerText: {
score: {
fontSize: 36,
color: randomColor,
},
level: {
fontSize: 18,
color: randomColor,
},
date: {
fontSize: 12,
color: randomColor,
fontWeight: "normal"
}
}
}
}

drawBaseMap()

})
})

</script>
</body>

</html>

版本2:已发布npm,
ESM+TS
的风格

Code : https://github.com/crper/canvas-credit-score/blob/master/src/index.ts

总结

公司有这么个需求,而我以前没用过

Canvas
,只能自行爬坑。

总体来说

canvas
的标准使用姿势并不复杂,复杂点在于数学这块。

写这个让我温习了的高中数学,

ESM
模块的发布,用了
rollup
来打包,

很不错的一个工具,有时间我写个

typescript-rollup-startkit

有不对之处请留言,会及时修复,谢谢阅读。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: