使用 HTML5 canvas 進行 Web 繪圖

簡介: 新的 HTML5 規範旨在幫助開發人員更輕鬆的編寫出各類 Web 應用,以順應當前 SaaS,雲計算以及 RIA 等技術的最新趨勢。在 HTML5 得以廣泛推廣之前,開發人員通常使用 SVG,VML 等技術進行 Web 繪圖操作,但這些基於 XML 的繪圖語言聲明式的繪圖方式並不能滿足複雜繪圖操作在性能上的需求,比如 Web 遊戲所需要的像素級別的繪圖能力。HTML5 canvas 元素的出現填補了這種不足,開發人員可以使用 JavaScript 腳本語言在 canvas 中進行一系列基於命令的圖形繪製操作,本文將通過講解如何使用 canvas 元素進行基本繪圖操作,以及完成簡單的動畫和用戶交互任務,闡明 canvas 在幫助構建 Web 圖形類應用時所能夠提供的能力。
背景介紹
HTML5 中新引入的 canvas 元素使得 Web 開發人員在無須藉助任何第三方插件(如 Flash,Silverlight)的情況下,可以直接使用 JavaScript 腳本在 Web 頁面進行繪圖。它首次由蘋果公司的 Webkit 框架引入實現,併成功運用在 Safari 瀏覽器中,讀者在 這裏可以體驗到基於 canvas 的精彩示例。目前,canvas 已成爲 HTML5 規範中的事實性標準,並且已經被 Firefox 3.0+, Safari 3.0+, Chrome 3.0+, Opera10.0+ 等瀏覽器所支持。最近(本文撰寫之時),IE 也正式宣稱將在其 9.0 版本之後,開始對 canvas 元素進行支持。
基於 canvas 的繪圖填補了 SVG 繪圖的在複雜繪圖操作,特別是性能方面的不足,可廣泛應用於 Dashboard,2D/3D Game 等 Web 應用中。
基本繪圖 API
在瞭解了什麼是 canvas 元素之後,是時候使用 canvas 在 Web 頁面上真正進行的繪圖操作了。實際上,單獨的一個 canvas 標記只是在頁面中定義了一塊矩形區域,並無特別之處,開發人員只有配合使用 JavaScript 腳本,才能夠完成各種圖形,線條,以及複雜的圖形變換操作,與基於 SVG 來實現同樣繪圖效果來比較,canvas 繪圖是一種像素級別的位圖繪圖技術,而 SVG 則是一種矢量繪圖技術。正鑑於這種本質機理的不同,如何更快速高效的進行 canvas 渲染成爲各主流 JavaScript 執行引擎性能比拼的重要指標之一。目前,Chrome 的 V8, Firefox 的 SpiderMonkey 以及 Safari 的 Nitro 等引擎都已經能夠很好的滿足二維繪圖所需的必要性能指標,雖然在運行一些基於 canvas 的遊戲時 CPU 佔用率還是相對較高,但我們有理由相信隨着 NVIDIA 和 AMD 等一系列硬件廠商的參與,硬件加速技術將大大提升 Web 應用的性能。
在開始繪圖之前,我們需要首先創建一個指定大小的 canvas,併爲其指定一個 id,方便在 JavaScript 腳本中獲取該 DOM 實例對象。聲明一個 canvas 節點的方式如下所示。
 <canvas id="canvas" width="300" height="200"> 
 Fallback content, in case the browser does not support Canvas. 
 </canvas> 

需要指明的是,由於無法保證所有用戶使用的瀏覽器都能夠支持 canvas 元素,所以在目前開發基於 canvas 的 Web 應用中需要增加“Fallback content”,以提示用戶他們無法正常體驗此功能的原因或建議他們去下載最新的瀏覽器。
這裏,好奇的讀者可能會問,既然這是一個普通的 DOM 節點,那麼便意味着可以通過直接改變其 width 或 height 屬性值來改變 canvas 的大小?確實如此,但是,正如之前提到的 canvas 是一種像素級別的繪圖方法,因而,一旦動態調整 canvas 的大小,canvas 將被“重置”到一個新的初始狀態,即便是如下這種操作,也會將 canvas 內的位圖清除並將所有相關屬性恢復到初始值的狀態。當然,我們也可以把這當作重置 canvas 的小技巧來使用。
 document.getElementById("canvas").width = document.getElementById("canvas").width; 

