建置 HTML5 應用程式

使用 HTML5 畫布進行資料視覺化

Brandon Satrom

下載代碼示例

在網路時代的早期階段,Web 只不過是靜態文本和連結的集合,人們越來越關注為其他類型的內容提供支援。 1993 年,Mosaic 流覽器(後來發展為 Netscape Navigator)的創建者 Marc Andreessen 提出將 IMG 標記作為在頁面上的文本中嵌入圖像的標準。 此後不久,IMG 標記成為向網頁中添加圖形資源的事實上的標準—至今仍在使用這一標準。 您甚至可以說,由於 Web 應用已經從 Web 文檔發展到 Web 應用程式,因此 IMG 標記比以往更加重要。

一般而言,媒體肯定比以往更加重要,儘管 Web 上的媒體需求在過去 18 年中不斷演變,但圖像一直是靜態的。 Web 作者越來越希望能夠在其網站和應用程式中使用動態媒體(例如音訊、視頻和互動式動畫),直到最近,主要的解決方案仍然是 Flash 或 Silverlight 之類的外掛程式。

現在有了 HTML5,流覽器中的媒體元件大受青睞。 您可能聽說過新的 Audio 和 Video 標記,二者均允許這些類型的內容充當流覽器中的第一類物件,不需要任何外掛程式。 下個月的文章將深入介紹這兩個元素及其 API。 您可能還聽說過 canvas 元素,它是一個繪圖表面,包含一組豐富的 JavaScript API,這些 API 使您能夠動態創建和操作圖像及動畫。 IMG 對靜態圖形內容起到了哪些作用,canvas 就可能對可編寫腳本的動態內容起到哪些作用。

儘管 canvas 元素令人興奮,但它也面臨一些認知問題。 由於畫布功能強大,它通常通過複雜動畫或遊戲來展示,儘管這些動畫或遊戲確實可以傳達能夠實現的功能,但它們也可能讓您產生誤解,認為畫布使用起來非常複雜和困難,只應該嘗試將其用於複雜情況(如動畫或遊戲)。

在本月的文章中,我將撇開畫布華而不實的功能和複雜性,介紹它的一些簡單的基本用法,目標是將畫布定位為在 Web 應用程式中進行資料視覺化的一個功能強大的選項。 明確這一點後,我將重點討論您如何開始使用畫布,以及如何繪製簡單的線條、形狀和文本。 然後,我將討論您如何在形狀中使用漸變,以及如何向畫布中添加外部圖像。 最後,按照我在本系列中的一貫做法,我會簡要探討一下對較早版本流覽器的“填充代碼”畫布支援。

HTML5 Canvas 簡介

根據 W3C HTML5 規範 (w3.org/TR/html5/the-canvas-element.html),canvas 元素“為腳本提供取決於解析度的點陣圖畫布,該畫布可用於動態呈現圖形、遊戲圖形或其他可視圖像。”Canvas 實際是在兩個 W3C 規範中定義的。 第一個規範是 HTML5 核心規範的一部分,其中詳細定義了該元素本身。 此規範介紹如何使用 canvas 元素、如何獲取其繪圖上下文、用於匯出畫布內容的 API 以及流覽器供應商的安全注意事項。 第二個規範是 HTML Canvas 2D Context (w3.org/TR/2dcontext),我稍後再介紹該規範。

開始使用畫布非常簡單,只需在 HTML5 標記中添加 <canvas> 元素即可,如下所示:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <title>My Canvas Demo </title>               
        <link rel="stylesheet" href="style.css" />
      </head>
      <body>
        <canvas id="chart" width="600" height="450"></canvas>       
      </body>
    </html>

儘管現在我已經在 DOM 中包含 canvas 元素,但在頁面上放置此標記不會產生任何效果,因為在您添加內容之前,canvas 元素中沒有任何內容。 這就是繪圖上下文應運而生的原因。 為了說明我的空白畫布所在的位置,我可以使用 CSS 設置其樣式,因此我將在空白元素周圍添加一條藍色虛線。

    canvas {
        border-width: 5px;
        border-style: dashed;
        border-color: rgba(20, 126, 239, 0.50)
    }

在 Internet Explorer 9+、Chrome、Firefox、Opera 或 Safari 中打開我的頁面時的結果如圖 1 所示。

A Blank, Styled Canvas Element
圖 1 已設置樣式的空白 Canvas 元素

