Canvas學習:繪製圓和圓弧

圓和圓弧是圖形中基本圖形之一,今天我們來了解在Canvas中怎麼繪製圓和圓弧。在Canvas中繪製圓和圓弧其實和繪製線段矩形一樣的簡單。在Canvas中,CanvasRenderingContext2D對象提供了兩個方法(arc()arcTo())來繪製圓和圓弧。

與圓和圓弧相關的基礎知識

在學習如何繪製圓和圓弧之前,有一些相關的基礎知識有必要先進行了解。

  • 角度旋轉
  • 角度和弧度
  • 正切

角度旋轉

座標系中,旋轉分爲順時針和逆時針兩個方向旋轉:

角度和弧度

在CSS中,做旋轉常用到的都是角度(deg)。但在Canvas中繪製圓或圓弧時用到的是弧度(rad)。維基百科中是這樣描述弧度的:

弧度又稱弳度,是平面角的單位,也是國際單位制導出單位。單位弧度定義爲圓弧長度等於半徑時的圓心角。角度以弧度給出時,通常不寫弧度單位,或有時記爲rad

一個完整的圓的弧度是,所以2π rad = 360°1 π rad = 180°1°=π/180 rad1 rad = 180°/π(約57.29577951°)。以度數表示的角度,把數字乘以π/180便轉換成弧度;以弧度表示的角度,乘以180/π便轉換成度數。

rad = (π / 180) * deg

同樣的:

deg = (rad * 180) / π

平時我們常看到的各種弧度如下:

JavaScript中弧度角度換算

僅難了解角度和弧度之間的關係是不夠的,我們還需要知道怎麼使用JavaScript來實現角度和弧度之間的換算。一個π大約是3.141592653589793,在JavaScript中對應的是Math.PI。那麼角度和弧度之間的換算:

rad = (Math.PI * deg) / 180

同樣的:

deg = (rad * 180) / Math.PI

爲了方便計算和使用,可以將其封裝成JavaScript函數:

function getRads (degrees) {
    return (Math.PI * degrees) / 180;
}

function getDegrees (rads) {
    return (rads * 180) / Math.PI;
}

比如我們要將30deg轉換成rad,可以直接使用:

getRads(30); // 0.5235987755982988rad
getDegrees(0.7853981633974483); // 45deg

下圖展示了常見的角度和弧度之間的換算:

正切

正切(Tangent,tan,也作tg)是三角函數的一種。它是週期函數,其最小正週期爲πMath.PI)。正切函數是奇函數

在Canvas中常常需要和三角函數打交道,這也說明了數學是多麼的重要,真後悔當初沒有認真學。有關於Canvas中三角函數的運用,後面我們將會花很大的篇幅來介紹。

爲什麼在畫圓要提到正切呢?那是因爲我們後面在介紹artTo()時會涉及到正切相關的知識。下圖可以說明,正切和圓以及圓弧之間的關係,看上去一點複雜,但不用急於求成,後面會慢慢懂的:

有了這些基礎,我們就可以開始學習在Canvas中怎麼畫圓和圓弧了。這也是這篇文章真正的主題,如果你等不及了,那繼續往後閱讀。

arc()方法

先來看arc()方法怎麼繪製圓和圓弧。Canvas中的arc()方法接受六個參數:

arc(x, y, radius, startRad, endRad, [anticlockwise]);

在Canvas畫布上繪製以座標點(x,y)爲圓心、半麼爲radius的圓上的一段弧線。這段弧線的起始弧度是startRad,結束弧度是endRad。這裏的弧度是以x軸正方向爲基準、進行順時針旋轉的角度來計算。其中anticlockwise表示arc()繪製圓或圓弧是以順時針還是逆時針方向開始繪製。如果其值爲true表示逆時針,如果是false表示爲順時針。該參數是一個可選參數,如果沒有顯式設置,其值是false(也是anticlockwise的默認值)。