簡單圖形繪製
基於 canvas 的繪圖並不是直接在 canvas 標記所創建的繪圖畫面上進行各種繪圖操作,而是依賴畫面所提供的 渲染上下文(Rendering Context),所有的繪圖命令和屬性都定義在渲染上下文當中。在通過 canvas id 獲取相應的 DOM 對象之後首先要做的事情就是獲取渲染上下文對象。 渲染上下文與 canvas 一一對應,無論對同一 canvas 對象調用幾次 getContext() 方法,都將返回同一個上下文對象。目前,所有支持 canvas 標籤的瀏覽器都支持 2D 渲染上下文,可以使用如下的代碼來獲取該對象。
 var context = document.getElementById("canvas").getContext("2d"); 

除此之外,在不久的將來,開發人員還會能夠得到基於 OpenGL 的 3D 渲染上下文以在 canvas 中進行 3D 繪圖。
與 SVG 不同,canvas 原生支持的基本圖形只有矩形一種,至於其他的圓形,多邊形等圖形則都由路徑來負責繪製實現。清單 1 展示瞭如何使用渲染上下文中的矩形繪圖方法完成了圖 1 所示圖形。

圖 1. 清單 1 對應的示例圖形
圖 

清單 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 矩形區域內所有像素
 ctx.fillStyle = '#00f';   // 設置矩形的填充屬性,#00f 代表藍色
 ctx.strokeStyle = '#f00';  // 設置矩形的線條顏色,#f00 代表紅色
 ctx.fillRect(50,25,150,80); // 使用 fillStyle 填充一個 150*80 大小的矩形
 ctx.strokeRect(45,20, 160, 90);  // 以 strokeStype 屬性爲邊的顏色繪製一個無填充矩形
     } 
 } 

繪製路徑
在開始動手繪製路徑之前,首先需要明確的是:矩形繪製 API 是一種即時性的 API,他會在相應的繪圖函數執行完畢之後,將圖形即時的渲染在畫面上。然而路徑繪製 API 並非如此,完整的路徑繪製過程大致可以分爲如下兩個階段:
  • 定義路徑輪廓:
在每個 canvas 實例對象中都擁有一個 path 對象,創建自定義圖形的過程就是不斷對 path 對象操作的過程。每當開始一次新的圖形繪製任務,都需要先使用 beginPath() 方法來重置 path 對象至初始狀態,進而通過一系列對 moveTo/lineTo 等畫線方法的調用,繪製期望的路徑,其中 moveTo(x, y) 方法設置繪圖起始座標,而 lineTo(x,y) 等畫線方法可以從當前起點繪製直線,圓弧以及曲線到目標位置。最後一步,也是可選的步驟,是調用 closePath() 方法將自定義圖形進行閉合,該方法將自動創建一條從當前座標到起始座標的直線。
  • 繪製路徑
定義完路徑的輪廓,此時 canvas 畫面中沒有顯示任何路徑,開發人員還可以對路徑進行修改。一旦確定完成,則需要繼續調用 stroke()/fill() 函數來完成將路徑渲染到畫面的最後一步。路徑的輪廓顏色和填充顏色由 strokeStyle 和 fillStyle 屬性決定。
清單 2 繪製一個圖 2 所示半圓弧,並通過 closePath() 方法完成圖形的閉合。

圖 2. 清單 2 對應的示例圖形
圖 

清單 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();    // 自動繪製一條直線來關閉弧線。若不調用此方法,將僅僅顯示一條半圓弧
 ctx.fill();      // 可以嘗試註釋掉 fill 或者 stroke 函數,觀察圖形的變化
 ctx.stroke();  
	 } 
 } 

二維變形
Canvas 繪圖中另一個重要的概念是 繪畫狀態(Drawing State),繪畫狀態反映了渲染上下文當前的瞬時狀態,開發人員可以通過對繪畫狀態的保存 / 恢復操作而快速的回到之前使用的各種屬性和變形操作。繪畫狀態主要由以下三個部分構成:
  • 當前的變形矩陣(transformation matrix)
  • 當前的裁剪區域(clipping region)
  • 當前上下文中的屬性,比如 strokeStyle, fillType, globalAlpha, font 等等。
