本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,博客地址為http://www.cnblogs.com/jasonnode/ 。網站上有對應每一小節的在線練習大家可以去試試。
1.路徑、描邊與填充
迄今為止,在本章之中我們所繪制的唯一圖形,就是通過在Canvas的繪圖環境對象上調用strokeRect()方法所畫的矩形。我們也通過調用fillRect()方法對其進行了填充。這兩個方法都是立即生效的。實際上,它們是Canvas繪圖環境中僅有的兩個可以用來立即繪制圖形的方法(strokeText()與fillText()方法也是進行立即繪制的,但文本不算是圖形)。繪圖環境對象中還有一些方法,用於繪制諸如貝塞爾曲線(bézier curve)這樣更為復雜的圖形,這些方法都是基於路徑(path)的。
大多數繪制系統,例如Scalable Vector Graphics(可縮放向量圖形,簡稱SVG)、Apple的Cocoa框架,以及Adobe Illustrator等,都是基於路徑的。使用這些繪制系統時,你需要先定義一個路徑,然后再對其進行描邊(也就是繪制路徑的輪廓線)或填充,也可以在描邊的同時進行填充。圖2-13演示了這三種繪制方式。
該應用程序創建了9個不同的路徑,對左邊一列的路徑進行了描邊操作,對中間一列的路徑進行了填充,並對右邊一列的路徑同時進行描邊與填充。
第一行的矩形路徑與最后一行的圓弧路徑都是封閉路徑(closed path),而中間一行的弧形路徑則是開放路徑(open path)。請注意,不論一個路徑是開放或是封閉,你都可以對其進行填充。當填充某個開放路徑時,瀏覽器會把它當成封閉路徑來填充。圖中右邊一列的中間那個圖形,就是這種效果。
程序清單2-9列出了圖2-13中那個應用程序的代碼。
程序清單2-9 文本、矩形與圓弧的描邊及填充
var context=document.getElementById('drawingCanvas').getContext('2d'); //Functions... function drawGrid(context,color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. } //Initialization... drawGrid(context,'lightgray',10,10); //Drawing attributes... context.font='48pt Helvetica'; context.strokeStyle='blue'; context.fillStyle='red'; context.lineWidth='2'; //Line width set to 2 for text //Text... context.strokeText('Stroke',60,110); context.fillText('Fill',440,110); context.strokeText('Stroke&Fill',650,110); context.fillText('Stroke&Fill',650,110); //Rectangles... context.lineWidth='5'; //Line width set to 5 for shapes context.beginPath(); context.rect(80,150,150,100); context.stroke(); context.beginPath(); context.rect(400,150,150,100); context.fill(); context.beginPath(); context.rect(750,150,150,100); context.stroke(); context.fill(); //Open arcs... context.beginPath(); context.arc(150,370,60,0,Math.PI*3/2); context.stroke(); context.beginPath(); context.arc(475,370,60,0,Math.PI*3/2); context.fill(); context.beginPath(); context.arc(820,370,60,0,Math.PI*3/2); context.stroke(); context.fill(); //Closed arcs... context.beginPath(); context.arc(150,550,60,0,Math.PI*3/2); context.closePath(); context.stroke(); context.beginPath(); context.arc(475,550,60,0,Math.PI*3/2); context.closePath(); context.fill(); context.beginPath(); context.arc(820,550,60,0,Math.PI*3/2); context.closePath(); context.stroke(); context.fill();
首先調用beginPath()方法來開始一段新的路徑,rect()與arc()方法分別用於創建矩形及弧形路徑。然后,應用程序在繪圖環境對象上調用stroke()與fill()方法,對剛才那些路徑進行描邊或填充。
描邊與填充操作的效果取決於當前的繪圖屬性,這些屬性包括了lineWidth、strokeStyle、fillStyle以及陰影屬性等。比如,程序清單2-9中的這個應用程序,將lineWidth屬性值設置為2,然后對文本進行描邊,其后又將其重置為5,再對路徑進行描邊。
由rect()方法所創建的路徑是封閉的,然而,arc()方法創建的圓弧路徑則不封閉,除非你用它創建的是個圓形路徑。要封閉某段路徑,必須像程序清單2-9中那樣,調用closePath()方法才行。
表2-5總結了本應用程序中與路徑相關的方法。
提示:路徑與隱形墨水
有一個很恰當的比喻,可以用來說明“創建路徑隨后對其進行描邊或填充”這個操作。我們可以將該操作比作“使用隱形墨水來繪圖”。
你用隱形墨水所繪制的內容並不會立刻顯示出來,必須進行一些后續操作,像是加熱、塗抹化學葯品、照射紅外線等,才可以將你所畫的內容顯示出來。如果讀者關注這個話題,可以在http://en.wikipedia.org/wiki/Invisible_ink讀到所有關於隱形墨水的知識。
使用rect()與arc()這樣的方法來創建路徑,就好比使用隱形墨水來進行繪制一樣。這些方法會創建一條不可見的路徑,稍后可以調用stroke()或fill()令其可見。
2. 路徑與子路徑
在某一時刻,canvas之中只能有一條路徑存在,Canvas規范將其稱為“當前路徑”(current path)。然而,這條路徑卻可以包含許多子路徑(subpath)。而子路徑,又是由兩個或更多的點組成的。比方說,可以像這樣繪制出兩個矩形來:
context.beginPath(); //Clear all subpaths from //the current path context.rect(10,10,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four points context.beginPath(); //Clear all subpaths from the //current path context.rect(50,50,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four point
以上這段代碼通過調用beginPath()來開始一段新的路徑,該方法會將當前路徑中的所有子路徑都清除掉。然后,這段代碼調用了rect()方法,此方法向當前路徑中增加了一個含有4個點的子路徑。最后,調用stroke()方法,將當前路徑的輪廓線描繪出來,使得這個矩形出現在canvas之中。
接下來,這段代碼又一次調用了beginPath()方法,該方法清除了上一次調用rect()方法時所創建的子路徑。然后,再一次調用rect()方法,這次還是會向當前路徑中增加一段含有4個點的子路徑。最后,對該路徑進行描邊,使得第二個矩形也出現在了canvas之中。
現在考慮一下,如果將第二個beginPath()調用去掉,會怎么樣呢?像是這樣:
context.beginPath(); //Clear all subpaths from the //current path context.rect(10,10,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four points context.rect(50,50,100,100);//Add a second subpath with //four points context.stroke(); //Stroke both subpaths
上面這段代碼在一開始與剛才那段是一樣的:先調用beginPath()來清除當前路徑中的所有子路徑,然后調用rect()來創建一條包含矩形4個點的子路徑,再調用stroke()方法使得這個矩形出現在canvas之上。
接下來,這段代碼再次調用了rect()方法,不過這一次,由於沒有調用beginPath()方法來清除原有的子路徑,所以第二次對rect()方法的調用,會向當前路徑中增加一條子路徑。最后,該段代碼再一次調用stroke()方法,這次對stroke()方法的調用,將會使得當前路徑中的兩條子路徑都被描邊,這意味着它會重繪第一個矩形。
填充路徑時所使用的“非零環繞規則”
如果當前路徑是循環的,或是包含多個相交的子路徑,那么Canvas的繪圖環境變量就必須要判斷,當fill()方法被調用時,應該如何對當前路徑進行填充。Canvas在填充那種互相有交叉的路徑時,使用“非零環繞規則”(nonzero winding rule)來進行判斷。圖2-14演示了該規則的運用。
“非零環繞規則”是這么來判斷有自我交叉情況的路徑的:對於路徑中的任意給定區域,從該區域內部畫一條足夠長的線段,使此線段的終點完全落在路徑范圍之外。圖2-14中的那三個箭頭所描述的就是上面這個步驟。