記得當初我們學數時,圓的周長與半徑的關係是:C = πdC = 2πr。具備這些基礎,我們就可以使用arc()繪製弧線或圓了。

繪製弧線

先來看arc()繪製弧線,根據上面介紹的內容,傳對應參數給他:

function drawScreen () {
    // x,y => 圓心座標點
    // r => 圓弧半徑
    var arc = {
        x: myCanvas.width / 2, 
        y: myCanvas.height / 2,
        r: 100
    },
        w = myCanvas.width,
        h = myCanvas.height;

    ctx.save();
    ctx.lineWidth = 10;
    ctx.strokeStyle = '#e3f';

    // startRad => getRads(-45)
    // endRad => getRads(45)
    // 順時針旋轉
    ctx.beginPath();
    ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45));
    ctx.stroke();


    // startRad => getRads(-135)
    // endRad => getRads(135)
    // 逆時針旋轉
    ctx.beginPath();
    ctx.strokeStyle = "#f36";
    ctx.arc(arc.x, arc.y, arc.r,getRads(-135),getRads(135),true);
    ctx.stroke();
    ctx.restore();

}

當我們把上面的stroke()fill(),上面的效果就不是一個弧線了:

另外在stroke()之前調用closePath(),那麼弧線的起始點和終止點將會以一條直接連接在一起。比如上面的示例,加上之後的效果:

ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45));
ctx.closePath();
ctx.stroke();

arc()繪製弧線是不是很簡單,在實際中,藉助一些條件循環,我們可以做一些有意思的效果。比如下面的這個示例,使用arc()繪製一個聲波波率放大圖:

function drawScreen () {

    var arc = {
        x: myCanvas.width / 2,
        y: myCanvas.height / 2,
        r: 10
    },
        w = myCanvas.width,
        h = myCanvas.height;

    ctx.save();
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#e3f';

    for(var i = 0;i < 10; i++){
        ctx.beginPath();
        ctx.arc(arc.x, arc.y, arc.r * i,getRads(-45),getRads(45));
        ctx.stroke();

        ctx.beginPath();
        ctx.arc(arc.x, arc.y, arc.r * i,getRads(-135),getRads(135),true);
        ctx.stroke();
    }
}

特別注意:

  • 使用arc()繪製圖形時,如果沒有設置moveTo()那麼會從圓弧的開始的點(startRad處)作爲起始點。如果設置了moveTo(),那麼該點會連線到圓弧起始點。
  • 如果使用stroke()方法,那麼會從開始連線到圓弧的起始位置。 如果是 fill 方法, 會自動閉合路徑填充

繪製制圓

使用arc繪製圓和繪製圓弧是一樣的,只不過繪製圓的時候startRadendRad是相同的。比如:

function drawScreen () {

    var arc = {
        x: myCanvas.width / 2,
        y: myCanvas.height / 2,
        r: 50
    },
        w = myCanvas.width,
        h = myCanvas.height;

    ctx.save();
    ctx.lineWidth = 2;
    ctx.strokeStyle = '#fff';
    ctx.fillStyle = '#000';
    // 繪製一個邊框圓
    ctx.beginPath();
    ctx.arc(arc.x / 2, arc.y, arc.r, getRads(0), getRads(360), false);
    ctx.stroke();

    // 繪製一個閉合邊框圓
    ctx.beginPath();
    ctx.arc(arc.x, arc.y, arc.r, getRads(0), getRads(360), false);
    ctx.closePath();
    ctx.stroke();
    // 繪製一個填充圓
    ctx.beginPath();
    ctx.arc(arc.x * 1.5, arc.y, arc.r,getRads(0), getRads(360), true);
    ctx.fill();
    //繪製一個帶邊框填充的圓
    ctx.beginPath();
    ctx.arc(arc.x, arc.y, arc.r / 2,getRads(0), getRads(360), false);
    ctx.stroke();
    ctx.fill();
    ctx.restore();

}

