圓和圓弧是圖形中基本圖形之一,今天我們來了解在Canvas中怎麼繪製圓和圓弧。在Canvas中繪製圓和圓弧其實和繪製線段和矩形一樣的簡單。在Canvas中,CanvasRenderingContext2D
對象提供了兩個方法(arc()
和arcTo()
)來繪製圓和圓弧。
與圓和圓弧相關的基礎知識
在學習如何繪製圓和圓弧之前,有一些相關的基礎知識有必要先進行了解。
- 角度旋轉
- 角度和弧度
- 正切
角度旋轉
在座標系中,旋轉分爲順時針和逆時針兩個方向旋轉:
角度和弧度
在CSS中,做旋轉常用到的都是角度(deg
)。但在Canvas中繪製圓或圓弧時用到的是弧度(rad
)。維基百科中是這樣描述弧度的:
弧度又稱弳度,是平面角的單位,也是國際單位制導出單位。單位弧度定義爲圓弧長度等於半徑時的圓心角。角度以弧度給出時,通常不寫弧度單位,或有時記爲
rad
。
一個完整的圓的弧度是2π
,所以2π rad = 360°
,1 π rad = 180°
,1°=π/180 rad
,1 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 = πd
或C = 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
繪製圓和繪製圓弧是一樣的,只不過繪製圓的時候startRad
和endRad
是相同的。比如:
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
點,對應圖上Start
和End
兩個點 - 從
P0
向S
點畫出一條線段 - 從
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。