MDN鏈接:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes
既然我們已經設置了 canvas 環境,我們可以深入瞭解如何在 canvas 上繪製。到本文的最後,你將學會如何繪製矩形,三角形,直線,圓弧和曲線,變得熟悉這些基本的形狀。繪製物體到Canvas前,需掌握路徑,我們看看到底怎麼做。
柵格
在我們開始畫圖之前,我們需要了解一下畫布柵格(canvas grid)以及座標空間。上一頁中的HTML模板中有個寬150px, 高150px的canvas元素。如右圖所示,canvas元素默認被網格所覆蓋。通常來說網格中的一個單元相當於canvas元素中的一像素。柵格的起點爲左上角(座標爲(0,0))。所有元素的位置都相對於原點定位。所以圖中藍色方形左上角的座標爲距離左邊(X軸)x像素,距離上邊(Y軸)y像素(座標爲(x,y))。在課程的最後我們會平移原點到不同的座標上,旋轉網格以及縮放。現在我們還是使用原來的設置。
繪製矩形
不同於SVG,HTML中的元素canvas只支持一種原生的圖形繪製:矩形。所有其他的圖形的繪製都至少需要生成一條路徑。不過,我們擁有衆多路徑生成的方法讓複雜圖形的繪製成爲了可能。
首先,我們回到矩形的繪製中。canvas提供了三種方法繪製矩形:
fillRect(x, y, width, height)
- 繪製一個填充的矩形
strokeRect(x, y, width, height)
- 繪製一個矩形的邊框
clearRect(x, y, width, height)
- 清除指定矩形區域,讓清除部分完全透明。
上面提供的方法之中每一個都包含了相同的參數。x與y指定了在canvas畫布上所繪製的矩形的左上角(相對於原點)的座標。width和height設置矩形的尺寸。
下面的draw() 函數是前一頁中取得的,現在就來使用上面的三個函數。
矩形(Rectangular)例子
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="150"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
}
}
該例子的輸出如下圖所示。
Screenshot | Live sample |
---|---|
fillRect()
函數繪製了一個邊長爲100px的黑色正方形。clearRect()
函數從正方形的中心開始擦除了一個60*60px的正方形,接着strokeRect()
在清除區域內生成一個50*50的正方形邊框。
接下來我們能夠看到clearRect()的兩個可選方法,然後我們會知道如何改變渲染圖形的填充顏色及描邊顏色。
不同於下一節所要介紹的路徑函數(path function),以上的三個函數繪製之後會馬上顯現在canvas上,即時生效。
繪製路徑
圖形的基本元素是路徑。路徑是通過不同顏色和寬度的線段或曲線相連形成的不同形狀的點的集合。一個路徑,甚至一個子路徑,都是閉合的。使用路徑繪製圖形需要一些額外的步驟。
- 首先,你需要創建路徑起始點。
- 然後你使用畫圖命令去畫出路徑。
- 之後你把路徑封閉。
- 一旦路徑生成,你就能通過描邊或填充路徑區域來渲染圖形。
以下是所要用到的函數:
beginPath()
- 新建一條路徑,生成之後,圖形繪製命令被指向到路徑上生成路徑。
closePath()
- 閉合路徑之後圖形繪製命令又重新指向到上下文中。
stroke()
- 通過線條來繪製圖形輪廓。
fill()
- 通過填充路徑的內容區域生成實心的圖形。
生成路徑的第一步叫做beginPath()。本質上,路徑是由很多子路徑構成,這些子路徑都是在一個列表中,所有的子路徑(線、弧形、等等)構成圖形。而每次這個方法調用之後,列表清空重置,然後我們就可以重新繪製新的圖形。
第二步就是調用函數指定繪製路徑,本文稍後我們就能看到了。
第三,就是閉合路徑closePath(),不是必需的。這個方法會通過繪製一條從當前點到開始點的直線來閉合圖形。如果圖形是已經閉合了的,即當前點爲開始點,該函數什麼也不做。
繪製一個三角形
例如,繪製三角形的代碼如下:
<html>
<body onload="draw();">
<canvas id="canvas" width="100" height="100"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
ctx.fill();
}
}
輸出看上去如下:
Screenshot | Live sample |
---|---|
移動筆觸
一個非常有用的函數,而這個函數實際上並不能畫出任何東西,也是上面所描述的路徑列表的一部分,這個函數就是moveTo()
。或者你可以想象一下在紙上作業,一支鋼筆或者鉛筆的筆尖從一個點到另一個點的移動過程。
moveTo(x, y)
- 將筆觸移動到指定的座標x以及y上。
當canvas初始化或者beginPath()
調用後,你通常會使用moveTo()
函數設置起點。我們也能夠使用moveTo()
繪製一些不連續的路徑。看一下下面的笑臉例子。我將用到moveTo()
方法(紅線處)的地方標記了。
你可以嘗試一下,使用下邊的代碼片。只需要將其複製到之前的draw()
函數即可。
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="150"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // 繪製
ctx.moveTo(110,75);
ctx.arc(75,75,35,0,Math.PI,false); // 口(順時針)
ctx.moveTo(65,65);
ctx.arc(60,65,5,0,Math.PI*2,true); // 左眼
ctx.moveTo(95,65);
ctx.arc(90,65,5,0,Math.PI*2,true); // 右眼
ctx.stroke();
}
}
結果看起來是這樣的:
Screenshot | Live sample |
---|---|
如果你想看到連續的線,你可以移除調用的moveTo()。
注意:需要學習更多關於arc()函數的內容,請看下面的圓弧
線
繪製直線,需要用到的方法lineTo()
。
lineTo(x, y)
- 繪製一條從當前位置到指定x以及y位置的直線。
該方法有兩個參數:x以及y ,代表座標系中直線結束的點。開始點和之前的繪製路徑有關,之前路徑的結束點就是接下來的開始點,等等。。。開始點也可以通過moveTo()
函數改變。
下面的例子繪製兩個三角形,一個是填充的,另一個是描邊的。
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="150"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
// 填充三角形
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.fill();
// 描邊三角形
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();
}
}
這裏從調用beginPath()
函數準備繪製一個新的形狀路徑開始。然後使用moveTo()
函數移動到目標位置上。然後下面,兩條線段繪製後構成三角形的兩條邊。
Screenshot | Live sample |
---|---|
你會注意到填充與描邊三角形步驟有所不同。正如上面所提到的,因爲路徑使用填充(fill)時,路徑自動閉合,使用描邊(stroke)則不會閉合路徑。如果沒有添加閉合路徑closePath()
到描述三角形函數中,則只繪製了兩條線段,並不是一個完整的三角形。
圓弧
繪製圓弧或者圓,我們使用arc()
方法。當然可以使用arcTo()
,不過這個的實現並不是那麼的可靠,所以我們這裏不作介紹。
arc(x, y, radius, startAngle, endAngle, anticlockwise)
- 畫一個以(x,y)爲圓心的以radius爲半徑的圓弧(圓),從startAngle開始到endAngle結束,按照anticlockwise給定的方向(默認爲順時針)來生成。
arcTo(x1, y1, x2, y2, radius)
- 根據給定的控制點和半徑畫一段圓弧,再以直線連接兩個控制點。
這裏詳細介紹一下arc方法,該方法有六個參數:x,y
爲繪製圓弧所在圓上的圓心座標。radius
爲半徑。startAngle
以及endAngle
參數用弧度定義了開始以及結束的弧度。這些都是以x軸爲基準。參數anticlockwise
爲一個布爾值。爲true時,是逆時針方向,否則順時針方向。
注意:arc()
函數中表示角的單位是弧度,不是角度。角度與弧度的js表達式:
弧度=(Math.PI/180)*角度。
下面的例子比上面的要複雜一下,下面繪製了12個不同的角度以及填充的圓弧。
下面兩個for
循環,生成圓弧的行列(x,y)座標。每一段圓弧的開始都調用beginPath()
。代碼中,每個圓弧的參數都是可變的,實際編程中,我們並不需要這樣做。
x,y座標是可變的。半徑(radius)和開始角度(startAngle)都是固定的。結束角度(endAngle)在第一列開始時是180度(半圓)然後每列增加90度。最後一列形成一個完整的圓。
clockwise
語句作用於第一、三行是順時針的圓弧,anticlockwise
作用於二、四行爲逆時針圓弧。if
語句讓一、二行描邊圓弧,下面兩行填充路徑。
注意: 這個示例所需的畫布大小略大於本頁面的其他例子: 150 x 200 像素。
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="200"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
for(var i=0;i<4;i++){
for(var j=0;j<3;j++){
ctx.beginPath();
var x = 25+j*50; // x 座標值
var y = 25+i*50; // y 座標值
var radius = 20; // 圓弧半徑
var startAngle = 0; // 開始點
var endAngle = Math.PI+(Math.PI*j)/2; // 結束點
var anticlockwise = i%2==0 ? false : true; // 順時針或逆時針
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i>1){
ctx.fill();
} else {
ctx.stroke();
}
}
}
}
}
Screenshot | Live sample |
---|---|
二次貝塞爾曲線及三次貝塞爾曲線
下一個十分有用的路徑類型就是貝塞爾曲線。二次及三次貝塞爾曲線都十分有用,一般用來繪製複雜有規律的圖形。
quadraticCurveTo(cp1x, cp1y, x, y)
- 繪製二次貝塞爾曲線,
cp1x,cp1y
爲一個控制點,x,y爲
結束點。 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
- 繪製三次貝塞爾曲線,
cp1x,cp1y
爲控制點一,cp2x,cp2y
爲控制點二,x,y
爲結束點。
右邊的圖能夠很好的描述兩者的關係,二次貝塞爾曲線有一個開始點(藍色)、一個結束點(藍色)以及一個控制點(紅色),而三次貝塞爾曲線有兩個控制點。
參數x、y在這兩個方法中都是結束點座標。cp1x,cp1y
爲座標中的第一個控制點,cp2x,cp2y
爲座標中的第二個控制點。
使用二次以及三次貝塞爾曲線是有一定的難度的,因爲不同於像Adobe Illustrators這樣的矢量軟件,我們所繪製的曲線沒有給我們提供直接的視覺反饋。這讓繪製複雜的圖形變得十分困難。在下面的例子中,我們會繪製一些簡單有規律的圖形,如果你有時間以及更多的耐心,很多複雜的圖形你也可以繪製出來。
下面的這些例子沒有多少困難。這兩個例子中我們會連續繪製貝塞爾曲線,最後形成複雜的圖形。
二次貝塞爾曲線
這個例子使用多個貝塞爾曲線來渲染對話氣泡。
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="150"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
// 二次貝塞爾曲線
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();
}
}
Screenshot | Live sample |
---|---|
三次貝塞爾曲線
這個例子使用貝塞爾曲線繪製心形。
<html>
<body onload="draw();">
<canvas id="canvas" width="150" height="150"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
//三次貝塞爾曲線
ctx.beginPath();
ctx.moveTo(75,40);
ctx.bezierCurveTo(75,37,70,25,50,25);
ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
ctx.bezierCurveTo(20,80,40,102,75,120);
ctx.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();
}
}
Screenshot | Live sample |
---|---|
矩形
直接在畫布上繪製矩形的三個額外方法,正如我們開始所見的繪製矩形,同樣,也有rect()方法,將一個矩形路徑增加到當前路徑上。
rect(x, y, width, height)
- 繪製一個左上角座標爲(x,y),寬高爲width以及height的矩形。
當該方法執行的時候,moveTo()方法自動設置座標參數(0,0)。也就是說,當前筆觸自動重置回默認座標。
組合使用
目前爲止,每一個例子中的每個圖形都只用到一種類型的路徑。然而,繪製一個圖形並沒有限制使用數量以及類型。所以在最後的一個例子裏,讓我們組合使用所有的路徑函數來重現一款著名的遊戲。
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
roundedRect(ctx,12,12,150,150,15);
roundedRect(ctx,19,19,150,150,9);
roundedRect(ctx,53,53,49,33,10);
roundedRect(ctx,53,119,49,16,6);
roundedRect(ctx,135,53,49,33,10);
roundedRect(ctx,135,119,25,49,10);
ctx.beginPath();
ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,false);
ctx.lineTo(31,37);
ctx.fill();
for(var i=0;i<8;i++){
ctx.fillRect(51+i*16,35,4,4);
}
for(i=0;i<6;i++){
ctx.fillRect(115,51+i*16,4,4);
}
for(i=0;i<8;i++){
ctx.fillRect(51+i*16,99,4,4);
}
ctx.beginPath();
ctx.moveTo(83,116);
ctx.lineTo(83,102);
ctx.bezierCurveTo(83,94,89,88,97,88);
ctx.bezierCurveTo(105,88,111,94,111,102);
ctx.lineTo(111,116);
ctx.lineTo(106.333,111.333);
ctx.lineTo(101.666,116);
ctx.lineTo(97,111.333);
ctx.lineTo(92.333,116);
ctx.lineTo(87.666,111.333);
ctx.lineTo(83,116);
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.moveTo(91,96);
ctx.bezierCurveTo(88,96,87,99,87,101);
ctx.bezierCurveTo(87,103,88,106,91,106);
ctx.bezierCurveTo(94,106,95,103,95,101);
ctx.bezierCurveTo(95,99,94,96,91,96);
ctx.moveTo(103,96);
ctx.bezierCurveTo(100,96,99,99,99,101);
ctx.bezierCurveTo(99,103,100,106,103,106);
ctx.bezierCurveTo(106,106,107,103,107,101);
ctx.bezierCurveTo(107,99,106,96,103,96);
ctx.fill();
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(101,102,2,0,Math.PI*2,true);
ctx.fill();
ctx.beginPath();
ctx.arc(89,102,2,0,Math.PI*2,true);
ctx.fill();
}
}
// 封裝的一個用於繪製圓角矩形的函數.
function roundedRect(ctx,x,y,width,height,radius){
ctx.beginPath();
ctx.moveTo(x,y+radius);
ctx.lineTo(x,y+height-radius);
ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
ctx.lineTo(x+width-radius,y+height);
ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
ctx.lineTo(x+width,y+radius);
ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
ctx.lineTo(x+radius,y);
ctx.quadraticCurveTo(x,y,x,y+radius);
ctx.stroke();
}
結果畫面如下:
Screenshot | Live sample |
---|---|
我們不會很詳細地講解上面的代碼,因爲事實上這很容易理解。重點是繪製上下文中使用到了fillStyle屬性,以及封裝函數(例子中的roundedRect()
)。使用封裝函數對於減少代碼量以及複雜度十分有用。
在稍後的課程裏,我們會討論fillStyle
樣式的更多細節。這章節中,我們對fillStyle
樣式所做的僅是改變填充顏色,由默認的黑色到白色,然後又是黑色。
Path2D 對象
正如我們在前面例子中看到的,你可以使用一系列的路徑和繪畫命令來把對象“畫”在畫布上。爲了簡化代碼和提高性能,Path2D
對象已可以在較新版本的瀏覽器中使用,用來緩存或記錄繪畫命令,這樣你將能快速地回顧路徑。
怎樣產生一個Path2D對象呢?
Path2D()
Path2D()
會返回一個新初始化的Path2D對象(可能將某一個路徑作爲變量——創建一個它的副本,或者將一個包含SVG path數據的字符串作爲變量)。
new Path2D(); // 空的Path對象
new Path2D(path); // 克隆Path對象
new Path2D(d); // 從SVG建立Path對象
所有的路徑方法比如moveTo
, rect
, arc
或quadraticCurveTo
等,如我們前面見過的,都可以在Path2D中使用。
Path2D API 添加了 addPath
作爲將path
結合起來的方法。當你想要從幾個元素中來創建對象時,這將會很實用。比如:
Path2D.addPath(path [, transform])
- 添加了一條路徑到當前路徑(可能添加了一個變換矩陣)。
Path2D 示例
在這個例子中,我們創造了一個矩形和一個圓。它們都被存爲Path2D對象,後面再派上用場。隨着新的Path2D API產生,幾種方法也相應地被更新來使用Path2D對象而不是當前路徑。在這裏,帶路徑參數的stroke
和fill
可以把對象畫在畫布上。
<html>
<body onload="draw();">
<canvas id="canvas" width="130" height="100"></canvas>
</body>
</html>
function draw() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
var rectangle = new Path2D();
rectangle.rect(10, 10, 50, 50);
var circle = new Path2D();
circle.moveTo(125, 35);
circle.arc(100, 35, 25, 0, 2 * Math.PI);
ctx.stroke(rectangle);
ctx.fill(circle);
}
}
Screenshot | Live sample |
---|---|
使用 SVG paths
新的Path2D API有另一個強大的特點,就是使用SVG path data來初始化canvas上的路徑。這將使你獲取路徑時可以以SVG或canvas的方式來重用它們。
這條路徑將先移動到點 (M10 10)
然後再水平移動80個單位(h 80)
,然後下移80個單位 (v 80)
,接着左移80個單位 (h -80)
,再回到起點處 (z
)。你可以在Path2D constructor 查看這個例子。
var p = new Path2D("M10 10 h 80 v 80 h -80 Z");