來做個小練習,使用arc()繪製一個太極圖:

  • 繪製一個白色和黑色大半圓,拼成一個圓形
  • 繪製制一個小的白色半圓和另一個黑色小半圓
  • 繪製一個白色和黑色小圓點

這幾個組合起來,就是我們想要的太極圖:

function drawScreen () {

    var arc = {
        x: myCanvas.width / 2,
        y: myCanvas.height / 2,
        r: 100
    },
        w = myCanvas.width,
        h = myCanvas.height;

    ctx.save();
    ctx.lineWidth = 1;

    // 繪製白色大圓
    ctx.beginPath();
    ctx.fillStyle = '#fff';
    ctx.arc(arc.x, arc.y, arc.r, getRads(-90), getRads(90), false);
    ctx.fill();

    // 繪製黑色大圓
    ctx.beginPath();
    ctx.fillStyle = '#000';
    ctx.arc(arc.x, arc.y, arc.r, getRads(-90), getRads(90), true);
    ctx.fill();

    // 繪製白色小圓
    ctx.beginPath();
    ctx.fillStyle = '#fff';
    ctx.arc(arc.x, arc.y - arc.r/2, arc.r / 2,getRads(-90), getRads(90), true);
    ctx.fill();

    // 繪製黑色小圓
    ctx.beginPath();
    ctx.fillStyle = '#000';
    ctx.arc(arc.x, arc.y + arc.r/2, arc.r / 2,getRads(-90), getRads(90), false);
    ctx.fill();

    // 繪製小黑點
    ctx.beginPath();
    ctx.fillStyle = '#000';
    ctx.arc(arc.x, arc.y - arc.r/2, arc.r / 10,getRads(0), getRads(360), false);
    ctx.fill();

    // 繪製小白點
    ctx.beginPath();
    ctx.fillStyle = '#fff';
    ctx.arc(arc.x, arc.y + arc.r/2, arc.r / 10,getRads(0), getRads(360), false);
    ctx.fill();
  }

繪製扇形

使用arc()除了可以繪製弧線和圓之外,還可以繪製扇形。繪製扇形關鍵點是通過moveTo()把起始點位置設置爲圓心處,然後通過closePath()閉合路徑。

function drawScreen () {

    var arc = {
        x: myCanvas.width / 2,
        y: myCanvas.height / 2,
        r: 100
    },
        w = myCanvas.width,
        h = myCanvas.height;

    ctx.save();
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#e3f';
    ctx.fillStyle = '#e3f';

    ctx.beginPath();
    // 起始點設置在圓心處
    ctx.moveTo(arc.x, arc.y);
    ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45));
    // 閉合路徑
    ctx.closePath();
    ctx.stroke();

    ctx.beginPath();
    // 起始點設置在圓心處
    ctx.moveTo(arc.x, arc.y);
    ctx.arc(arc.x, arc.y, arc.r,getRads(-135),getRads(135),true);
    // 閉合路徑
    ctx.closePath();
    ctx.fill();

    ctx.restore();
}

利用這個原理,可以很輕鬆的實現一個餅圖效果。

特別聲明:arc()方法中的起始弧度參數startRad和結束弧度參數endRad都是以弧度爲單位,即使你填入一個數字,例如360,仍然會被看作是360弧度。

arcTo()方法

前面學習了arc()方法如何繪製弧線、圓或扇形等。在Canvas中CanvasRenderingContext2D還提供了另一個方法arcTo()用來繪製弧線,但arcTo()繪製不出圓。爲什麼呢?接下來,咱們就來了解arcTo()的使用方法。

arcTo()接受五個參數:

arcTo(x1, y1, x2, y2, radius)

arcTo()方法將利用當前端點、端點一(x1, y1)和端點二(x2, y2)這三點所形成的夾角,然後繪製一段與夾角的兩邊相切並且半徑爲radius的圓上的弧線。弧線的起點就是當前端點所在邊與圓的切點,弧線的終點就是商端點二(x2,y2)所在邊與圓的切點,並且繪製的弧線是兩個切點之間長度最短的那個圓弧。此外,如果當前端點不是弧線起點,arcTo()方法還將添加一條當前端點到弧線起點的直線線段。

