??HTML5 添加的最受歡迎的功能就是<canvas>元素。這個(gè)元素負(fù)責(zé)在頁面中設(shè)定一個(gè)區(qū)域,然后就可以通過 JavaScript 動(dòng)態(tài)地在這個(gè)區(qū)域中繪制圖形。
??<canvas>元素最早是由蘋果公司推出的,當(dāng)時(shí)主要用在其 Dashboard 微件中。
??很快,HTML5 加入了這個(gè)元素,主流瀏覽器也迅速開始支持它。IE9+、Firefox 1.5+、Safari 2+、Opera 9+、Chrome、iOS 版 Safari 以及 Android 版 WebKit 都在某種程度上支持<canvas>。
??與瀏覽器環(huán)境中的其他組件類似,<canvas>由幾組 API 構(gòu)成,但并非所有瀏覽器都支持所有這些 API。除了具備基本繪圖能力的 2D 上下文,<canvas>還建議了一個(gè)名為 WebGL 的 3D 上下文。
??目前,支持該元素的瀏覽器都支持 2D 上下文及文本 API,但對 WebGL 的支持還不夠好。由于 WebGL 還是實(shí)驗(yàn)性的,因此要得到所有瀏覽器支持還需要很長一段時(shí)間。Firefox 4+和 Chrome 支持 WebGL 規(guī)范的早期版本,但一些老版本的操作系統(tǒng),比如 Windows XP,由于缺少必要的繪圖驅(qū)動(dòng)程序,即便安裝了這兩款瀏覽器也無濟(jì)于事。
1、基本用法
??要使用 <canvas> 元素,必須先設(shè)置 width 和 height 屬性,指定可以繪圖的區(qū)域大小。出現(xiàn)在開始和結(jié)束標(biāo)簽中的內(nèi)容是后備信息,如果瀏覽器不支持 <canvas> 元素,就會(huì)顯示這些信息。下面就是 <canvas> 元素的例子。
<canvas id="drawing" width="200" height="200">A drawing of something.</canvas>
??與其它元素一樣,<canvas> 元素對應(yīng)的 DOM 元素對象也有 width 和 height 屬性,可以隨意修改。而且,也能通過 CSS 為該元素添加樣式,如果不添加任何樣式或者不繪制任何圖形,在頁面中是看不到該元素的。
??要在這塊畫布(canvas)上繪圖,需要取得繪圖上下文。而取得繪圖上下文對象的引用,需要調(diào)用 getContext() 方法并傳入上下文的名字。傳入"2d",就可以取得 2D 上下文對象。
var drawing = document.getElementById('drawing');
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext) {
var context = drawing.getContext("2d");
// 更多代碼
}
??在使用 <canvas> 元素之前,首先要檢測 getContext() 方法是否存在,這一步非常重要。有些瀏覽器會(huì)為 HTML 規(guī)范之外的元素創(chuàng)建默認(rèn)的 HTML 元素對象(假設(shè)你想在 Firefox3 中使用 <canvas> 元素,雖然瀏覽器會(huì)為該標(biāo)簽創(chuàng)建一個(gè) DOM 對象,但這個(gè)對象中并沒有 getContext() 方法。)。在這種情況下,即使 drawing 變量中保存著一個(gè)有效的元素引用,也檢測不到 getContext() 方法。
??使用 toDataURL() 方法,可以導(dǎo)出在<canvas>元素上繪制的圖像。這個(gè)方法接受一個(gè)參數(shù),即圖像的 MIME 類型格式,而且適合用于創(chuàng)建圖像的任何上下文。比如,要取得畫布中的一幅 PNG 格式的圖像,可以使用以下代碼。
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext()) {
// 取得圖像的數(shù)據(jù) URI
var imgURI = drawing.toDataURL("image/png");
// 顯示圖像
var image = document.creatElement("img");
image.src = imgURI;
document.body.appendChild(image);
}
??默認(rèn)情況下,瀏覽器會(huì)將圖像編碼為 PNG 格式(除非另行指定)。Firefox 和 Opera 也支持基于 "image/jpeg" 參數(shù)的 JPEG 編碼格式。由于這個(gè)方法是后來才追加的,所以支持<canvas>的瀏覽器也是在較新的版本中才加入了對它的支持,比如 IE9、Firefox3.5 和 Opera10。
如果繪制到畫布上的圖像源自不同的域,toDataURL() 方法會(huì)拋出錯(cuò)誤。
2、2D 上下文
??使用 2D 繪圖上下文提供的方法,可以繪制簡單的 2D 圖形,比如矩形、弧線和路徑。
??2D 上下文的坐標(biāo)開始于<canvas>元素的左上角,原點(diǎn)坐標(biāo)是(0,0)。所有坐標(biāo)值都基于這個(gè)原點(diǎn)計(jì)算,x 值越大表示越靠右,y 值越大表示越靠下。默認(rèn)情況下,width 和 height 表示水平和垂直兩個(gè)方向上可用的像素?cái)?shù)目。
2.1、 填充和描邊
??2D 上下文的兩種基本繪圖操作是填充和描邊。
??填充,就是用指定的樣式(顏色、漸變或圖像)填充圖形;
??描邊,就是只在圖形的邊緣畫線。
??大多數(shù) 2D 上下文操作都會(huì)細(xì)分為填充和描邊兩個(gè)操作,而操作的結(jié)果取決于兩個(gè)屬性:fillStyle 和 strokeStyle。這兩個(gè)屬性的值可以是字符串、漸變對象或模式對象,而且它們的默認(rèn)值是"#000000"。
??如果為它們指定表示顏色的字符串值,可以使用 CSS 中指定顏色值的任何格式,包括顏色名、十六進(jìn)制碼、rgb、rgba、hsl 或 hsla。示例:
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
context.strokeStyle = "red";
context.fillStyle = "#0000ff";
}
??以上代碼將 strokeStyle 設(shè)置為 red(CSS 中的顏色名),將 fillStyle 設(shè)置為#0000ff(藍(lán)色)。
??然后,所有涉及描邊和填充的操作都將使用這兩個(gè)樣式,直至重新設(shè)置這兩個(gè)值。如前所述,這兩個(gè)屬性的值也可以是漸變對象或模式對象。本章后面會(huì)討論這兩種對象。
2.2、 繪制矩形
??矩形是唯一一種可以直接在 2D 上下文中繪制的形狀。與矩形有關(guān)的方法包括 fillRect()、strokeRect() 和 clearRect()。
??這三個(gè)方法都能接收 4 個(gè)參數(shù):矩形的 x 坐標(biāo)、矩形的 y 坐標(biāo)、矩形寬度和矩形高度。這些參數(shù)的單位都是像素。
??首先,fillRect() 方法在畫布上繪制的矩形會(huì)填充指定的顏色。填充的顏色通過 fillStyle 屬性指定,比如:
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 繪制半透明的藍(lán)色矩形
context.fillStyle = "rgba(0, 0, 255, .5)";
context.fillRect(30, 30, 50, 50);
}
??以上代碼首先將 fillStyle 設(shè)置為紅色,然后從(10, 10)處開始繪制矩形,矩形的寬和高均為 50 像素。然后,通過 rgba() 格式再將 fillStyle 設(shè)置為半透明的藍(lán)色,在第一個(gè)矩形上面繪制第二個(gè)矩形。結(jié)果就是可以透過藍(lán)色的矩形看到紅色的矩形。
??strokeRect() 方法在畫布上繪制的矩形會(huì)使用指定的顏色描邊。描邊顏色通過 strokeStyle 屬性指定。示例:
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 繪制紅色描邊矩形
context.strokeStyle= "#ff0000";
context.strokeRect(10, 10, 50, 50);
// 繪制半透明的藍(lán)色描邊矩形
context.strokeStyle= "rgba(0, 0, 255, .5)";
context.strokeRect(30, 30, 50, 50);
}
??以上代碼繪制了兩個(gè)重疊的矩形。不過,這兩個(gè)矩形都只有框線,內(nèi)部并沒有填充顏色。
??描邊線條的寬度由 lineWidth 屬性控制,該屬性的值可以是任意整數(shù)。
??另外,通過 lineCap 屬性可以控制線條末端的形狀是平頭、圓頭還是方頭("butt"、"round" 或 "square")。
??通過 lineJoin 屬性可以控制線條相交的方式是圓交、斜
交還是斜接("round"、"bevel" 或 "miter")。
??最后,clearRect() 方法用于清除畫布上的矩形區(qū)域。本質(zhì)上,這個(gè)方法可以把繪制上下文中的某一矩形區(qū)域變透明。通過繪制形狀然后再清除指定區(qū)域,就可以生成有意思的效果,例如把某個(gè)形狀切掉一塊。示例:
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 繪制半透明的藍(lán)色矩形
context.fillStyle = "rgba(0, 0, 255, .5)";
context.fillRect(30, 30, 50, 50);
// 在兩個(gè)矩形重疊的地方清除一個(gè)小矩形
context.clearRect(40, 40, 10, 10);
}
2.3、繪制路徑
??2D 繪制上下文支持很多在畫布上繪制路徑的方法。通過路徑可以創(chuàng)造出復(fù)雜的形狀和線條。
??要繪制路徑,首先必須調(diào)用 beginPath() 方法,表示要開始繪制新路徑。然后,再通過調(diào)用下列方法來實(shí)際地繪制路徑。
- arc(x, y, radius, startAngle, endAngle, counterclockwise):以(x, y)為圓心繪制一條弧線,弧線半徑為 radius,起始和結(jié)束角度(用弧度表示)分別為 startAngle 和 endAngle。最后一個(gè)參數(shù)表示 startAngle 和 endAngle 是否按逆時(shí)針方向計(jì)算,值為 false 表示按順時(shí)針方向計(jì)算。
- arcTo(x1, y1, x2, y2, radius):從上一點(diǎn)開始繪制一條弧線,到(x2, y2)為止,并且以給定的半徑 radius 穿過(x1, y1)。
- bezierCurveTo(c1x, c1y, c2x, c2y, x, y):從上一點(diǎn)開始繪制一條曲線,到(x, y) 為止,并且以 (c1x, c1y) 和 (c2x, c2y) 為控制點(diǎn)。
- lineTo(x, y):從上一點(diǎn)開始繪制一條直線,到(x, y)為止。
- moveTo(x, y):將繪圖游標(biāo)移動(dòng)到(x, y),不畫線。
- quadraticCurveTo(cx, cy, x, y):從上一點(diǎn)開始繪制一條二次曲線,到(x, y)為止,并且以(cx, cy)作為控制點(diǎn)。
- rect(x, y, width, height):從點(diǎn)(x, y)開始繪制一個(gè)矩形,寬度和高度分別由 width 和 height 指定。這個(gè)方法繪制的是矩形路徑,而不是 strokeRect() 和 fillRect() 所繪制的獨(dú)立的形狀。
??創(chuàng)建了路徑后,接下來有幾種可能的選擇。如果想繪制一條連接到路徑起點(diǎn)的線條,可以調(diào)用 closePath()。
??如果路徑已經(jīng)完成,你想用 fillStyle 填充它,可以調(diào)用 fill() 方法。
??另外,還可以調(diào)用 stroke() 方法對路徑描邊,描邊使用的是 strokeStyle。
??最后還可以調(diào)用 clip(),這個(gè)方法可以在路徑上創(chuàng)建一個(gè)剪切區(qū)域。
??下面看一個(gè)例子,即繪制一個(gè)不帶數(shù)字的時(shí)鐘表盤。
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 開始路徑
context.beginPath();
// 繪制外圓
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 繪制內(nèi)圓
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 繪制分針
context.moveTo(100, 100);
context.lineTo(100, 15);
// 繪制時(shí)針
context.moveTo(100, 100);
context.lineTo(35, 100);
// 描邊路徑
context.stroke();
}
??上述例子使用 arc() 方法繪制了兩個(gè)圓形:一個(gè)外圓和一個(gè)內(nèi)圓,構(gòu)成了表盤的邊框。外圓的半徑是 99 像素,圓心位于點(diǎn)(100,100),也是畫布的中心點(diǎn)。為了繪制一個(gè)完整的圓形,我們從 0 弧度開始,繪制 2π 弧度(通過 Math.PI 來計(jì)算)。
??在繪制內(nèi)圓之前,必須把路徑移動(dòng)到內(nèi)圓上的某一點(diǎn),以避免繪制出多余的線條。
??第二次調(diào)用 arc() 使用了小一點(diǎn)的半徑,以便創(chuàng)造邊框的效果。
??然后,組合使用 moveTo() 和 lineTo() 方法來繪制時(shí)針和分針。
??最后一步是調(diào)用 stroke() 方法,這樣才能把圖形繪制到畫布上,如下圖所示:
??在 2D 繪圖上下文中,路徑是一種主要的繪圖方式,因?yàn)槁窂侥転橐L制的圖形提供更多控制。
??由于路徑的使用很頻繁,所以就有了一個(gè)名為 isPointInPath() 的方法。這個(gè)方法接收 x 和 y 坐標(biāo)作為參數(shù),用于在路徑被關(guān)閉之前確定畫布上的某一點(diǎn)是否位于路徑上,例如:
if (context.isPointInPath(100, 100)){
alert("Point (100, 100) is in the path.");
}
??2D 上下文中的路徑 API 已經(jīng)非常穩(wěn)定,可以利用它們結(jié)合不同的填充和描邊樣式,繪制出非常復(fù)雜的圖形來。
2.4、 繪制文本
??文本與圖形總是如影隨形。為此,2D 繪圖上下文也提供了繪制文本的方法。
??繪制文本主要有兩個(gè)方法:fillText() 和 strokeText()。這兩個(gè)方法都可以接收 4 個(gè)參數(shù):要繪制的文本字符串、x 坐標(biāo)、y 坐標(biāo)和可選的最大像素寬度。而且,這兩個(gè)方法都以下列 3 個(gè)屬性為基礎(chǔ)。
- font:表示文本樣式、大小及字體,用 CSS 中指定字體的格式來指定,例如"10px Arial"。
- textAlign:表示文本對齊方式。可能的值有 "start"、"end"、"left"、"right" 和 "center"。建議使用"start"和"end",不要使用"left"和"right",因?yàn)榍皟烧叩囊馑几€(wěn)妥,能同時(shí)適合從左到右和從右到左顯示(閱讀)的語言。
- textBaseline:表示文本的基線。可能的值有 "top"、"hanging"、"middle"、 "alphabetic"、"ideographic" 和 "bottom"。
??這幾個(gè)屬性都有默認(rèn)值,因此沒有必要每次使用它們都重新設(shè)置一遍值。
??fillText() 方法使用 fillStyle 屬性繪制文本,而 strokeText() 方法使用 strokeStyle 屬性為文本描邊。
??相對來說,還是使用 fillText() 的時(shí)候更多,因?yàn)樵摲椒7铝嗽诰W(wǎng)頁中正常顯示文本。例如,下面的代碼在前一節(jié)創(chuàng)建的表盤上方繪制了數(shù)字 12:
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
??因?yàn)檫@里把 textAlign 設(shè)置為"center",把 textBaseline 設(shè)置為"middle",所以坐標(biāo)(100, 20) 表示的是文本水平和垂直中點(diǎn)的坐標(biāo)。
??如果將 textAlign 設(shè)置為"start",則 x 坐標(biāo)表示的是文本左端的位置(從左到右閱讀的語言);設(shè)置為"end",則 x 坐標(biāo)表示的是文本右端的位置(從左到右閱讀的
語言)。例如:
// 正常
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
// 起點(diǎn)對齊
context.textAlign = "start";
context.fillText("12", 100, 40);
// 終點(diǎn)對齊
context.textAlign = "end";
context.fillText("12", 100, 60);
??這一回繪制了三個(gè)字符串"12",每個(gè)字符串的 x 坐標(biāo)值相同,但 textAlign 值不同。另外,后兩個(gè)字符串的 y 坐標(biāo)依次增大,以避免相互重疊。結(jié)果如下圖所示:
??表盤中的分針恰好位于正中間,因此文本的水平對齊方式如何變化也能夠一目了然。
??類似地,修改 textBaseline 屬性的值可以調(diào)整文本的垂直對齊方式:值為"top",y 坐標(biāo)表示文本頂端;值為 "bottom",y 坐標(biāo)表示文本底端;值為 "hanging"、"alphabetic" 和 "ideographic",則 y 坐標(biāo)分別指向字體的特定基線坐標(biāo)。
??由于繪制文本比較復(fù)雜,特別是需要把文本控制在某一區(qū)域中的時(shí)候,2D 上下文提供了輔助確定文本大小的方法 measureText()。
??這個(gè)方法接收一個(gè)參數(shù),即要繪制的文本;返回一個(gè) TextMetrics 對象。返回的對象目前只有一個(gè) width 屬性,但將來還會(huì)增加更多度量屬性。
??measureText() 方法利用 font、textAlign 和 textBaseline 的當(dāng)前值計(jì)算指定文本的大小。
??比如,假設(shè)你想在一個(gè) 140 像素寬的矩形區(qū)域中繪制文本 Hello world!,下面的代碼從 100 像素的字體大小開始遞減,最終會(huì)找到合適的字體大小。
var fontSize = 100;
context.font = fontSize + "px Arial";
while(context.measureText("Hello world!").width > 140){
fontSize--;
context.font = fontSize + "px Arial";
}
context.fillText("Hello world!", 10, 10);
context.fillText("Font size is " + fontSize + "px", 10, 50);
??前面提到過,fillText 和 strokeText() 方法都可以接收第四個(gè)參數(shù),也就是文本的最大像素寬度。不過,這個(gè)可選的參數(shù)尚未得到所有瀏覽器支持(最早支持它的是 Firefox 4)。
??提供這個(gè)參數(shù)后,調(diào)用 fillText() 或 strokeText() 時(shí)如果傳入的字符串大于最大寬度,則繪制的文本字符的高度正確,但寬度會(huì)收縮以適應(yīng)最大寬度。
??繪制文本還是相對比較復(fù)雜的操作,因此支持<canvas>元素的瀏覽器也并未完全實(shí)現(xiàn)所有與繪制文本相關(guān)的 API。
2.5、變換
??通過上下文的變換,可以把處理后的圖像繪制到畫布上。2D 繪制上下文支持各種基本的繪制變換。
??創(chuàng)建繪制上下文時(shí),會(huì)以默認(rèn)值初始化變換矩陣,在默認(rèn)的變換矩陣下,所有處理都按描述直接繪制。
??為繪制上下文應(yīng)用變換,會(huì)導(dǎo)致使用不同的變換矩陣應(yīng)用處理,從而產(chǎn)生不同的結(jié)果。
??可以通過如下方法來修改變換矩陣。
- rotate(angle):圍繞原點(diǎn)旋轉(zhuǎn)圖像 angle 弧度。
- scale(scaleX, scaleY):縮放圖像,在 x 方向乘以 scaleX,在 y 方向乘以 scaleY。scaleX 和 scaleY 的默認(rèn)值都是 1.0。
- translate(x, y):將坐標(biāo)原點(diǎn)移動(dòng)到(x,y)。執(zhí)行這個(gè)變換之后,坐標(biāo)(0,0)會(huì)變成之前由(x, y)表示的點(diǎn)。
- transform(m1_1, m1_2, m2_1, m2_2, dx, dy):直接修改變換矩陣,方式是乘以如下矩陣。
m1_1??m1_2??dx
m2_1??m2_2??dy
0????0????1 - setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):將變換矩陣重置為默認(rèn)狀態(tài),然后再調(diào)用 transform()。
??變換有可能很簡單,但也可能很復(fù)雜,這都要視情況而定。比如,就拿前面例子中繪制表針來說,如果把原點(diǎn)變換到表盤的中心,然后再繪制表針就容易多了。請看下面的例子。
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 開始路徑
context.beginPath();
// 繪制外圓
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 繪制內(nèi)圓
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 變換原點(diǎn)
context.translate(100, 100);
// 繪制分針
context.moveTo(0,0);
context.lineTo(0, -85);
// 繪制時(shí)針
context.moveTo(0, 0);
context.lineTo(-65, 0);
// 描邊路徑
context.stroke();
}
??把原點(diǎn)變換到時(shí)鐘表盤的中心點(diǎn)(100, 100)后,在同一方向上繪制線條就變成了簡單的數(shù)學(xué)問題了。所有數(shù)學(xué)計(jì)算都基于(0, 0),而不是(100, 100)。
??還可以更進(jìn)一步,像下面這樣使用 rotate() 方法旋轉(zhuǎn)時(shí)鐘的表針。
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 開始路徑
context.beginPath();
// 繪制外圓
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 繪制內(nèi)圓
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 變換原點(diǎn)
context.translate(100, 100);
// 旋轉(zhuǎn)表針
context.rotate(1);
// 繪制分針
context.moveTo(0,0);
context.lineTo(0, -85);
// 繪制時(shí)針
context.moveTo(0, 0);
context.lineTo(-65, 0);
// 描邊路徑
context.stroke();
}
??因?yàn)樵c(diǎn)已經(jīng)變換到了時(shí)鐘表盤的中心點(diǎn),所以旋轉(zhuǎn)也是以該點(diǎn)為圓心的。結(jié)果就像是表針真地被固定在表盤中心一樣,然后向右旋轉(zhuǎn)了一定角度。
??無論是剛才執(zhí)行的變換,還是 fillStyle、strokeStyle 等屬性,都會(huì)在當(dāng)前上下文中一直有效,除非再對上下文進(jìn)行什么修改。
??雖然沒有什么辦法把上下文中的一切都重置回默認(rèn)值,但有兩個(gè)方法可以跟蹤上下文的狀態(tài)變化。如果你知道將來還要返回某組屬性與變換的組合,可以調(diào)用 save() 方法。
??調(diào)用這個(gè)方法后,當(dāng)時(shí)的所有設(shè)置都會(huì)進(jìn)入一個(gè)棧結(jié)構(gòu),得以妥善保管。然后可以對上下文進(jìn)行其他修改。等想要回到之前保存的設(shè)置時(shí),可以調(diào)用 restore() 方法,在保存設(shè)置的棧結(jié)構(gòu)中向前返回一級,
恢復(fù)之前的狀態(tài)。
??連續(xù)調(diào)用 save() 可以把更多設(shè)置保存到棧結(jié)構(gòu)中,之后再連續(xù)調(diào)用 restore() 則可以一級一級返回。下面來看一個(gè)例子。
context.fillStyle = "#ff0000";
context.save();
context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); // 從點(diǎn)(100,100)開始繪制藍(lán)色矩形
context.restore();
context.fillRect(10, 10, 100, 200); // 從點(diǎn)(110,110)開始繪制綠色矩形
context.restore();
context.fillRect(0, 0, 100, 200); // 從點(diǎn)(0,0)開始繪制紅色矩形
??首先,將 fillStyle 設(shè)置為紅色,并調(diào)用 save() 保存上下文狀態(tài)。
??接下來,把 fillStyle 修改為綠色,把坐標(biāo)原點(diǎn)變換到(100, 100),再調(diào)用 save() 保存上下文狀態(tài)。
??然后,把 fillStyle 修改為藍(lán)色并繪制藍(lán)色的矩形。因?yàn)榇藭r(shí)的坐標(biāo)原點(diǎn)已經(jīng)變了,所以矩形的左上角坐標(biāo)實(shí)際上是(100, 100)。
??然后調(diào)用 restore(),之后 fillStyle 變回了綠色,因而第二個(gè)矩形就是綠色。之所以第二個(gè)矩形的起點(diǎn)坐標(biāo)是(110, 110),是因?yàn)樽鴺?biāo)位置的變換仍然起作用。
??再調(diào)用一次 restore(),變換就被取消了,而 fillStyle 也返回了紅色。所以最后一個(gè)矩形是紅色的,而且繪制的起點(diǎn)是(0,0)。
??需要注意的是,save() 方法保存的只是對繪圖上下文的設(shè)置和變換,不會(huì)保存繪圖上下文的內(nèi)容。
2.6、 繪制圖像
?? 繪圖上下文內(nèi)置了對圖像的支持。如果你想把一幅圖像繪制到畫布上,可以使用 drawImage() 方法。根據(jù)期望的最終結(jié)果不同,調(diào)用這個(gè)方法時(shí),可以使用三種不同的參數(shù)組合。最簡單的調(diào)用方式是傳入一個(gè) HTML <img> 元素,以及繪制該圖像的起點(diǎn)的 x 和 y 坐標(biāo)。例如:
var image = document.images[0];
context.drawImage(image, 10, 10);
??這兩行代碼取得了文檔中的第一幅圖像,然后將它繪制到上下文中,起點(diǎn)為(10, 10)。繪制到畫布上的圖像大小與原始大小一樣。
??如果你想改變繪制后圖像的大小,可以再多傳入兩個(gè)參數(shù),分別表示目標(biāo)寬度和目標(biāo)高度。通過這種方式來縮放圖像并不影響上下文的變換矩陣。例如:
context.drawImage(image, 50, 10, 20, 30);
??執(zhí)行代碼后,繪制出來的圖像大小會(huì)變成 20×30 像素。
??除了上述兩種方式,還可以選擇把圖像中的某個(gè)區(qū)域繪制到上下文中。
??drawImage() 方法的這種調(diào)用方式總共需要傳入 9 個(gè)參數(shù):要繪制的圖像、源圖像的 x 坐標(biāo)、源圖像的 y 坐標(biāo)、源圖像的寬度、源圖像的高度、目標(biāo)圖像的 x 坐標(biāo)、目標(biāo)圖像的 y 坐標(biāo)、目標(biāo)圖像的寬度、目標(biāo)圖像的高度。這樣調(diào)用 drawImage() 方法可以獲得最多的控制。例如:
context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60);
??這行代碼只會(huì)把原始圖像的一部分繪制到畫布上。原始圖像的這一部分的起點(diǎn)為(0, 10),寬和高都是 50 像素。最終繪制到上下文中的圖像的起點(diǎn)是(0, 100),而大小變成了 40×60 像素。
??這種調(diào)用方式可以創(chuàng)造出很有意思的效果,如下圖所示:
??除了給 drawImage() 方法傳入 HTML <img> 元素外,還可以傳入另一個(gè)<canvas>元素作為其第一個(gè)參數(shù)。這樣,就可以把另一個(gè)畫布內(nèi)容繪制到當(dāng)前畫布上。
??結(jié)合使用 drawImage() 和其他方法,可以對圖像進(jìn)行各種基本操作。而操作的結(jié)果可以通過 toDataURL() 方法獲得。(請讀者注意,雖然本章至今一直在討論 2D 繪圖上下文,但 toDataURL()是 Canvas 對象的方法,不是上下文對象的方法。)
??不過,有一個(gè)例外,即圖像不能來自其他域。如果圖像來自其他域,調(diào)用 toDataURL() 會(huì)拋出一個(gè)錯(cuò)誤。打個(gè)比方,假如位于www.example.com 上的頁面繪制的圖像來自于 www.wrox.com,那當(dāng)前上下文就會(huì)被認(rèn)為“不干凈”,因而會(huì)拋出錯(cuò)誤。
2.7、陰影
??2D 上下文會(huì)根據(jù)以下幾個(gè)屬性的值,自動(dòng)為形狀或路徑繪制出陰影。
- shadowColor:用 CSS 顏色格式表示的陰影顏色,默認(rèn)為黑色。
- shadowOffsetX:形狀或路徑 x 軸方向的陰影偏移量,默認(rèn)為 0。
- shadowOffsetY:形狀或路徑 y 軸方向的陰影偏移量,默認(rèn)為 0。
- shadowBlur:模糊的像素?cái)?shù),默認(rèn) 0,即不模糊。
??這些屬性都可以通過 context 對象來修改。只要在繪制前為它們設(shè)置適當(dāng)?shù)闹担湍茏詣?dòng)產(chǎn)生陰影。例如:
var context = drawing.getContext("2d");
// 設(shè)置陰影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
??兩個(gè)矩形的陰影樣式相同,如下圖所示:
??不同瀏覽器對陰影的支持有一些差異。IE9、Firefox 4 和 Opera 11 的行為最為規(guī)范,其他瀏覽器多多少少會(huì)有一些奇怪的現(xiàn)象,甚至根本不支持陰影。
??Chrome(直至第 10 版)不能正確地為描邊的形狀應(yīng)用實(shí)心陰影。Chrome 和 Safari(直至第 5 版)在為帶透明像素的圖像應(yīng)用陰影時(shí)也會(huì)有問題:不透明部分的下方本來是該有陰影的,但此時(shí)則一概不見了。Safari 也不能給漸變圖形應(yīng)用陰影,其他瀏覽器都可以。
2.8、漸變
??漸變由 CanvasGradient 實(shí)例表示,很容易通過 2D 上下文來創(chuàng)建和修改。要?jiǎng)?chuàng)建一個(gè)新的線性漸變,可以調(diào)用 createLinearGradient() 方法。這個(gè)方法接收 4 個(gè)參數(shù):起點(diǎn)的 x 坐標(biāo)、起點(diǎn)的 y 坐標(biāo)、終點(diǎn)的 x 坐標(biāo)、終點(diǎn)的 y 坐標(biāo)。
??調(diào)用這個(gè)方法后,它就會(huì)創(chuàng)建一個(gè)指定大小的漸變,并返回 CanvasGradient 對象的實(shí)例。
??創(chuàng)建了漸變對象后,下一步就是使用 addColorStop() 方法來指定色標(biāo)。這個(gè)方法接收兩個(gè)參數(shù):色標(biāo)位置和 CSS 顏色值。色標(biāo)位置是一個(gè) 0(開始的顏色)到 1(結(jié)束的顏色)之間的數(shù)字。例如:
var gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
??此時(shí),gradient 對象表示的是一個(gè)從畫布上點(diǎn)(30, 30)到點(diǎn)(70, 70)的漸變。起點(diǎn)的色標(biāo)是白色,終點(diǎn)的色標(biāo)是黑色。然后就可以把 fillStyle 或 strokeStyle 設(shè)置為這個(gè)對象,從而使用漸變來繪制形狀或描邊:
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 繪制漸變矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
??為了讓漸變覆蓋整個(gè)矩形,而不是僅應(yīng)用到矩形的一部分,矩形和漸變對象的坐標(biāo)必須匹配才行。以上代碼會(huì)得到如下圖所示的結(jié)果:
??如果沒有把矩形繪制到恰當(dāng)?shù)奈恢茫强赡芫椭粫?huì)顯示部分漸變效果。例如:
context.fillStyle = gradient;
context.fillRect(50, 50, 50, 50);
??這兩行代碼執(zhí)行后得到的矩形只有左上角稍微有一點(diǎn)白色。這主要是因?yàn)榫匦蔚钠瘘c(diǎn)位于漸變的中間位置,而此時(shí)漸變差不多已經(jīng)結(jié)束了。由于漸變不重復(fù),所以矩形的大部分區(qū)域都是黑色。如下圖所示:
??確保漸變與形狀對齊非常重要,有時(shí)候可以考慮使用函數(shù)來確保坐標(biāo)合適。例如:
function createRectLinearGradient(context, x, y, width, height){
return context.createLinearGradient(x, y, x + width, y + height);
}
??這個(gè)函數(shù)基于起點(diǎn)的 x 和 y 坐標(biāo)以及寬度和高度值來創(chuàng)建漸變對象,從而讓我們可以在 fillRect() 中使用相同的值。
var gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
//繪制漸變矩形
context.fi llStyle = gradient;
context.fillRect(30, 30, 50, 50);
??使用畫布的時(shí)候,確保坐標(biāo)匹配很重要,也需要一些技巧。類似 createRectLinearGradient() 這樣的輔助方法可以讓控制坐標(biāo)更容易一些。
??要?jiǎng)?chuàng)建徑向漸變(或放射漸變),可以使用 createRadialGradient() 方法。這個(gè)方法接收 6 個(gè)參數(shù),對應(yīng)著兩個(gè)圓的圓心和半徑。前三個(gè)參數(shù)指定的是起點(diǎn)圓的原心(x 和 y)及半徑,后三個(gè)參數(shù)指定的是終點(diǎn)圓的原心(x 和 y)及半徑。
??可以把徑向漸變想象成一個(gè)長圓桶,而這 6 個(gè)參數(shù)定義的正是這個(gè)桶的兩個(gè)圓形開口的位置。如果把一個(gè)圓形開口定義得比另一個(gè)小一些,那這個(gè)圓桶就變成了圓錐體,而通過移動(dòng)每個(gè)圓形開口的位置,就可達(dá)到像旋轉(zhuǎn)這個(gè)圓錐體一樣的效果。
??如果想從某個(gè)形狀的中心點(diǎn)開始創(chuàng)建一個(gè)向外擴(kuò)散的徑向漸變效果,就要將兩個(gè)圓定義為同心圓。
??比如,就拿前面創(chuàng)建的矩形來說,徑向漸變的兩個(gè)圓的圓心都應(yīng)該在(55, 55),因?yàn)榫匦蔚膮^(qū)域是從(30, 30)到(80,80)。請看代碼:
var gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
//繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//繪制漸變矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
??運(yùn)行代碼,會(huì)得到如下圖所示的結(jié)果。
??因?yàn)閯?chuàng)建比較麻煩,所以徑向漸變并不那么容易控制。不過,一般來說,讓起點(diǎn)圓和終點(diǎn)圓保持為同心圓的情況比較多,這時(shí)候只要考慮給兩個(gè)圓設(shè)置不同的半徑就好了。
2.9、模式
??模式其實(shí)就是重復(fù)的圖像,可以用來填充或描邊圖形。要?jiǎng)?chuàng)建一個(gè)新模式,可以調(diào)用 createPattern() 方法并傳入兩個(gè)參數(shù):一個(gè) HTML <img> 元素和一個(gè)表示如何重復(fù)圖像的字符串。
??其中,第二個(gè)參數(shù)的值與 CSS 的background-repeat 屬性值相同,包括"repeat"、"repeat-x"、"repeat-y" 和 "no-repeat"。示例:
var image = document.images[0],
pattern = context.createPattern(image, "repeat");
// 繪制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);
??需要注意的是,模式與漸變一樣,都是從畫布的原點(diǎn)(0, 0)開始的。
??將填充樣式(fillStyle)設(shè)置為模式對象,只表示在某個(gè)特定的區(qū)域內(nèi)顯示重復(fù)的圖像,而不是要從某個(gè)位置開始繪制重復(fù)的圖像。
??createPattern() 方法的第一個(gè)參數(shù)也可以是一個(gè)<video>元素,或者另一個(gè)<canvas>元素。
2.10、使用圖像數(shù)據(jù)
??2D 上下文的一個(gè)明顯的長處就是,可以通過 getImageData() 取得原始圖像數(shù)據(jù)。
??這個(gè)方法接收 4 個(gè)參數(shù):要取得其數(shù)據(jù)的畫面區(qū)域的 x 和 y 坐標(biāo)以及該區(qū)域的像素寬度和高度。
??示例,要取得左上角坐標(biāo)為(10, 5)、大小為 50x50 像素的區(qū)域的圖像數(shù)據(jù),可以使用以下代碼:
var imageData = context.getImageData(10, 5, 50, 50);
??上述返回的對象是 ImageData 的實(shí)例。
??每個(gè) ImageData 對象都有三個(gè)屬性:width、height 和 data。
??其中 data 屬性是一個(gè)數(shù)組,保存著圖像中每一個(gè)像素的數(shù)據(jù)。在 data 數(shù)組中,每一個(gè)像素用 4 個(gè)元素來保存,分別表示紅、綠、藍(lán)和透明度。因此,第一個(gè)像素的數(shù)據(jù)就保存在數(shù)組的第 0 到第 3 個(gè)元素中,示例:
var data = imageData,
red = data[0],
green = data[1],
blue = data[2],
alpha = data[3];
??數(shù)組中的每個(gè)元素的值都介于 0 到 255 之間(包括 0 和 255)。能夠直接訪問到原始圖像數(shù)據(jù),就能夠以各種方式來操作這些數(shù)據(jù)。
??例如,通過修改圖像數(shù)據(jù),可以像下面這樣創(chuàng)建一個(gè)簡單的灰階過濾器。
var drawing = document.getElementById("drawing");
// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d"),
image = document.images[0],
imageData, data,
i, len, average,
red, green, blue, alpha;
// 繪制原始圖像
context.drawImage(image, 0, 0);
// 取得圖像數(shù)據(jù)
imageData = context.getImageData(0, 0, image.width, image.height);
data = imageData.data;
for (i=0, len=data.length; i < len; i+=4){
red = data[i];
green = data[i+1];
blue = data[i+2];
alpha = data[i+3];
// 求得 rgb 平均值
average = Math.floor((red + green + blue) / 3);
// 設(shè)置顏色值,透明度不變
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
// 回寫圖像數(shù)據(jù)并顯示結(jié)果
imageData.data = data;
context.putImageData(imageData, 0, 0);
}
??上述例子首先在畫面上繪制了一幅圖像,然后取得了原始圖像數(shù)據(jù)。其中的 for 循環(huán)遍歷了圖像數(shù)據(jù)中的每一個(gè)像素。這里要注意的是,每次循環(huán)控制變量 i 都遞增 4。
??在取得每個(gè)像素的紅、綠、藍(lán)顏色值后,計(jì)算出它們的平均值。再把這個(gè)平均值設(shè)置為每個(gè)顏色的值,結(jié)果就是去掉了每個(gè)像素的顏色,只保留了亮度接近的灰度值(即彩色變黑白)。
??在把 data 數(shù)組回寫到 imageData 對象后,調(diào)用putImageData() 方法把圖像數(shù)據(jù)繪制到畫布上。最終得到了圖像的黑白版。
??當(dāng)然,通過操作原始像素值不僅能實(shí)現(xiàn)灰階過濾,還能實(shí)現(xiàn)其他功能。
??只有在畫布“干凈”的情況下(即圖像并非來自其他域),才可以取得圖像數(shù)據(jù)。如果畫布“不干凈”,那么訪問圖像數(shù)據(jù)時(shí)會(huì)導(dǎo)致 JavaScript 錯(cuò)誤。
2.11、合成
??還有兩個(gè)會(huì)應(yīng)用到 2D 上下文中所有繪制操作的屬性:globalAlpha 和 globalCompositionOperation。
??其中,globalAlpha 是一個(gè)介于 0 和 1 之間的值(包括 0 和 1),用于指定所有繪制的透明度。默認(rèn)值為 0。如果所有后續(xù)操作都要基于相同的透明度,就可以先把 globalAlpha 設(shè)置為適當(dāng)值,然后繪制,最后再把它設(shè)置回默認(rèn)值 0。示例:
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 修改全局透明度
context.globalAlpha = 0.5;
// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0, 0, 255, 1)";
context.fillRect(30, 30, 50, 50);
// 重置全局透明度
context.globalAlpha = 0;
??在上述例子中,我們把藍(lán)色矩形繪制到了紅色矩形上面。因?yàn)樵诶L制藍(lán)色矩形前,globalAlpha 已經(jīng)被設(shè)置為 0.5,所以藍(lán)色矩形會(huì)呈現(xiàn)半透明效果,透過它可以看到下面的紅色矩形。
??第二個(gè)屬性 globalCompositionOperation 表示后繪制的圖形怎樣與先繪制的圖形結(jié)合。這個(gè)屬性的值是字符串,可能的值如下。
- source-over(默認(rèn)值):后繪制的圖形位于先繪制的圖形上方。
- source-in:后繪制的圖形與先繪制的圖形重疊的部分可見,兩者其他部分完全透明。
- source-out:后繪制的圖形與先繪制的圖形不重疊的部分可見,先繪制的圖形完全透明。
- source-atop:后繪制的圖形與先繪制的圖形重疊的部分可見,先繪制圖形不受影響。
- destination-over:后繪制的圖形位于先繪制的圖形下方,只有之前透明像素下的部分才可見。
- destination-in:后繪制的圖形位于先繪制的圖形下方,兩者不重疊的部分完全透明。
- destination-out:后繪制的圖形擦除與先繪制的圖形重疊的部分。
- destination-atop:后繪制的圖形位于先繪制的圖形下方,在兩者不重疊的地方,先繪制的圖形會(huì)變透明。
- lighter:后繪制的圖形與先繪制的圖形重疊部分的值相加,使該部分變亮。
- copy:后繪制的圖形完全替代與之重疊的先繪制圖形。
- xor:后繪制的圖形與先繪制的圖形重疊的部分執(zhí)行“異或”操作。
??這個(gè)合成操作實(shí)際上用語言或者黑白圖像是很難說清楚的。要了解每個(gè)操作的具體效果,請參見 https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html。推薦使用 IE9+或 Firefox 4+訪問前面的網(wǎng)頁,因?yàn)檫@兩款瀏覽器對 Canvas 的實(shí)現(xiàn)最完善。下面來看一個(gè)例子。
// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 設(shè)置合成操作
context.globalCompositeOperation = "destination-over";
// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
??如果不修改 globalCompositionOperation,那么藍(lán)色矩形應(yīng)該位于紅色矩形之上。但把 globalCompositionOperation 設(shè)置為"destination-over"之后,紅色矩形跑到了藍(lán)色矩形上面。
??在使用 globalCompositionOperation 的情況下,一定要多測試一些瀏覽器。因?yàn)椴煌瑸g覽器對這個(gè)屬性的實(shí)現(xiàn)仍然存在較大的差別。Safari 和 Chrome 在這方面還有問題,至于有什么問題,大家可以比較在打開上述頁面的情況下,IE9+和 Firefox 4+與它們有什么差異。
小結(jié)
??HTML5 的<canvas>元素提供了一組 JavaScript API,讓我們可以動(dòng)態(tài)地創(chuàng)建圖形和圖像。圖形是在一個(gè)特定的上下文中創(chuàng)建的,而上下文對象目前有兩種。第一種是 2D 上下文,可以執(zhí)行原始的繪圖操作,比如:
- 設(shè)置填充、描邊顏色和模式
- 繪制矩形
- 繪制路徑
- 繪制文本
- 創(chuàng)建漸變和模式
??第二種是 3D 上下文,即 WebGL 上下文。WebGL 是從 OpenGL ES 2.0 移植到瀏覽器中的,而 OpenGLES 2.0 是游戲開發(fā)人員在創(chuàng)建計(jì)算機(jī)圖形圖像時(shí)經(jīng)常使用的一種語言。WebGL 支持比 2D 上下文更豐富和更強(qiáng)大的圖形圖像處理能力,比如:
- 用 GLSL(OpenGL Shading Language,OpenGL 著色語言)編寫的頂點(diǎn)和片段著色器
- 支持類型化數(shù)組,即能夠?qū)?shù)組中的數(shù)據(jù)限定為某種特定的數(shù)值類型
- 創(chuàng)建和操作紋理
??目前,主流瀏覽器的較新版本大都已經(jīng)支持<canvas>標(biāo)簽。同樣地,這些版本的瀏覽器基本上也都支持 2D 上下文。但對于 WebGL 而言,目前還只有 Firefox 4+和 Chrome 支持它。