接下來,將計數器初始化為0,然后,每當這條線段與路徑上的直線或曲線相交時,就改變計數器的值。如果是與路徑的順時針部分相交,則加1,如果是與路徑的逆時針部分相交,則減1。若計數器的最終值不是0,那么此區域就在路徑里面,在調用fill()方法時,瀏覽器就會對其進行填充。如果最終值是0,那么此區域就不在路徑內部,瀏覽器也就不會對其進行填充了。
可以從圖2-14中看出“非零環繞規則”是如何運用的。左邊的那個帶箭頭的線段,先穿過了路徑的逆時針部分,然后又穿過了路徑的順時針部分。這意味着其計數值是0,所以該線段起點所在的那個區域就不在范圍內,在調用fill()方法時,瀏覽器也就不會對其進行填充。然而,其余兩條帶箭頭的線段,其計數值都不是0,所以它們的起點所在的區域就會被瀏覽器填充。
3. 剪紙效果
我們來運用一下所學到的路徑、陰影以及非零環繞原則等知識,實現如圖2-15所示的剪紙(cutout)效果。
圖2-15所示的應用程序,其JavaScript代碼列在了程序清單2-10之中。
這段JavaScript代碼創建了一條路徑,它由兩個圓形所組成,其中一個圓形在另一個的內部。通過設定arc()方法的最后一個參數值,該應用程序以順時針方向繪制了內部的圓形,並且以逆時針方向繪制了外圍的圓形。繪制效果如圖2-16上方所示。
在創建好路徑之后,圖2-15中的那個應用程序對該路徑進行了填充。瀏覽器運用“非零環繞規則”,對外圍圓形的內部進行了填充,不過填充的范圍並不包括里面的圓,這就產生了一種剪紙圖案的效果。你也可以利用此技術來剪出任意想要的形狀來。
程序清單2-10 圖2-15所示應用程序的JavaScript代碼
var context=document.getElementById('canvas').getContext('2d'); //Functions... function drawGrid(color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. function drawTwoArcs(){ context.beginPath(); context.arc(300,190,150,0,Math.PI*2,false);//Outer:CCW context.arc(300,190,100,0,Math.PI*2,true);//Inner:CW context.fill(); context.shadowColor=undefined; context.shadowOffsetX=0; context.shadowOffsetY=0; context.stroke(); function draw(){ context.clearRect(0,0,context.canvas.width, context.canvas.height); } } drawGrid('lightgray',10,10); context.save(); context.shadowColor='rgba(0,0,0,0.8)'; context.shadowOffsetX=12; context.shadowOffsetY=12; context.shadowBlur=15; drawTwoArcs(); context.restore(); } //Initialization... context.fillStyle='rgba(100,140,230,0.5)'; context.strokeStyle=context.fillStyle; draw();
圖2-16之中的例子是對圖2-15所示應用程序的一種擴展,它會告訴你如果兩個圓形子路徑都在同一個方向上,繪制效果會如何,同時它也增加了一些注釋信息,用以顯示圓形子路徑的繪制方向以及“非零環繞規則”的計算過程。而且,這個程序還顯示了創建圓形子路徑所調用的arc()方法。
提示:圖2-16之中的那條橫線是怎么回事
請注意圖2-16中兩個圓之間的那條橫線。在圖2-15之中也有這樣一條線,不過圖2-16使用了更深一些的描邊顏色,把它畫得更加明顯了。
根據Canvas規范,當使用arc()方法向當前路徑中增加子路徑時,該方法必須將上一條子路徑的終點與所畫圓弧的起點相連。
制作剪紙圖形
圖2-17之中的應用程序在矩形內剪出了三個圖形。與上一小節中所討論的那個程序不同,圖2-17所示的應用程序采用了完全不透明的顏色來填充這個包含剪紙圖形的矩形。
該應用程序有兩個值得注意的地方。首先,包圍剪紙圖形的是一個矩形而不是圓形。這個矩形的使用向你表明,可以用任意形狀的路徑來包圍剪紙圖形,並不一定非要用圓形。該程序建立剪紙圖形所用的代碼如下:
function drawCutouts(){ context.beginPath(); addOuterRectanglePath();//Clockwise(CW) addCirclePath(); //Counter-clockwise(CCW) addRectanglePath(); //CCW addTrianglePath(); //CCW context.fill();//Cut out shapes }
addOuterRectanglePath()、addCirclePath()、addRectanglePath()及addTrianglePath()方法分別向當前路徑中添加了表示剪紙圖形的子路徑。
圖2-17中的應用程序還有一個有意思的地方,就是其中的矩形剪紙圖案。arc()方法可以讓調用者控制圓弧的繪制方向,然而rect()方法則沒有那么方便,它總是按照順時針方向來創建路徑。可是,在本例這種情況下,需要的是一條逆時針的矩形路徑,所以我們自己創建了一個rect()方法,此方法像arc()一樣,可以讓調用者控制矩形路徑的方向:
function rect(x,y,w,h,direction){ if(direction){//CCW context.moveTo(x,y); context.lineTo(x,y+h); context.lineTo(x+w,y+h); context.lineTo(x+w,y); } else{ context.moveTo(x,y); context.lineTo(x+w,y); context.lineTo(x+w,y+h); context.lineTo(x,y+h); } context.closePath(); }
上述代碼使用moveTo()與lineTo()方法來創建順時針或者逆時針的矩形路徑。在2.8節中我們將詳細講述這些方法。
該應用程序在建立路徑時,分別使用了兩種不同的方式來創建外圍矩形及內部的矩形剪紙圖形:
function addOuterRectanglePath(){ context.rect(110,25,370,335); } function addRectanglePath(){ rect(310,55,70,35,true); }
addOuterRectanglePath()方法使用了繪圖環境對象的rect()方法,此方法總是按照順時針方向來繪制矩形的,並沒有提供逆時針繪制的選項。addRectanglePath()方法創建了矩形剪紙圖形的路徑,它使用上面列出的那個rect()方法來繪制逆時針的矩形路徑。
圖2-17所示應用程序的JavaScript代碼列在了程序清單2-11之中。
小技巧:路徑方向真的很重要
arc()方法的最后一個boolean參數用於控制所繪圓弧路徑的方向。如果該參數是默認值true,那么瀏覽器就會以順時針方向來繪制路徑,否則,瀏覽器就按照逆時針(counterclockwise)方向來繪制(或者按照Canvas規范中的說法,“反時針方向”(anti-clockwise))。
提示:arc()方法可以控制路徑方向,而rect()方法則不行
arc()方法與rect()方法都可以向當前路徑中添加子路徑,然而arc()方法可以讓調用者來控制路徑的繪制方向。幸好可以非常容易地實現一個函數,用它來創建具有特定方向的矩形路徑。程序清單2-11中的rect()方法就演示了這種做法。
小技巧:去掉由arc()方法所產生的那條不太美觀的連接線
如果在當前路徑中存在子路徑的情況下調用arc()方法,那么此方法就會從子路徑的終點向圓弧的起點畫一條線。通常情況下,你並不想看到這條線段。
如果不想讓這條連線出現,可以在調用arc()方法來繪制圓弧之前,先調用beginPath()方法。調用此方法會將當前路徑下的所有子路徑都清除掉,這樣一來,arc()方法也就不會再繪制那條連接線了。
程序清單2-11 繪制剪紙圖形的代碼
var context=document.getElementById('canvas').getContext('2d'); //Functions... function drawGrid(color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. } function draw(){ context.clearRect(0,0,context.canvas.width, context.canvas.height); drawGrid('lightgray',10,10); context.save(); context.shadowColor='rgba(200,200,0,0.5)'; context.shadowOffsetX=12; context.shadowOffsetY=12; context.shadowBlur=15; drawCutouts(); strokeCutoutShapes(); context.restore(); } function drawCutouts(){ context.beginPath(); addOuterRectanglePath();//CW addCirclePath(); //CCW addRectanglePath();//CCW addTrianglePath();//CCW context.fill();//Cut out shapes } function strokeCutoutShapes(){ context.save(); context.strokeStyle='rgba(0,0,0,0.7)'; context.beginPath(); addOuterRectanglePath();//CW context.stroke(); context.beginPath(); addCirclePath(); addRectanglePath(); addTrianglePath(); context.stroke(); context.restore(); function rect(x,y,w,h,direction){ if(direction){//CCW context.moveTo(x,y); context.lineTo(x,y+h); context.lineTo(x+w,y+h); context.lineTo(x+w,y); context.closePath(); } else{ context.moveTo(x,y); context.lineTo(x+w,y); context.lineTo(x+w,y+h); context.lineTo(x,y+h); context.closePath(); } function addOuterRectanglePath(){ context.rect(110,25,370,335); } function addCirclePath(){ context.arc(300,300,40,0,Math.PI*2,true); } function addRectanglePath(){ rect(310,55,70,35,true); } function addTrianglePath(){ context.moveTo(400,200); context.lineTo(250,115); context.lineTo(200,200); context.closePath(); //Initialization... context.fillStyle='goldenrod'; draw(); } } }