上面理解起來有點喫力。事實上,arcTo()儘管是通過兩點和半徑繪製弧線或圓,但事實上是有三個點參與。也就是說,不管是否調用arcTo(),其實就有一個點已經存在(x0,y0)。這樣就(x0,y0)(x1,y1)構成一線,然後(x1,y1)(x2,y2)構成一線,這兩條線交叉點就是(x1,y1)。然後以radius繪製的圓弧或圓都會與這兩條線相切。這也就是在文章開頭提正切的基礎。

或者換下圖,你能更好的理解。

arcTo() 函數中,雖然參數只涉及到 P1也就是參數中的(x1,y1)P2也就是參數中的(x2,y2) 兩個點,實際上還有一個隱含的點,就是畫布上的當前點(P0)也就是前面所說的(x0,y0)。當 P0, P1, P2 不重疊也不在一條直線的時候,這 3 個點可以構成一個三角形。想象一下,從 P0 開始,向 P1 畫一條線段,從 P1 開始到 P2 再畫一條線段,這兩條線段形成一個夾角,然後以 r 畫一個圓,移動這個圓將這個圓與線段 P0P1 和線段 P1P2 相切(也可能切點是在 P0P1 或者 P1P2 的延長線上),然後保留朝向 P1 這個點的弧線,就是 arcTo() 在弧線這部分做的事情。

實際繪製是這樣的:

  • moveTo() 給出 P0 點座標
  • arcTo() 函數中的參數給出了 P1 點和 P2 點的座標,以及圓形的半徑 r
  • 計算以 r 爲半徑的圓和直線 P0P1 以及 P1P2 的切點,記爲 S 點和 E 點,對應圖上 StartEnd 兩個點
  • P0S 點畫出一條線段
  • S 點到 E 點,畫出一段圓弧,半徑爲 r
  • 此時,畫布當前點爲 E
  • 然後從 E 點又畫了一條線段到 P2 點,至此 arcTo 的工作已經完成,接下來你可以 stroke() 或者 fill()

來看一個繪製過程:

function drawScreen () {
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#f36';
    ctx.fillStyle = 'red';

    // 一個起始點 ( 100, 50 ), 那麼繪製其點. 顏色設置爲紅色
    ctx.fillRect( 100 - 4, 50 - 4, 8, 8 );
    // 兩個參考點分別爲 ( 100, 200 ) 和 ( 300, 200 ), 繪製出該點
    ctx.fillRect( 100 - 4, 200 - 4, 8, 8 );
    ctx.fillRect( 300 - 4, 200 - 4, 8, 8 );

    // 連接兩個參考點
    ctx.beginPath();
    ctx.strokeStyle = 'red';
    ctx.moveTo( 100, 200 );
    ctx.lineTo( 300, 200 );
    ctx.stroke();

    // 調用 arcTo 方法繪製圓弧. 記得將起始點設置爲 ( 100, 50 )
    ctx.beginPath();
    ctx.strokeStyle = 'blue';
    ctx.moveTo( 100, 50 );
    ctx.arcTo( 100, 200, 300, 200, 80);
    ctx.stroke();
}

繪製帶圓角矩形

在學習Canvas中繪製矩形一節時,我們提到通過lineJoin改變線段端點形狀來模擬一個圓角矩形。通過這樣的方法繪製帶圓角的矩形,侷限性還是非常大的。不過值得慶達的是,acrTo()可以輕易實現兩線內切圓弧,言外之意,使用arcTo()來繪製一個圓角矩形是非常的方便。

圓角矩形是由四段線條和四個1/4圓弧組成,拆解如下。