使用畫布時,您將在 JavaScript 中執行大多數工作,可通過 JavaScript 利用畫布繪圖上下文公開的 API 來操作圖面的每個圖元。 要獲取畫布繪圖上下文,您需要從 DOM 獲得您的 canvas 元素,然後調用該元素的 getContext 方法。

var _canvas = document.getElementById('chart');
var _ctx = _canvas.getContext("2d");

GetContext 返回一個物件,其中包含可用於在相關畫布上繪圖的 API。 該方法的第一個參數(在本例中為“2d”)指定我們要用於畫布的繪圖 API。 “2d” refers to the HTML Canvas 2D Context I mentioned earlier. 您可能已經猜到,2D 表示這是一個二維繪圖上下文。 到撰寫本文時為止,2D Context 是唯一受到廣泛支援的繪圖上下文,我們將在本文中使用該上下文。 圍繞 3D 繪圖上下文的工作和試驗正在進行當中,因此將來畫布應該能夠為我們的應用程式提供更多功能。

繪製線條、形狀和文本

現在頁面上已經有了 canvas 元素,並且我們已經在 JavaScript 中獲取了其繪圖上下文,我們可以開始添加內容了。 因為我想重點介紹資料視覺化,所以我將使用畫布繪製一個橫條圖來表示一個虛構的體育用品商店當月的銷售資料。 本練習將需要為軸繪製軸線;為條繪製形狀和填充;以及為每個軸和條上的標籤繪製文本。

讓我們從 x 和 y 軸的軸線開始。 在畫布上下文中繪製直線(或路徑)分兩個步驟進行。 首先,使用一系列 lineTo(x, y) 和 moveTo(x, y) 調用在圖面上“描摹”直線。 每種方法都會獲取畫布物件(從左上角開始)上的 x 座標和 y 座標(而非螢幕本身上的座標)以便在執行操作時使用。 moveTo 方法將移至所指定的座標,lineTo 將在當前座標與您指定的座標之間描摹一條直線。 例如,以下代碼將在圖面上描摹我們的 y 軸:

// Draw y axis.
_ctx.moveTo(110, 5);
_ctx.lineTo(110, 375);

如果您向腳本中添加此代碼並在流覽器中運行它,您會注意到什麼也不會發生。 因為這第一步只是一個描摹步驟,並未在螢幕上繪製任何內容。 描摹僅指示流覽器記錄將在以後某個時刻刷新到螢幕上的路徑操作。 當我準備好在螢幕上繪製路徑時,我可以選擇設置我的上下文的 strokeStyle 屬性,然後調用 stroke 方法,該方法將填充不可見線條。 结果如图 2 所示。

 

// Define Style and stroke lines.
_ctx.strokeStyle = "#000";
_ctx.stroke();

A Single Line on the Canvas
圖 2 畫布上的一條直線

因為定義線條(lineTo、moveTo)和繪製線條 (stroke) 是相對獨立的,所以您實際可以批量處理一系列 lineTo 和 moveTo 操作,然後將它們同時輸出到螢幕上。 我將通過此方法繪製 x 軸和 y 軸並完成在每個軸的一端繪製箭頭的操作。 用於繪製軸的完整函數如圖 3 所示,結果如圖 4 所示。

圖 3 drawAxes 函數

function drawAxes(baseX, baseY, chartWidth) {
   var leftY, rightX;
   leftY = 5;
   rightX = baseX + chartWidth;
   // Draw y axis.
_ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX, baseY);
   // Draw arrow for y axis.
_ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX + 5, leftY + 5);
   _ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX - 5, leftY + 5);
   // Draw x axis.
_ctx.moveTo(baseX, baseY);
   _ctx.lineTo(rightX, baseY);
   // Draw arrow for x axis.
_ctx.moveTo(rightX, baseY);
   _ctx.lineTo(rightX - 5, baseY + 5);
   _ctx.moveTo(rightX, baseY);
   _ctx.lineTo(rightX - 5, baseY - 5);
   // Define style and stroke lines.
_ctx.strokeStyle = "#000";
   _ctx.stroke();
}

Completed X- and Y-Axes
圖 4 完成的 X 軸和 Y 軸

