Canvas繪製一個相似老版支付寶信用分儀表盤效果

前言

使用了ESM+TS的風格來寫一個相似老版本支付寶信用分的效果(會動!!!);javascript

一開始用的是普通的ES5+的風格來寫,這兩版的代碼都會展現,css

模塊的版本增長了一些細節的考慮,有興趣的看官能夠看看html

效果圖及Demo

具體的效果圖能夠在Codesanbox上看java

Codesanbox : codesandbox.io/s/4rvo5mwxj…git

具體的亮點能夠看READMEgithub

Github: github.com/crper/canva…typescript

能收穫什麼?

代碼寫了一大堆註釋。npm

個人實現思路及編碼姿式,以及一些typescript的用法canvas

代碼

版本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 : github.com/crper/canva…app

總結

公司有這麼個需求,而我之前沒用過Canvas,只能自行爬坑。

整體來講canvas的標準使用姿式並不複雜,複雜點在於數學這塊。

寫這個讓我溫習了的高中數學,ESM模塊的發佈,用了rollup來打包,

很不錯的一個工具,有時間我寫個typescript-rollup-startkit

有不對之處請留言,會及時修復,謝謝閱讀。

相關文章
相關標籤/搜索