HTML5 中新引入的 canvas 元素使得 Web 開發(fā)人員在無須借助任何第三方插件(如 Flash,Silverlight)的情況下,可以直接使用 JavaScript 腳本在 Web 頁面進行繪圖。它首次由蘋果公司的 Webkit 框架引入實現(xiàn),并成功運用在 Safari 瀏覽器中,讀者在 這里可以體驗到基于 canvas 的精彩示例。目前,canvas 已成為 HTML5 規(guī)范中的事實性標準,并且已經(jīng)被 Firefox 3.0+, Safari 3.0+, Chrome 3.0+, Opera10.0+ 等瀏覽器所支持。最近(本文撰寫之時),IE 也正式宣稱將在其 9.0 版本之后,開始對 canvas 元素進行支持。
基于 canvas 的繪圖填補了 SVG 繪圖的在復(fù)雜繪圖操作,特別是性能方面的不足,可廣泛應(yīng)用于 Dashboard,2D/3D Game 等 Web 應(yīng)用中。
在了解了什么是 canvas 元素之后,是時候使用 canvas 在 Web 頁面上真正進行的繪圖操作了。實際上,單獨的一個 canvas 標記只是在頁面中定義了一塊矩形區(qū)域,并無特別之處,開發(fā)人員只有配合使用 JavaScript 腳本,才能夠完成各種圖形,線條,以及復(fù)雜的圖形變換操作,與基于 SVG 來實現(xiàn)同樣繪圖效果來比較,canvas 繪圖是一種像素級別的位圖繪圖技術(shù),而 SVG 則是一種矢量繪圖技術(shù)。正鑒于這種本質(zhì)機理的不同,如何更快速高效的進行 canvas 渲染成為各主流 JavaScript 執(zhí)行引擎性能比拼的重要指標之一。目前,Chrome 的 V8, Firefox 的 SpiderMonkey 以及 Safari 的 Nitro 等引擎都已經(jīng)能夠很好的滿足二維繪圖所需的必要性能指標,雖然在運行一些基于 canvas 的游戲時 CPU 占用率還是相對較高,但我們有理由相信隨著 NVIDIA 和 AMD 等一系列硬件廠商的參與,硬件加速技術(shù)將大大提升 Web 應(yīng)用的性能。
在開始繪圖之前,我們需要首先創(chuàng)建一個指定大小的 canvas,并為其指定一個 id,方便在 JavaScript 腳本中獲取該 DOM 實例對象。聲明一個 canvas 節(jié)點的方式如下所示。
<canvas id="canvas" width="300" height="200"> Fallback content, in case the browser does not support Canvas. </canvas> |
需要指明的是,由于無法保證所有用戶使用的瀏覽器都能夠支持 canvas 元素,所以在目前開發(fā)基于 canvas 的 Web 應(yīng)用中需要增加“Fallback content”,以提示用戶他們無法正常體驗此功能的原因或建議他們?nèi)ハ螺d最新的瀏覽器。
這里,好奇的讀者可能會問,既然這是一個普通的 DOM 節(jié)點,那么便意味著可以通過直接改變其 width 或 height 屬性值來改變 canvas 的大小?確實如此,但是,正如之前提到的 canvas 是一種像素級別的繪圖方法,因而,一旦動態(tài)調(diào)整 canvas 的大小,canvas 將被“重置”到一個新的初始狀態(tài),即便是如下這種操作,也會將 canvas 內(nèi)的位圖清除并將所有相關(guān)屬性恢復(fù)到初始值的狀態(tài)。當然,我們也可以把這當作重置 canvas 的小技巧來使用。
document.getElementById("canvas").width = document.getElementById("canvas").width;
基于 canvas 的繪圖并不是直接在 canvas 標記所創(chuàng)建的繪圖畫面上進行各種繪圖操作,而是依賴畫面所提供的 渲染上下文(Rendering Context),所有的繪圖命令和屬性都定義在渲染上下文當中。在通過 canvas id 獲取相應(yīng)的 DOM 對象之后首先要做的事情就是獲取渲染上下文對象。 渲染上下文與 canvas 一一對應(yīng),無論對同一 canvas 對象調(diào)用幾次 getContext() 方法,都將返回同一個上下文對象。目前,所有支持 canvas 標簽的瀏覽器都支持 2D 渲染上下文,可以使用如下的代碼來獲取該對象。
var context = document.getElementById("canvas").getContext("2d");
除此之外,在不久的將來,開發(fā)人員還會能夠得到基于 OpenGL 的 3D 渲染上下文以在 canvas 中進行 3D 繪圖。
與 SVG 不同,canvas 原生支持的基本圖形只有矩形一種,至于其他的圓形,多邊形等圖形則都由路徑來負責繪制實現(xiàn)。清單 1 展示了如何使用渲染上下文中的矩形繪圖方法完成了圖 1 所示圖形。
圖 1. 清單 1 對應(yīng)的示例圖形
清單 1. 繪制 canvas 矩形
function drawRect(){
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d'); // 獲取 2D 渲染上下文
ctx.clearRect(0,0,300,200) ;// 清除以(0,0)為左上坐標原點,300*200 矩形區(qū)域內(nèi)所有像素
ctx.fillStyle = '#00f'; // 設(shè)置矩形的填充屬性,#00f 代表藍色
ctx.strokeStyle = '#f00'; // 設(shè)置矩形的線條顏色,#f00 代表紅色
ctx.fillRect(50,25,150,80); // 使用 fillStyle 填充一個 150*80 大小的矩形
ctx.strokeRect(45,20, 160, 90); // 以 strokeStype 屬性為邊的顏色繪制一個無填充矩形
}
}
在開始動手繪制路徑之前,首先需要明確的是:矩形繪制 API 是一種即時性的 API,他會在相應(yīng)的繪圖函數(shù)執(zhí)行完畢之后,將圖形即時的渲染在畫面上。然而路徑繪制 API 并非如此,完整的路徑繪制過程大致可以分為如下兩個階段:
- 定義路徑輪廓:
在每個 canvas 實例對象中都擁有一個 path 對象,創(chuàng)建自定義圖形的過程就是不斷對 path 對象操作的過程。每當開始一次新的圖形繪制任務(wù),都需要先使用 beginPath() 方法來重置 path 對象至初始狀態(tài),進而通過一系列對 moveTo/lineTo 等畫線方法的調(diào)用,繪制期望的路徑,其中 moveTo(x, y) 方法設(shè)置繪圖起始坐標,而 lineTo(x,y) 等畫線方法可以從當前起點繪制直線,圓弧以及曲線到目標位置。最后一步,也是可選的步驟,是調(diào)用 closePath() 方法將自定義圖形進行閉合,該方法將自動創(chuàng)建一條從當前坐標到起始坐標的直線。
- 繪制路徑
定義完路徑的輪廓,此時 canvas 畫面中沒有顯示任何路徑,開發(fā)人員還可以對路徑進行修改。一旦確定完成,則需要繼續(xù)調(diào)用 stroke()/fill() 函數(shù)來完成將路徑渲染到畫面的最后一步。路徑的輪廓顏色和填充顏色由 strokeStyle 和 fillStyle 屬性決定。
清單 2 繪制一個圖 2 所示半圓弧,并通過 closePath() 方法完成圖形的閉合。
圖 2. 清單 2 對應(yīng)的示例圖形
清單 2. 繪制 canvas 路徑
function draw(){
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#00f';
ctx.strokeStyle = '#f00';
ctx.beginPath();
ctx.arc(75,75,30,0,Math.PI, false); // 繪制一條半圓弧線
ctx.closePath(); // 自動繪制一條直線來關(guān)閉弧線。若不調(diào)用此方法,將僅僅顯示一條半圓弧
ctx.fill(); // 可以嘗試注釋掉 fill 或者 stroke 函數(shù),觀察圖形的變化
ctx.stroke();
}
}
Canvas 繪圖中另一個重要的概念是 繪畫狀態(tài)(Drawing State),繪畫狀態(tài)反映了渲染上下文當前的瞬時狀態(tài),開發(fā)人員可以通過對繪畫狀態(tài)的保存 / 恢復(fù)操作而快速的回到之前使用的各種屬性和變形操作。繪畫狀態(tài)主要由以下三個部分構(gòu)成:
- 當前的變形矩陣(transformation matrix)
- 當前的裁剪區(qū)域(clipping region)
- 當前上下文中的屬性,比如 strokeStyle, fillType, globalAlpha, font 等等。
需要指出的是,當前路徑對象以及當前的位圖都不包含在繪畫狀態(tài)之中,路徑是持續(xù)性的對象,如前文所講,只有通過 beginPath() 操作才會進行重置,而位圖則是 canvas 的屬性,并非屬于渲染上下文的。
開發(fā)人員可以使用 save 和 restore 兩種方法來保存和恢復(fù) canvas 狀態(tài),每調(diào)用 save 方法,都會將當前狀態(tài)壓入堆棧中,而相應(yīng)的 restore 方法則會從堆棧中彈出一個狀態(tài),并將當前畫面恢復(fù)至該狀態(tài)。繪畫狀態(tài)在 canvas 圖形變形操作中應(yīng)用極為廣泛,也非常重要,因為調(diào)用一個 restore 方法遠比手動恢復(fù)先前狀態(tài)要簡單許多,因而,一個較好的習(xí)慣是在做變形操作之前先保存 canvas 狀態(tài)。
二維繪圖的常用變形操作在 canvas 中都可到了很好的支持,包括平移(Translate),旋轉(zhuǎn)(Rotate),伸縮(Scale)等等。由于所有的變形操作都基于變形矩陣,因而開發(fā)人員始終需要記住一點的就是,一旦沒有使用 save/restore 操作保持住原來的繪圖狀態(tài),那么后續(xù)的繪圖操作,都會在當前所應(yīng)用的變形狀態(tài)下完成。清單 3 使用平移和旋轉(zhuǎn)方法繪制了如下所示畫面。
圖 3. 清單 3 所示示例圖形
清單 3. 使用平移 / 旋轉(zhuǎn)變形方法繪制復(fù)雜位圖
function drawPointCircle(){
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
ctx.translate(150,150); // 將 canvas 的原點從 (0,0) 平移至(150,150)
for (i=1;i<=2;i++){ // 繪制內(nèi)外 2 層
if ((i % 2) == 1) {ctx.fillStyle = '#00f';}
else{ ctx.fillStyle = '#f00'; }
ctx.save(); // 保持開始繪制每一層時的狀態(tài)一致
for (j=0;j<=i*6;j++){ // 每層生成點的數(shù)量
ctx.rotate(Math.PI/(3*i)); // 繞當前原點將坐標系順時針旋轉(zhuǎn) Math.Pi/(3*i) 度
ctx.beginPath();
ctx.arc(0,20*i,5,0,Math.PI*2,true);
ctx.fill(); // 使用 fillType 值填充每個點
}
ctx.restore();
}
}
}
像素級別的繪圖操作是 canvas 繪圖區(qū)別于 SVG,VML 等繪圖技術(shù)的最為明顯特征之一,渲染上下文提供了 createImageData, getImageData, 和 putImageData 三種方法來進行針對像素的操作,所基于的對象都是 imageData 對象。imageData 對象包含 width、height 和 data 三個屬性,其中 data 包含了 width × height × 4 個像素值,之所以乘以 4,在于每個像素都有 RGB 值和透明度 alpha 值。
清單 4 中所示代碼為上一節(jié)中示例圖形增添了簡單的顏色反轉(zhuǎn)濾鏡效果,通過調(diào)用 getImageData(x,y,width,height) 方法獲取以(x,y)為左上坐標的矩形區(qū)域內(nèi)所有像素,而后對所有像素的 RGB 值做取反操作,最后通過 putImageData(imageData, x, y)將修改后的像素值重新繪制到在 canvas 上。
圖 4. 清單 4 所示示例圖形
清單 4. 實現(xiàn)簡單濾鏡效果
function revertImage(){
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var context = canvas.getContext('2d');
// 從指定的矩形區(qū)域獲取 canvas 像素數(shù)組
var imgdata = context.getImageData(100, 100, 100, 100);
var pixels = imgdata.data;
// 遍歷每個像素并對 RGB 值進行取反
for (var i=0, n=pixels.length; i<n; i+= 4){
pixels[i] = 255-pixels[i];
pixels[i+1] = 255-pixels[i+1];
pixels[i+2] = 255-pixels[i+2];
}
// 在指定位置進行像素重繪
context.putImageData(imgdata, 100, 100);
}
}
Canvas 并非為了制作動畫而出現(xiàn),自然沒有動畫制作中幀的概念。因而,使用定時器不斷的重繪 canvas 畫面成為了實現(xiàn)動畫效果的通用解決方式。Javascript 中的 setInterval(code,millisec) 方法可以按照指定的時間間隔 millisec 來反復(fù)調(diào)用 code 所指向的函數(shù)或代碼串,這樣,通過將繪圖函數(shù)作為第一個參數(shù)傳給 setInterval 方法,在每次被調(diào)用的過程中移動畫面中圖形的位置,來最終達到一種動畫的體驗。需要注意的一點是,雖然 setinterval 方法的第二個參數(shù)允許開發(fā)人員對繪圖函數(shù)的調(diào)用頻率進行設(shè)定,但這始終都是一種最為理想的情況,由于這種繪圖頻率很大程度上取決于支持 canvas 的底層 JavaScript 引擎的渲染速度以及相應(yīng)繪圖函數(shù)的復(fù)雜性,因而實際運行的結(jié)果往往都是要慢于指定繪圖頻率的。
清單 5 顯示了一個小彈力球動畫效果,在球沒有到達四周邊界時,繪圖方法不斷的移動所繪小球的橫縱坐標。并且,在每次重繪之前,都是用 clear 方法將之前的畫面清除。
清單 5. 實現(xiàn)小彈力球動畫
<script type="text/javascript">
var x=0,y=0,dx=2,dy=3,context2D; // 小球從(0,0)開始移動,橫向步長為 2,縱向步長為 3
function draw(){
context2D.clearRect(0, 0, canvas.width, canvas.height); // 清除整個 canvas 畫面
drawCircle(x, y); // 使用自定義的畫圓方法,在當前(x,y)坐標出畫一個圓
// 判斷邊界值,調(diào)整 dx/dy 以改變 x/y 坐標變化方向。
if (x + dx > canvas.width || x + dx < 0) dx = -dx;
if (y + dy > canvas.height || y + dy < 0) dy = -dy;
x += dx;
y += dy;
}
window.onload = function (){
var canvas = document.getElementById('canvas');
context2D = canvas.getContext('2d');
setInterval(draw, 20); // 設(shè)置繪圖周期為 20 毫秒
}
</script>
一款優(yōu)秀的 Web 應(yīng)用必須要做到的就是提供給用戶很好的可訪問性,這包括對鼠標,鍵盤以及快捷鍵等操作的響應(yīng),canvas 畫面的本質(zhì)仍是一個 DOM 節(jié)點,因而開發(fā)人員可以通過常規(guī)的方法來處理響應(yīng)。這里,與基于 SVG 的繪圖不同,由于 SVG 是一種基于 XML 的聲明式的繪圖方式,因而,SVG 中任何的圖形都可以作為一個獨立的 DOM 節(jié)點去接收并響應(yīng)特定事件,而 canvas 由于其像素繪圖的本質(zhì),則只可以在 canvas 元素節(jié)點去處理。
圖 5 所示示例代碼,當鼠標在 canvas 中移動時,鼠標當前相對于 canvas 中的橫縱坐標將實時輸出到上方提示信息區(qū)域;當用戶在 canvas 中單擊鼠標左鍵,將在相應(yīng)位置創(chuàng)建一個藍色小球,而后用戶可以通過鍵盤上的左 / 右方向鍵對藍色小球進行控制,使其進行橫向的移動。示例代碼如清單 6 所示。
圖 5. 清單 6 所示示例展現(xiàn)
清單 6. 實現(xiàn) canvas 對方向鍵和鼠標點擊事件的響應(yīng)
<script type="text/javascript">
var g_x,g_y; // 鼠標當前的坐標
var g_pointx, g_pointy; // 藍色小球當前的坐標
var canvas;
function drawCircle(x,y){ // 以鼠標當前位置為原點繪制一個藍色小球
var ctx = canvas.getContext('2d');
ctx.clearRect(0,0,300,300);
ctx.fillStyle = '#00f';
ctx.beginPath();
ctx.arc(x,y,20,0,Math.PI*2,true);
ctx.fill();
g_pointx = x;
g_pointy = y
}
function onMouseMove(evt) {
// 獲取鼠標在 canvas 中的坐標位置
if (evt.layerX || evt.layerX == 0) { // FireFox
g_x = evt.layerX;
g_y = evt.layerY;
}
document.getElementById("xinfo").innerHTML = g_x;
document.getElementById("yinfo").innerHTML = g_y;
}
function onKeyPress(evt) {
var dx = 3; // 橫向平移步長
var kbinfo = document.getElementById("kbinfo");
if (evt.keyCode == 39){
kbinfo.innerHTML="right";
if (g_x<300-dx) drawCircle(g_pointx+dx,g_pointy);
document.getElementById("xinfo").innerHTML = g_pointx;
}else if (evt.keyCode == 37){
kbinfo.innerHTML = "left";
if (g_x>dx) drawCircle(g_pointx-dx,g_pointy);
document.getElementById("xinfo").innerHTML = g_pointx;
}
}
window.onload = function(){
canvas = document.getElementById('canvas');
// 增加 canvas 節(jié)點對鼠標單擊,移動以及鍵盤事件的響應(yīng)函數(shù)
canvas.addEventListener('click', function(evt){drawCircle(g_x, g_y);} , false);
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
canvas.addEventListener('mousemove', onMouseMove, false);
canvas.addEventListener('keypress', onKeyPress, false);
canvas.focus(); // 獲得焦點之后,才能夠?qū)︽I盤事件進行捕獲
}
</script>
這里我們對鼠標的移動,單擊操作進行響應(yīng),在實際應(yīng)用中可以視特定應(yīng)用的需求,增加對鼠標摁下,松開或雙擊等更為豐富操作的響應(yīng),增強應(yīng)用的可訪問性。
細心的讀者可能發(fā)現(xiàn),在通過不斷重繪畫面以達到動畫效果的過程中,我們的重繪方法首先做的事情都是調(diào)用 clearRect(x, y, width, height) 方法將原畫面清空,這種銷毀而后重繪的方式丟失了之前的畫面,使得開發(fā)人員不得不重繪整幅畫面,這在性能上是難以接受的,一種可行的做法是通過多個 canvas 疊加的方式,根據(jù)不同 canvas 上的不同刷新頻率,分別完成各自的重繪任務(wù)。這種多 canvas 技巧,在處理繪圖類應(yīng)用中最為常見的“撤銷”操作時也非常有效,所有的繪圖都發(fā)生在上層 canvas,只有被用戶確認的畫面,才會被繪制到底層 canvas 上。鑒于本文所討論技術(shù)范圍,這里不做過多講解,有興趣的讀者可以通過本文參考文獻所列資源,進行進一步的深入學(xué)習(xí)。
本文對 HTML5 新引入的 canvas 元素在 Web 繪圖中所扮演的角色和所發(fā)揮的作用做了最基本的介紹,其中包括使用 canvas 完成基本的 Web 繪圖,動畫和交互任務(wù),雖然 Flash,Silverlight 也都可以完成相同的任務(wù),甚至在性能上更勝一籌,但是作為一種不依賴任何插件的標準 Web 像素級繪圖技術(shù),我們有理由相信隨著各大瀏覽器廠商的加入,canvas 將會更加成熟完善,也會有更多基于 canvas 的繪圖類應(yīng)用不斷涌現(xiàn)。
本人所發(fā)表的內(nèi)容僅為個人觀點,不代表 IBM 公司立場、戰(zhàn)略和觀點。
學(xué)習(xí)
- 查看 HTML5 專題,了解更多和 HTML5 相關(guān)的知識和動向。
- 查看 Mozilla 開發(fā)者中心,了解更多 HTML5 canvas 知識。
- 查看 WhatWG 制定的關(guān)于 HTML5 canvas 規(guī)范,了解更多底層繪圖 API。
- 查看 Opera 開發(fā)者中心,了解如何使用 canvas 創(chuàng)建 Web 繪圖類應(yīng)用。
- 通過 結(jié)合 GFX, DnD 與 Dijit 創(chuàng)建基于 Dojo 的 Web 圖形類應(yīng)用,了解如何基于 SVG 和 Dojo 構(gòu)建 Web 繪圖類應(yīng)用。
- 訪問 這里,查看更多基于 HTML5 canvas 構(gòu)建的精彩示例。
- developerWorks Web development 專區(qū):通過專門關(guān)于 Web 技術(shù)的文章和教程,擴展您在網(wǎng)站開發(fā)方面的技能。
- developerWorks Ajax 資源中心:這是有關(guān) Ajax 編程模型信息的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新信息都能在這里找到。
- developerWorks Web 2.0 資源中心,這是有關(guān) Web 2.0 相關(guān)信息的一站式中心,包括大量 Web 2.0 技術(shù)文章、教程、下載和相關(guān)技術(shù)資源。您還可以通過 Web 2.0 新手入門 欄目,迅速了解 Web 2.0 的相關(guān)概念。
討論