繪製完兩個軸之後,我們可能要為其添加標籤使其更加有用。 2D 畫布上下文指定用於向 canvas 元素添加文本的 API,因此您不需要擺弄雜亂的操作,例如使文本在 canvas 元素上浮動。 儘管如此,畫布文本並不提供方框模型,也不接受為頁面範圍的文本定義的 CSS 樣式,等等。 API 提供與 CSS 字體規則工作方式相同的字體屬性 (Attribute)—以及 textAlign 和 textBaseline 屬性 (Property),以便您能夠對相對於所提供的座標的位置進行某種控制—但除此之外,在畫布上繪製文本實際上就是在畫布上為您所提供的文本選取一個確切的點。

X 軸表示我們虛構的體育用品商店中的商品,所以我們應相應地為該軸添加標籤:

var height, widthOffset;
height = _ctx.canvas.height;
widthOffset = _ctx.canvas.width/2;
_ctx.font = "bold 18px sans-serif";
_ctx.fillText("Product", widthOffset, height - 20);

在此程式碼片段中,我將設置可選字體屬性並提供一個要繪製在圖面上的字串,以及要用作字串起始位置的 x 座標和 y 座標。 在此例中,我將在畫布中間、底部上方 20 個圖元的位置繪製“Product”一詞,從而為我的橫條圖上的每種商品的標籤留出空間。 我將對 y 軸標籤(包含每種商品的銷售資料)執行類似操作。 结果如图 5 所示。

Canvas with Text
圖 5 包含文本的畫布

現在我們已經有了圖表框架,可以添加條了。 讓我們為橫條圖創建一些虛擬銷售資料,我將其定義為物件文字的 JavaScript 陣列。

var salesData = [{
   category: "Basketballs",
   sales: 150
}, {
   category: "Baseballs",
   sales: 125
}, {
   category: "Footballs",
   sales: 300
}];

有了這些資料後,我們可以使用 fillRect 和 fillStyle 在圖表上繪製條。

fillRect(x, y, width, height) 將使用您指定的寬度和高度,在畫布上的 x 和 y 座標位置繪製一個矩形。 務必注意,除非您指定負寬度和高度值(在這種情況下,填充將按相反方向向外伸出),否則 fillRect 繪製的形狀將從左上角開始向外伸出。 對於繪製圖表之類的繪圖任務,這意味著我們將自上而下(而非自下而上)繪製條。

要繪製條,我們可以遍歷銷售資料陣列,並使用相應的座標調用 fillRect:

var i, length, category, sales;
var barWidth = 80;
var xPos = baseX + 30;
var baseY = 375;       
for (i = 0, length = salesData.length; i < length; i++) {
   category = salesData[i].category;
   sales = salesData[i].sales;
   _ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);
   xPos += 125;
}

在此代碼中,每個條的寬度是標準的,而高度是從陣列中每種商品的銷售屬性中獲取的。 圖 6 顯示了此代碼的結果。

Rectangles as Bar Chart Data
圖 6 用作橫條圖資料的矩形

現在,我們有了一個圖表,它在技術上是準確的,但這些純黑色的條還有待改進。 我們可以通過填充某種顏色使其更加清晰明瞭,然後添加漸變效果。

使用顏色和漸變

調用繪圖上下文的 fillRect 方法時,上下文將使用當前 fillStyle 屬性在繪製矩形時設置其樣式。 預設樣式為純黑色,因此我們的條看上去才如圖 6 所示。 fillStyle 接受指定的十六進位和 RGB 顏色,因此我們可以添加一些功能以便在繪製每個條之前設置其樣式:

// Colors can be named hex or RGB.
colors = ["orange", "#0092bf", "rgba(240, 101, 41, 0.90)"];       
...
_ctx.fillStyle = colors[i % length];
_ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);

首先,我們需要創建一個顏色陣列。 然後,在遍歷每種商品時,我們將使用其中一種顏色作為該元素的填充樣式。 结果如图 7 所示。

Using fillStyle to Style Shapes
圖 7 使用 fillStyle 設置形狀的樣式

效果有所改善,但 fillStyle 非常靈活,允許您使用線性和徑向漸變而非只使用純色。 2D 繪圖上下文指定兩個漸變函數:createLinerGradient 和 createRadialGradient,二者均可通過平滑顏色過渡來改善您的形狀的樣式。

對於此示例,我將定義一個 createGradient 函數,它將接受漸變的 x 和 y 座標、寬度和要使用的原色:

function createGradient(x, y, width, color) {
   var gradient;
   gradient = _ctx.createLinearGradient(x, y, x+width, y);
   gradient.addColorStop(0, color);
   gradient.addColorStop(1, "#efe3e3");
   return gradient;
}