需要指出的是,當前路徑對象以及當前的位圖都不包含在繪畫狀態之中,路徑是持續性的對象,如前文所講,只有通過 beginPath() 操作纔會進行重置,而位圖則是 canvas 的屬性,並非屬於渲染上下文的。
開發人員可以使用 save 和 restore 兩種方法來保存和恢復 canvas 狀態,每調用 save 方法,都會將當前狀態壓入堆棧中,而相應的 restore 方法則會從堆棧中彈出一個狀態,並將當前畫面恢復至該狀態。繪畫狀態在 canvas 圖形變形操作中應用極爲廣泛,也非常重要,因爲調用一個 restore 方法遠比手動恢復先前狀態要簡單許多,因而,一個較好的習慣是在做變形操作之前先保存 canvas 狀態。
二維繪圖的常用變形操作在 canvas 中都可到了很好的支持,包括平移(Translate),旋轉(Rotate),伸縮(Scale)等等。由於所有的變形操作都基於變形矩陣,因而開發人員始終需要記住一點的就是,一旦沒有使用 save/restore 操作保持住原來的繪圖狀態,那麼後續的繪圖操作,都會在當前所應用的變形狀態下完成。清單 3 使用平移和旋轉方法繪製瞭如下所示畫面。

圖 3. 清單 3 所示示例圖形
圖 

清單 3. 使用平移 / 旋轉變形方法繪製複雜位圖
				 
 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++){        // 繪製內外 2 層
 if ((i % 2) == 1) {ctx.fillStyle = '#00f';} 
 else{ ctx.fillStyle = '#f00'; } 
 ctx.save();             // 保持開始繪製每一層時的狀態一致
 for (j=0;j<=i*6;j++){   // 每層生成點的數量
 ctx.rotate(Math.PI/(3*i));  // 繞當前原點將座標系順時針旋轉 Math.Pi/(3*i) 度
				 ctx.beginPath(); 
				 ctx.arc(0,20*i,5,0,Math.PI*2,true); 
 ctx.fill();         // 使用 fillType 值填充每個點
 } 
 ctx.restore();   
		 } 
	 } 
 } 

像素級繪圖
像素級別的繪圖操作是 canvas 繪圖區別於 SVG,VML 等繪圖技術的最爲明顯特徵之一,渲染上下文提供了 createImageData, getImageData, 和 putImageData 三種方法來進行鍼對像素的操作,所基於的對象都是 imageData 對象。imageData 對象包含 width、height 和 data 三個屬性,其中 data 包含了 width × height × 4 個像素值,之所以乘以 4,在於每個像素都有 RGB 值和透明度 alpha 值。
清單 4 中所示代碼爲上一節中示例圖形增添了簡單的顏色反轉濾鏡效果,通過調用 getImageData(x,y,width,height) 方法獲取以(x,y)爲左上座標的矩形區域內所有像素,而後對所有像素的 RGB 值做取反操作,最後通過 putImageData(imageData, x, y)將修改後的像素值重新繪製到在 canvas 上。

圖 4. 清單 4 所示示例圖形
圖 

清單 4. 實現簡單濾鏡效果
				 
 function revertImage(){ 
 var canvas = document.getElementById('canvas'); 
 if (canvas.getContext){ 
 var context = canvas.getContext('2d'); 
 // 從指定的矩形區域獲取 canvas 像素數組
 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 並非爲了製作動畫而出現,自然沒有動畫製作中幀的概念。因而,使用定時器不斷的重繪 canvas 畫面成爲了實現動畫效果的通用解決方式。Javascript 中的 setInterval(code,millisec) 方法可以按照指定的時間間隔 millisec 來反覆調用 code 所指向的函數或代碼串,這樣,通過將繪圖函數作爲第一個參數傳給 setInterval 方法,在每次被調用的過程中移動畫面中圖形的位置,來最終達到一種動畫的體驗。需要注意的一點是,雖然 setinterval 方法的第二個參數允許開發人員對繪圖函數的調用頻率進行設定,但這始終都是一種最爲理想的情況,由於這種繪圖頻率很大程度上取決於支持 canvas 的底層 JavaScript 引擎的渲染速度以及相應繪圖函數的複雜性,因而實際運行的結果往往都是要慢於指定繪圖頻率的。
清單 5 顯示了一個小彈力球動畫效果,在球沒有到達四周邊界時,繪圖方法不斷的移動所繪小球的橫縱座標。並且,在每次重繪之前,都是用 clear 方法將之前的畫面清除。

清單 5. 實現小彈力球動畫
				 
 <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)座標出畫一個圓
	
 // 判斷邊界值,調整 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);     // 設置繪圖週期爲 20 毫秒
 } 
 </script> 

