用Canvas畫個圓盤

需求的來源通常是工作,這次要完成功能核心是一個向量圖逾一,嗯...其實就是一個定制化的儀表盤铸本,換做平時一個上午就能搞定,只要數(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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末天通,一起剝皮案震驚了整個濱河市泊窘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌像寒,老刑警劉巖烘豹,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诺祸,居然都是意外死亡携悯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門筷笨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憔鬼,“玉大人龟劲,你說我怎么就攤上這事≈峄颍” “怎么了昌跌?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長照雁。 經(jīng)常有香客問我蚕愤,道長,這世上最難降的妖魔是什么囊榜? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任审胸,我火速辦了婚禮,結(jié)果婚禮上卸勺,老公的妹妹穿的比我還像新娘砂沛。我一直安慰自己,他們只是感情好曙求,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布碍庵。 她就那樣靜靜地躺著,像睡著了一般悟狱。 火紅的嫁衣襯著肌膚如雪静浴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天挤渐,我揣著相機與錄音苹享,去河邊找鬼。 笑死浴麻,一個胖子當(dāng)著我的面吹牛得问,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播软免,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宫纬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膏萧?” 一聲冷哼從身側(cè)響起漓骚,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎榛泛,沒想到半個月后蝌蹂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡挟鸠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年叉信,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艘希。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡硼身,死狀恐怖硅急,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佳遂,我是刑警寧澤营袜,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站丑罪,受9級特大地震影響荚板,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吩屹,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一跪另、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧煤搜,春花似錦免绿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至迹卢,卻和暖如春辽故,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腐碱。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工誊垢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人症见。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓彤枢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筒饰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容