使用我的起點和終點座標調用 createLinearGradient 後,我將向繪圖上下文返回的漸變物件中添加兩個顏色中斷點。 addColorStop 方法將沿漸變添加顏色過渡;該方法可以調用任意多次,但第一個參數值需介於 0 和 1 之間。 在設置我的漸變後,我將從函數返回它。

漸變物件隨後可在我的上下文中設置為 fillStyle 屬性,來代替我在上一示例中指定的十六進位和 RGB 字串。 我將使用這些相同的顏色作為起點,然後使其逐漸變為淺灰色。

colors = ["orange", "#0092bf", "rgba(240, 101, 41, 0.90)"];
_ctx.fillStyle = createGradient(xPos, baseY - sales-1, barWidth, colors[i % length]);
_ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);

漸變方法的結果如圖 8 所示。

Using Gradients in a Canvas
圖 8 在畫布中使用漸變

處理圖像

此時,我們得到了一個非常漂亮的圖表,我們已經能夠使用數十行 JavaScript 在流覽器中呈現該圖表。 我可以就此打住,但我仍想介紹一個與處理圖像相關的基本畫布 API。 借助畫布,您不僅可以將靜態圖像替換為基於腳本的互動式內容,而且可以使用靜態圖像增強您的畫布的視覺化效果。

對於本演示,我想將圖像用作橫條圖上的條。 不只是圖像,還包括專案本身的圖片。 抱著這一目標,我的網站中有一個資料夾包含每種商品的 JPG 圖像—在本例中為 basketballs.jpg、baseballs.jpg 和 footballs.jpg。 我只需要正確放置每個圖像並設置其大小。

2D 繪圖上下文定義一個帶有三個重載的 drawImage 方法,接受三個、五個或九個參數。 第一個參數始終是要繪製的 DOM 元素圖像。 DrawImage 的最簡單版本還接受畫布上的 x 和 y 座標,並在該位置按原樣繪製圖像。 您也可以將寬度和高度值作為最後兩個參數提供,這會將圖像縮放為該大小,然後再在圖面上繪製它。 最後,drawImage 的最複雜用法允許您將圖像裁剪為定義的矩形,將其縮放為一組給定的維度,最終再在畫布上指定的座標位置繪製它。

由於我的源圖像是在我的網站上的其他位置使用的大比例圖像,因此我將採用後一種方法。 在此例中,我不在遍歷 salesData 陣列時為每個專案調用 fillRect,而是創建一個 Image DOM 元素,將其來源設置為我的商品圖像之一,並將該圖像裁剪後的版本呈現在我的圖表上,如圖 9 所示。

圖 9 在畫布上繪製圖像

// Set outside of my loop.
xPos = 110 + 30;     
// Create an image DOM element.
img = new Image();
img.onload = (function(height, base, currentImage, currentCategory) {
  return function() {
    var yPos, barWidth, xPos;
    barWidth = 80;
      yPos = base - height - 1;
    _ctx.drawImage(currentImage, 30, 30, barWidth, height, xPos, yPos,
      barWidth, height);
      xPos += 125;           
  }
})(salesData[i].sales, baseY, img, salesData[i].category);
img.src = "images/" + salesData[i].category + ".jpg";

因為我將動態創建這些圖像,而不是在設計時手動將其添加到我的標記,所以我不應認為我可以設置圖像來源,然後立即將該圖像繪製到我的畫布上。 為確保我僅在每個圖像完全載入後才繪製該圖像,我會將我的繪圖邏輯添加到圖像的 onload 事件中,然後將該代碼打包到自調用函數中,該函數會創建一個閉包,其中包含指向正確的商品類別的變數、銷售變數和定位變數。 結果如圖 10 所示。

Using Images on a Canvas
圖 10 在畫布上使用圖像

使用畫布填充代碼

您可能已經知道,Internet Explorer 9 之前的版本以及其他流覽器的較早版本不支援 canvas 元素。 通過在 Internet Explorer 中打開演示專案並按 F12 打開開發人員工具,您可以親自驗證這一點。 通過 F12 工具,您可以將“流覽器模式”更改為 Internet Explorer 8 或 Internet Explorer 7 並刷新頁面。 您可能會看到一個包含以下消息的 JavaScript 異常:“物件不支援 getContext 屬性或方法”。2D 繪圖上下文不可用,canvas 元素本身也不可用。 還務必要知道,即使在 Internet Explorer 9 中,除非您指定 DOCTYPE,否則畫布也不可用。 正如我在本系列第一篇文章 (msdn.microsoft.com/magazine/hh335062) 中提到的,最好在所有 HTML 頁面的頂部使用 <!DOCTYPE html> 以確保可使用流覽器中的最新功能。