提高可訪問性
一款優秀的 Web 應用必須要做到的就是提供給用戶很好的可訪問性,這包括對鼠標,鍵盤以及快捷鍵等操作的響應,canvas 畫面的本質仍是一個 DOM 節點,因而開發人員可以通過常規的方法來處理響應。這裏,與基於 SVG 的繪圖不同,由於 SVG 是一種基於 XML 的聲明式的繪圖方式,因而,SVG 中任何的圖形都可以作爲一個獨立的 DOM 節點去接收並響應特定事件,而 canvas 由於其像素繪圖的本質,則只可以在 canvas 元素節點去處理。
圖 5 所示示例代碼,當鼠標在 canvas 中移動時,鼠標當前相對於 canvas 中的橫縱座標將實時輸出到上方提示信息區域;當用戶在 canvas 中單擊鼠標左鍵,將在相應位置創建一個藍色小球,而後用戶可以通過鍵盤上的左 / 右方向鍵對藍色小球進行控制,使其進行橫向的移動。示例代碼如清單 6 所示。

圖 5. 清單 6 所示示例展現
 

清單 6. 實現 canvas 對方向鍵和鼠標點擊事件的響應
				 
 <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 節點對鼠標單擊,移動以及鍵盤事件的響應函數
 canvas.addEventListener('click', function(evt){drawCircle(g_x, g_y);} , false);
 canvas.addEventListener('mousemove', onMouseMove, false); 
 canvas.addEventListener('keypress', onKeyPress, false); 
 canvas.focus();  // 獲得焦點之後,才能夠對鍵盤事件進行捕獲
 } 
 </script> 

這裏我們對鼠標的移動,單擊操作進行響應,在實際應用中可以視特定應用的需求,增加對鼠標摁下,鬆開或雙擊等更爲豐富操作的響應,增強應用的可訪問性。
細心的讀者可能發現,在通過不斷重繪畫面以達到動畫效果的過程中,我們的重繪方法首先做的事情都是調用 clearRect(x, y, width, height) 方法將原畫面清空,這種銷燬而後重繪的方式丟失了之前的畫面,使得開發人員不得不重繪整幅畫面,這在性能上是難以接受的,一種可行的做法是通過多個 canvas 疊加的方式,根據不同 canvas 上的不同刷新頻率,分別完成各自的重繪任務。這種多 canvas 技巧,在處理繪圖類應用中最爲常見的“撤銷”操作時也非常有效,所有的繪圖都發生在上層 canvas,只有被用戶確認的畫面,纔會被繪製到底層 canvas 上。鑑於本文所討論技術範圍,這裏不做過多講解,有興趣的讀者可以通過本文參考文獻所列資源,進行進一步的深入學習。
總結
本文對 HTML5 新引入的 canvas 元素在 Web 繪圖中所扮演的角色和所發揮的作用做了最基本的介紹,其中包括使用 canvas 完成基本的 Web 繪圖,動畫和交互任務,雖然 Flash,Silverlight 也都可以完成相同的任務,甚至在性能上更勝一籌,但是作爲一種不依賴任何插件的標準 Web 像素級繪圖技術,我們有理由相信隨着各大瀏覽器廠商的加入,canvas 將會更加成熟完善,也會有更多基於 canvas 的繪圖類應用不斷涌現。
聲明
本人所發表的內容僅爲個人觀點,不代表 IBM 公司立場、戰略和觀點。

 
參考資料
學習
  • 查看 HTML5 專題,瞭解更多和 HTML5 相關的知識和動向。

  • 查看 Mozilla 開發者中心,瞭解更多 HTML5 canvas 知識。

  • 查看 WhatWG 制定的關於 HTML5 canvas 規範,瞭解更多底層繪圖 API。

  • 查看 Opera 開發者中心,瞭解如何使用 canvas 創建 Web 繪圖類應用。

  • 通過 結合 GFX, DnD 與 Dijit 創建基於 Dojo 的 Web 圖形類應用,瞭解如何基於 SVG 和 Dojo 構建 Web 繪圖類應用。

  • 訪問 這裏,查看更多基於 HTML5 canvas 構建的精彩示例。

  • developerWorks Web development 專區:通過專門關於 Web 技術的文章和教程,擴展您在網站開發方面的技能。

  • developerWorks Ajax 資源中心:這是有關 Ajax 編程模型信息的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新信息都能在這裏找到。

  • developerWorks Web 2.0 資源中心,這是有關 Web 2.0 相關信息的一站式中心,包括大量 Web 2.0 技術文章、教程、下載和相關技術資源。您還可以通過 Web 2.0 新手入門 欄目,迅速瞭解 Web 2.0 的相關概念。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章