Canvas學習:繪制圓和圓弧
圓和圓弧是圖形中基本圖形之一,今天我們來了解在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() 可以更輕易的幫助我們實現帶圓角的矩形。
來自:http://www.w3cplus.com/canvas/drawing-arc-and-circle.html