對於其流覽器不支援畫布的使用者來說,可以採取的最簡單的措施是使用後備元素,例如圖像或文本。 例如,要向使用者顯示後備圖像,您可以使用類似如下的標記:

    <canvas id=”chart”>
      <img id=”chartIMG” src=”images/fallback.png”/>
    </canvas>

您放在 <canvas> 標記內的任何內容都只在使用者的流覽器不支援畫布時才會呈現。 這意味著,您可以在畫布內放置圖像或文本,作為使用者的簡單的零檢查後備圖像。

如果您要進一步探索後備支援,好消息是,存在各種針對畫布的填充代碼解決方案,因此,只要您仔細審查潛在解決方案並瞭解給定填充代碼的限制,就可以放心地將其與較早流覽器結合使用。 如我在本系列的其他文章中所述,您查找任何 HTML5 技術的填充代碼的切入點應該是 GitHub 上 Modernizr wiki 中的“HTML5 跨流覽器填充代碼”頁 (bit.ly/nZW85d)。 到撰寫本文時為止,市場上已經有多個畫布填充代碼,包括兩個回退到 Flash 和 Silverlight 的填充代碼。

在本文的可下載演示專案中,我使用 explorercanvas (code.google.com/p/explorercanvas) 以及 canvas-text (code.google.com/p/canvas-text),前者使用 Internet Explorer 支援的向量標記語言 (VML) 創建近似的畫布功能,後者為在較早流覽器中呈現文本提供額外支援。

如前面的文章所示,您可以使用 Modernizr 通過調用 Modernizr.canvas 對畫布(和 canvastext)支援進行功能檢測,然後使用 Modernizr.load 在需要時非同步載入 explorercanvas。 有關詳細資訊,請參閱 modernizr.com

如果您不想使用 Modenrizr,則可以使用另一種方法按條件為較早版本的 IE 添加 explorercanvas:條件注釋:

    <!--[if lt IE 9]>
      <script src="js/excanvas.js"></script>
      <script src="js/canvas.text.js"></script>
    <![endif]-->

當 Internet Explorer 8 或較早版本遇到按此方式設置格式的注釋時,它們會將該代碼塊作為 if 語句執行,並包括 explorercanvas 和 canvas-text 指令檔。 其他流覽器(包括 Internet Explorer 10)會將整個代碼塊視為注釋並完全忽略它。

在評估您的應用程式的潛在填充代碼時,請務必確定給定填充代碼支援多少個 2D 繪圖上下文。 只有極少 2D 繪圖上下文完全支援每種用法,但幾乎所有上下文都可以處理我們在本文中討論的基本情形。

雖然在這裡我無法面面俱到,但您還可以對畫布執行更多操作,從回應 Click(和其他)事件和更改畫布資料,到為繪圖圖面設置動畫、逐圖元呈現和操作圖像、保存狀態以及將整個圖面匯出為其自己的圖像,均包括在內。 事實上,市場上已經有介紹畫布的完整書籍。 您不必是遊戲開發人員即可體驗畫布的強大功能,我希望通過在本文仲介紹其基本功能,能夠向您證實這一點。 建議您親自閱讀這些規範,全身心投入到這一振奮人心的新圖形技術的研究當中。

如果您要查找有關 Internet Explorer 9 中的畫布支援的詳細資訊,請連線查看 IE9 開發人員指南 (msdn.microsoft.com/ie/ff468705)。 另外,請務必查看 IE Test Drive 網站 (bit.ly/9v2zv5) 上提供的 Canvas Pad 演示。 有關畫布的其他一些跨流覽器填充代碼清單,請查看完整填充代碼清單,網址為 bit.ly/eBMoLW

最後,本文中的所有演示(可線上獲取)都是使用 Microsoft 免費提供的輕型 Web 開發工具 WebMatrix 構建的。 您可以訪問 aka.ms/webm,親自試用 WebMatrix。

Brandon Satrom 是 Microsoft 在德克薩斯州奧斯丁市以外的開發推廣人員。他的博客網址是 userinexperience.com,您可以通過 Twitter 位址 twitter.com/BrandonSatrom 關注他。

衷心感謝以下技術專家對本文進行了審閱:Jatinder MannClark Sell