如此一來,我們就可以封裝一個函數,用來繪製圓角矩形。根據上圖,我們可以給這個函數,比如drawRoundedRect()函數傳遞對應的參數:

  • ctx:Canvas畫布繪圖環境
  • x,y:左上角
  • width:矩形寬度
  • height:矩形高度
  • r:矩形圓角半徑
  • fill: 繪製一個填充的矩形
  • stroke:繪製一個邊框矩形

開始封裝函數:

function drawRoundedRect(ctx, x, y, width, height, fill, stroke) {
    ctx.save();
    ctx.beginPath();

    // draw top and top right corner
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + width, y, x + width, y + r, r);

    // draw right side and bottom right corner
    ctx.arcTo(x + width, y + height, x + width - r, y + height, r);

    // draw bottom and bottom left corner
    ctx.arcTo(x, y + height, x, y + height - r, r);

    // draw left and top left corner
    ctx.arcTo(x, y, x + r, y, r);

    if (fill) {
        ctx.fill();
    }

    if (stroke) {
        ctx.stroke();
    }

    ctx.restore();
}

函數封裝好之後,只需要調用,就可以輕易繪製出帶圓角的矩形,而且簡單易用:

function drawScreen () {

    ctx.strokeStyle = 'rgb(150,0,0)';
    ctx.fillStyle = 'rgb(0,150,0)';
    ctx.lineWidth = 7;
    drawRoundedRect(ctx, 30, 50, 200, 220, 20, true, true);

    ctx.strokeStyle = 'rgb(150,0,150)';
    ctx.fillStyle = 'rgba(0,0,150,0.6)';
    ctx.lineWidth = 7;
    drawRoundedRect(ctx, 300, 100, 250, 150, 8, true, false);
}

是不是很簡單。可以來個複雜點的練習。比如,當初火熱的2048遊戲。

使用drawRoundedRect()函數就可以很輕易的繪製出來,有興趣的同學不仿一試。

繪製月亮

這節主要學習了arc()arcTo()兩個方法,學習了怎麼使用這兩個方法在Canvas中如何繪製圓弧或圓。那麼我們來看一個小示例,結合兩者來繪製一月亮。

用一張圖來闡述繪製月亮的原理:

此圖已經說明一切,直接上代碼吧:

function drawMoon(cxt, d, x, y, R, rot){
    cxt.save();
    cxt.translate(x, y);
    cxt.scale(R, R);
    cxt.rotate(Math.PI / 180 * rot);
    pathMoon(cxt, d);
    cxt.fillStyle = 'hsl' + randomColor();
    cxt.fill();
    cxt.restore();
 }
 //畫路徑
 function pathMoon(cxt, d){
    //D表示控制點的橫座標;
    cxt.beginPath();
    cxt.arc(0, 0, 1, Math.PI * 0.5, Math.PI * 1.5, true);
    cxt.moveTo(0, -1);
    cxt.arcTo(d, 0, 0, 1, dis(0, -1, d, 0) * 1 / d);
    cxt.closePath();
 }

 function dis(x1, y1, x2, y2){
    return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
 }

 function drawScreen () {
    drawMoon(ctx,2,myCanvas.width / 2,myCanvas.height / 2,100,15);
 }

總結

在Canvas中,CanvasRenderingContext2D對象提供了兩個方法(arc()arcTo())來繪製圓和圓弧。其中arc()即可繪製弧線,圓,也可以繪製扇形,但arcTo()僅能繪製出弧線。但arcTo()可以更輕易的幫助我們實現帶圓角的矩形。到目前爲止,我們學習了在Canvas中繪製線段、矩形、弧線和圓等,但基本圖形不僅僅這些,接下來我們將學習如何在Canvas中繪製箭頭。感興趣的同學,歡迎持續關注相關更新。

著作權歸作者所有。
商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
原文: http://www.w3cplus.com/canvas/drawing-arc-and-circle.html © w3cplus.com

個人建了前端學習羣,旨在一起學習前端。純淨、純粹技術討論,非前端人員勿擾!入羣加我微信iamaixiaoxiao。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章