需求的來源通常是工作,這次要完成功能核心是一個向量圖逾一,嗯...其實就是一個定制化的儀表盤铸本,換做平時一個上午就能搞定,只要數(shù)據(jù)給到順手測試了就交貨遵堵,但這次卻有些不一樣箱玷,這次要完成的,是一個微信小程序陌宿。嗯锡足,小程序有什么不同呢?答案是小程序沒有好用的儀表盤插件庫壳坪,網(wǎng)上搜了一下感覺都不太符合需求舶得,沒法子只能自己手動搞一個,但沒關(guān)系爽蝴,我甚至還有一點點激動沐批,因為可以乘機玩一下Canvas。
需求及目標(biāo)
這里繪制一個半徑RADIUS = 300
的外圓蝎亚,為了實驗我將用常規(guī)的Web代碼編寫九孩。最終完成品如下。
向量圖
解決的問題
目標(biāo)清晰了颖对,就大概知道怎么走了捻撑。從圖中可以看出整個圖就是一個表盤+表針的表現(xiàn)形式,細(xì)節(jié)在于表盤和表針的樣式缤底,接下來我們一一分析
1. 表盤的制作
可以看到表盤的中心是空的顾患,圓邊有刻度線,且還有相應(yīng)的刻度標(biāo)識个唧。那么問題來了:
- 如何讓表盤的中心看起來像空江解?這個問題很簡單,畫兩個圓即可徙歼。首先畫外圓犁河,選擇填充樣式鳖枕,具體代碼如下
ctx.translate(RADIUS, RADIUS); //坐標(biāo)原點
ctx.beginPath();
ctx.arc(0, 0, RADIUS - 2, 0, 2 * PI); //繪制圓
ctx.strokeStyle = '#3F51B5'; //邊框樣式
ctx.fillStyle = '#EEEEEE'; //填充樣式
ctx.fill();
ctx.stroke();
ctx.closePath();
其次在外圓的基礎(chǔ)上繪制一個空白的內(nèi)圓,效果便出來了
ctx.beginPath();
ctx.arc(0, 0, RADIUS * 2 / 3, 0, 2 * PI); //繪制圓
ctx.strokeStyle = '#3F51B5';
ctx.fillStyle = '#fff';
ctx.fill()
ctx.stroke();
ctx.closePath();
基礎(chǔ)空心圓
-
如何畫刻度線桨螺?
一個圓有360°宾符,也就是說要在圓弧上標(biāo)上360個刻度(廢話)。這里需要的做法是每畫一個刻度則旋轉(zhuǎn)一度灭翔,但這里要注意魏烫,rotate(deg)
需要傳入的是弧度,所以需要做一個轉(zhuǎn)化肝箱。但首先哄褒,弧度的概念了解一下:來自維基百科的弧度定義
可以從上面推出,假設(shè)我們要旋轉(zhuǎn)一度煌张,則需要做如下操作ctx.rotate((PI / 180 * i) - (PI / 2));
呐赡。另外需要注意的一個點,每次對畫布改變狀態(tài)之前都要調(diào)用save()
方法骏融,保存上一幀的畫面链嘀,待操作完畢后調(diào)用restore()
恢復(fù),刻度代碼如下
//表盤刻度
for (let i = -180; i < 180; i++) {
ctx.save();
ctx.rotate((PI / 180 * i) - (PI / 2)); //旋轉(zhuǎn)坐標(biāo)軸
if (i % 20 === 0) {
ctx.fillText(-i, RADIUS - 40, 0); //每20個刻度打上刻度標(biāo)簽
}
ctx.beginPath();
ctx.moveTo(RADIUS - 17, 0);
ctx.lineTo(RADIUS - 7, 0);
ctx.lineWidth = (i % 20) !== 0 ? 1 : 2; //每20個指針加粗一次
ctx.strokeStyle = (i % 20) !== 0 ? '#BDBDBD' : '#2196F3';
ctx.stroke();
ctx.closePath();
ctx.restore();
}
最終效果如下:
表盤
2. 指針的制作
用canvas畫一根線是不難的绎谦,一個路徑API調(diào)用就可以了管闷,但是,要怎么畫一個有箭頭的線呢窃肠?這個想法其實不難,就是通過在線末端畫多兩根線就好了刷允,難點在于怎么確定箭頭兩個線的坐標(biāo)及角度的問題冤留,這里就需要用到三角函數(shù)來定位了。具體可參考這篇教程
3. 跟隨數(shù)據(jù)的轉(zhuǎn)動
……這個在有需要的時候調(diào)用該函數(shù)就可以了树灶,比如在我自己的項目中纤怒,我需要根據(jù)數(shù)據(jù)的變動來調(diào)整,則在請求數(shù)據(jù)后調(diào)用該方法即可
完整代碼
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const PI = Math.PI;
const RADIUS = 300; //半徑
/**
* 渲染表盤
* @param {CanvasRenderingContext2D} ctx
*/
function renderDial(ctx) {
ctx.clearRect(0, 0, RADIUS * 2, RADIUS * 2);
ctx.save();
//外圓定中心
ctx.translate(RADIUS, RADIUS); //坐標(biāo)原點
ctx.beginPath();
ctx.arc(0, 0, RADIUS - 2, 0, 2 * PI); //繪制圓
ctx.strokeStyle = '#3F51B5';
ctx.fillStyle = '#EEEEEE'
ctx.fill();
ctx.stroke();
ctx.closePath();
//內(nèi)圓
ctx.beginPath();
ctx.arc(0, 0, RADIUS * 2 / 3, 0, 2 * PI); //繪制圓
ctx.strokeStyle = '#EEEEEE';
ctx.fillStyle = '#fff';
ctx.fill()
ctx.stroke();
ctx.closePath();
//中心點
ctx.beginPath();
ctx.arc(0, 0, 10, 0, 2 * PI);
ctx.fillStyle = '#000';
ctx.fill()
ctx.closePath();
//表盤刻度
for (let i = -180; i < 180; i++) {
ctx.save();
ctx.rotate((PI / 180 * i) - (PI / 2)); //旋轉(zhuǎn)坐標(biāo)軸
if (i % 20 === 0) {
ctx.fillText(-i, RADIUS - 40, 0);
}
ctx.beginPath();
ctx.moveTo(RADIUS - 17, 0);
ctx.lineTo(RADIUS - 7, 0);
ctx.lineWidth = (i % 20) !== 0 ? 1 : 2; //每20個指針加粗一次
ctx.strokeStyle = (i % 20) !== 0 ? '#BDBDBD' : '#2196F3';
ctx.stroke();
ctx.closePath();
ctx.restore();
}
ctx.restore();
}
/**
* 渲染表針
* @param {CanvasRenderingContext2D} ctx
*/
function renderHands(ctx, angleA, angleB, angleC) {
//角度轉(zhuǎn)化成弧度
let angle = PI / 180 * angleA;
let angle1 = PI / 180 * angleB;
let angle2 = PI / 180 * angleC;
// drawHand(angle, 250, 5, '#2196F3', ctx)
drawArrow(ctx, angle, RADIUS * 5 / 6, 5, 15, 20, '#2196F3', 'A')
drawArrow(ctx, -angle1, RADIUS * 5 / 6, 5, 15, 20, '#2196F3', 'B')
drawArrow(ctx, -angle2, RADIUS * 5 / 6, 5, 15, 20, '#2196F3', 'C')
}
/**
* 渲染表針
* @param {CanvasRenderingContext2D} ctx
* @param {Number} angle 弧度
* @param {Number} len 線長
* @param {Number} width 線寬度
* @param {Number} theta 三角斜邊 —— 直線夾角
* @param {Number} headlen 三角斜邊長度
* @param {String} color 線條顏色
*/
function drawArrow(ctx, angle, len, width, theta, headlen, color, text) {
let arrowAngle = Math.atan2(0, len) * 180 / Math.PI,
angle1 = (arrowAngle + theta) * PI / 180,
angle2 = (arrowAngle - theta) * PI / 180,
topX = headlen * Math.cos(angle1),
topY = headlen * Math.sin(angle1),
botX = headlen * Math.cos(angle2),
botY = headlen * Math.sin(angle2);
ctx.save();
ctx.translate(RADIUS, RADIUS);
ctx.rotate(-Math.PI / 2 + angle);
ctx.beginPath();
let arrowX = -4 - topX,
arrowY = 0 - topY;
// ctx.moveTo(arrowX, arrowY);
/* 繪制直線 */
ctx.moveTo(-4, 0);
ctx.lineTo(len, 0);
arrowX = len - topX;
arrowY = topY;
ctx.moveTo(arrowX, arrowY);
ctx.lineTo(len, 0);
arrowX = len - botX;
arrowY = botY;
ctx.lineTo(arrowX, arrowY);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
ctx.closePath();
ctx.restore()
ctx.save();
ctx.translate(RADIUS, RADIUS)
ctx.rotate(-Math.PI / 2 + angle);
ctx.translate(len - 50, 0);
ctx.rotate(90 * Math.PI / 180);
ctx.font = '16px bold sans-serif'
ctx.fillText(text, -20, -10);
ctx.restore();
}
renderDial(ctx)
renderHands(ctx, 0, -120, 120);