繪製圖形

瀏覽器包含一些非常強大的圖形程式設計工具,從可縮放向量圖形(SVG)語言,到在 HTML <canvas> 元素上繪圖的 API(參見Canvas APIWebGL)。本文將介紹 Canvas 及其更多學習資源。

預備知識 熟悉 HTMLCSSJavaScript,尤其是 JavaScript 物件基礎知識以及 DOM 指令碼網路請求等核心 API 知識。
學習成果
  • 本課程涵蓋的 API 所實現的理念和用例。
  • <canvas> 及其相關 API 的基本語法和用法。
  • 使用計時器和 requestAnimationFrame() 設定動畫迴圈。

Web 上的圖形

Web 最初只是文字,這非常無聊,因此引入了影像——首先透過 <img> 元素,後來透過 background-image 等 CSS 屬性和 SVG

然而,這仍然不夠。雖然你可以使用 CSSJavaScript 來動畫(以及以其他方式操作)SVG 向量影像——因為它們是透過標記表示的——但仍然無法對點陣圖影像進行同樣的操作,而且可用的工具也相當有限。Web 仍然無法有效地建立動畫、遊戲、3D 場景以及通常由 C++ 或 Java 等低階語言處理的其他需求。

當瀏覽器開始支援 <canvas> 元素和相關的 Canvas API 時,情況開始好轉(2004 年)。正如你將在下面看到的,Canvas 提供了一些有用的工具來建立 2D 動畫、遊戲、資料視覺化和其他型別的應用程式,特別是當它與 Web 平臺提供的其他一些 API 結合使用時,但可能難以或無法實現可訪問性。

下面的示例展示了一個簡單的基於 2D Canvas 的彈跳球動畫,我們最初在 JavaScript 物件介紹模組中遇到過。

大約在 2006-2007 年,Mozilla 開始研究實驗性的 3D Canvas 實現。這最終成為了 WebGL,它在瀏覽器供應商中獲得了關注,並於 2009-2010 年左右標準化。WebGL 允許你在 Web 瀏覽器中建立真實的 3D 圖形。

本文將主要關注 2D Canvas,因為原始 WebGL 程式碼非常複雜。不過,我們將展示如何使用 WebGL 庫更輕鬆地建立 3D 場景,你可以在其他地方找到涵蓋原始 WebGL 的教程——參見WebGL 入門

<canvas> 入門

如果你想在網頁上建立 2D 3D 場景,你需要從 HTML <canvas> 元素開始。此元素用於定義頁面上將繪製圖像的區域。這就像在頁面中包含元素一樣簡單。

html
<canvas width="320" height="240"></canvas>

這將建立一個大小為 320 x 240 畫素的 Canvas。

你應該在 <canvas> 標籤內放置一些備用內容。這應該向不支援 Canvas 的瀏覽器使用者或螢幕閱讀器使用者描述 Canvas 內容。

html
<canvas width="320" height="240">
  <p>Description of the canvas for those unable to view it.</p>
</canvas>

備用內容應提供 Canvas 內容的有用替代內容。例如,如果你正在渲染不斷更新的股票價格圖表,備用內容可以是最新股票圖表的靜態影像,並帶有文字形式的 alt 文字說明價格或指向單個股票頁面的連結列表。

注意:螢幕閱讀器無法訪問 Canvas 內容。在 Canvas 元素本身上直接將描述性文字作為 aria-label 屬性的值包含,或者在 <canvas> 開閉標籤內包含備用內容。Canvas 內容不是 DOM 的一部分,但巢狀的備用內容是。

建立和調整我們的 Canvas

讓我們從建立自己的 Canvas 模板開始,以便進行未來的實驗。

  1. 首先,在你的本地硬碟上建立一個名為 canvas-template 的目錄。

  2. 在該目錄中建立一個名為 index.html 的新檔案,並儲存以下內容。

    html
    <!doctype html>
    <html lang="en-US">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Canvas</title>
        <script src="script.js" defer></script>
        <link href="style.css" rel="stylesheet" />
      </head>
      <body>
        <canvas class="myCanvas">
          <p>Add suitable fallback here.</p>
        </canvas>
      </body>
    </html>
    
  3. 在該目錄中建立一個名為 style.css 的新檔案,並儲存以下 CSS 規則。

    css
    body {
      margin: 0;
      overflow: hidden;
    }
    
  4. 在該目錄中建立一個名為 script.js 的新檔案。暫時將其留空。

  5. 現在開啟 script.js 並新增以下 JavaScript 程式碼行。

    js
    const canvas = document.querySelector(".myCanvas");
    const width = (canvas.width = window.innerWidth);
    const height = (canvas.height = window.innerHeight);
    

    在這裡,我們將 Canvas 的引用儲存在 canvas 常量中。在第二行中,我們將新的常量 width 和 Canvas 的 width 屬性都設定為等於 Window.innerWidth(它為我們提供了視口寬度)。在第三行中,我們將新的常量 height 和 Canvas 的 height 屬性都設定為等於 Window.innerHeight(它為我們提供了視口高度)。所以現在我們有一個 Canvas 填滿了瀏覽器視窗的整個寬度和高度!

    你還會看到我們正在使用多個等號將賦值連結在一起——這在 JavaScript 中是允許的,如果你想讓多個變數都等於相同的值,這是一個很好的技巧。我們希望 Canvas 的寬度和高度在 width/height 變數中易於訪問,因為它們是以後有用的值(例如,如果你想在 Canvas 寬度的一半處繪製某些內容)。

注意:你通常應該使用 HTML 屬性或 DOM 屬性設定 Canvas 的大小,如上所述。你可以使用 CSS,但問題是 Canvas 渲染後才進行大小調整,就像任何其他影像一樣,Canvas 可能會出現畫素化/失真。

獲取 Canvas 上下文和最終設定

在我們認為 Canvas 模板完成之前,我們還需要做最後一件事。要繪製到 Canvas 上,我們需要獲得一個指向繪圖區域的特殊引用,稱為上下文。這是透過 HTMLCanvasElement.getContext() 方法完成的,該方法在基本用法中接受一個字串作為引數,表示你想要檢索的上下文型別。

在這種情況下,我們想要一個 2D Canvas,所以在 script.js 中的其他行下面新增以下 JavaScript 行。

js
const ctx = canvas.getContext("2d");

注意:你可以選擇的其他上下文值包括 WebGL 的 webgl、WebGPU 的 webgpu 等,但我們不需要在本文中使用這些。

所以就這樣——我們的 Canvas 現在已經準備好可以繪圖了!ctx 變數現在包含一個 CanvasRenderingContext2D 物件,Canvas 上的所有繪圖操作都將涉及操作此物件。

在繼續之前,讓我們做最後一件事。我們將 Canvas 背景顏色設定為黑色,讓你初步體驗 Canvas API。在 JavaScript 的底部新增以下幾行。

js
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);

在這裡,我們使用 Canvas 的 fillStyle 屬性設定填充顏色(它接受像 CSS 屬性一樣的顏色值),然後使用 fillRect 方法繪製一個覆蓋 Canvas 整個區域的矩形(前兩個引數是矩形左上角的座標;後兩個是你希望繪製的矩形的寬度和高度——我們告訴你那些 widthheight 變數會很有用)!

好的,我們的模板完成了,是時候繼續了。

2D Canvas 基礎

如上所述,所有繪圖操作都透過操作 CanvasRenderingContext2D 物件(在我們的例子中是 ctx)來完成。許多操作需要給定座標來精確地確定在哪裡繪製某些內容——Canvas 的左上角是點 (0, 0),水平 (x) 軸從左到右執行,垂直 (y) 軸從上到下執行。

Gridded graph paper with small squares covering its area with a steelblue square in the middle. The top left corner of the canvas is point (0, 0) of the canvas x-axis and y-axis. The horizontal (x) axis runs from left to right denoting the width, and the vertical (y) axis runs from top to bottom denotes the height. The top left corner of the blue square is labeled as being a distance of x units from the y-axis and y units from the x-axis.

繪製形狀通常使用矩形形狀基元,或者沿著特定路徑描繪線條然後填充形狀。下面我們將展示如何做這兩種。

簡單的矩形

讓我們從一些簡單的矩形開始。

  1. 首先,複製你新編碼的 Canvas 模板目錄。

  2. 將以下行新增到你的 JavaScript 檔案底部。

    js
    ctx.fillStyle = "red";
    ctx.fillRect(50, 50, 100, 150);
    

    如果你在瀏覽器中載入 HTML,你應該會看到 Canvas 上出現了一個紅色矩形。它的左上角距離 Canvas 邊緣的頂部和左側各 50 畫素(由前兩個引數定義),寬度為 100 畫素,高度為 150 畫素(由第三個和第四個引數定義)。

  3. 讓我們再新增一個矩形——這次是綠色的。在 JavaScript 的底部新增以下內容。

    js
    ctx.fillStyle = "green";
    ctx.fillRect(75, 75, 100, 100);
    

    儲存並重新整理,你將看到你的新矩形。這引出了一個重要觀點:繪製矩形、線條等圖形操作是按照它們發生的順序執行的。把它想象成粉刷牆壁,每一層油漆都會重疊,甚至可能隱藏下面的東西。你無法改變這一點,所以你必須仔細考慮繪製圖形的順序。

  4. 請注意,你可以透過指定半透明顏色來繪製半透明圖形,例如使用 rgb()。 “alpha 通道”定義了顏色的透明度。其值越高,它遮擋其後面內容的程度就越大。將以下內容新增到你的程式碼中。

    js
    ctx.fillStyle = "rgb(255 0 255 / 75%)";
    ctx.fillRect(25, 100, 175, 50);
    
  5. 現在嘗試繪製更多你自己的矩形;玩得開心!

描邊和線寬

到目前為止,我們已經討論了繪製填充矩形,但你也可以繪製只有輪廓的矩形(在圖形設計中稱為描邊)。要設定你想要的描邊顏色,你使用 strokeStyle 屬性;繪製描邊矩形使用 strokeRect

  1. 將以下內容新增到上一個示例中,同樣在之前的 JavaScript 行下方。

    js
    ctx.strokeStyle = "white";
    ctx.strokeRect(25, 25, 175, 200);
    
  2. 描邊的預設寬度為 1 畫素;你可以調整 lineWidth 屬性值來更改它(它接受一個表示描邊寬度的畫素數量的數字)。在之前的兩行之間新增以下行。

    js
    ctx.lineWidth = 5;
    

現在你應該看到你的白色輪廓變得更厚了!就目前而言就是這樣。此時你的示例應該看起來像這樣。

你可以按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。

繪製路徑

如果你想繪製比矩形更復雜的圖形,你需要繪製路徑。基本上,這涉及編寫程式碼來精確指定畫筆在畫布上應該沿著什麼路徑移動以描繪你想要繪製的形狀。Canvas 包含用於繪製直線、圓形、貝塞爾曲線等的函式。

首先,複製你的 Canvas 模板,以便在其中繪製新示例。

我們將在下面所有部分中使用一些常見方法和屬性。

  • beginPath() — 開始在畫筆當前在 Canvas 上的位置繪製路徑。在新 Canvas 上,畫筆從 (0, 0) 開始。
  • moveTo() — 將畫筆移動到 Canvas 上的不同點,而不記錄或描繪線條;畫筆“跳”到新位置。
  • fill() — 透過填充你迄今為止描繪的路徑來繪製填充形狀。
  • stroke() — 透過沿著你迄今為止繪製的路徑繪製描邊來繪製輪廓形狀。
  • 你還可以將 lineWidthfillStyle/strokeStyle 等功能用於路徑以及矩形。

典型的簡單路徑繪製操作將如下所示。

js
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(50, 50);
// draw your path
ctx.fill();

繪製線條

讓我們在 Canvas 上繪製一個等邊三角形。

  1. 首先,將以下輔助函式新增到程式碼底部。這會將度數轉換為弧度,這很有用,因為每當你需要在 JavaScript 中提供角度值時,它幾乎總是以弧度表示,但人類通常以度數思考。

    js
    function degToRad(degrees) {
      return (degrees * Math.PI) / 180;
    }
    
  2. 接下來,透過在之前新增的內容下方新增以下內容來開始你的路徑;在這裡我們為三角形設定顏色,開始繪製路徑,然後將畫筆移動到 (50, 50) 而不繪製任何內容。這就是我們開始繪製三角形的地方。

    js
    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.moveTo(50, 50);
    
  3. 現在在指令碼底部新增以下行。

    js
    ctx.lineTo(150, 50);
    const triHeight = 50 * Math.tan(degToRad(60));
    ctx.lineTo(100, 50 + triHeight);
    ctx.lineTo(50, 50);
    ctx.fill();
    

    讓我們依次來回顧一下:

    首先,我們畫一條線到 (150, 50)——我們的路徑現在沿著 x 軸向右移動了 100 畫素。

    其次,我們利用一些簡單的三角學計算等邊三角形的高度。基本上,我們正在繪製一個向下指向的三角形。等邊三角形的角度總是 60 度;為了計算高度,我們可以將其沿中間分成兩個直角三角形,每個三角形的角度分別為 90 度、60 度和 30 度。就邊而言:

    • 最長的一邊叫做斜邊
    • 60 度角旁邊的一邊叫做鄰邊——我們知道它是 50 畫素,因為它是我們剛剛繪製的線的一半。
    • 60 度角對邊的一邊叫做對邊,也就是我們想要計算的三角形的高度。

    An equilateral triangle pointing downwards with labeled angles and sides. The horizontal line at the top is labeled 'adjacent'. A perpendicular dotted line, from the middle of the adjacent line, labeled 'opposite', splits the triangle creating two equal right triangles. The right side of the triangle is labeled the hypotenuse, as it is the hypotenuse of the right triangle formed by the line labeled 'opposite'. while all three-sided of the triangle are of equal length, the hypotenuse is the longest side of the right triangle.

    一個基本的三角公式表明,鄰邊的長度乘以角度的正切值等於對邊,因此我們得到 50 * Math.tan(degToRad(60))。我們使用 degToRad() 函式將 60 度轉換為弧度,因為 Math.tan() 期望輸入值為弧度。

  4. 計算出高度後,我們畫另一條線到 (100, 50 + triHeight)。X 座標很簡單;它必須是我們設定的前兩個 X 值之間的一半。Y 值必須是 50 加上三角形的高度,因為我們知道三角形的頂部距離 Canvas 頂部 50 畫素。

  5. 下一行將一條線畫回三角形的起點。

  6. 最後,我們執行 ctx.fill() 來結束路徑並填充形狀。

繪製圓形

現在讓我們看看如何在 Canvas 中繪製圓形。這透過使用 arc() 方法實現,該方法在指定點繪製圓形的一部分或全部。

  1. 讓我們在 Canvas 中新增一個弧線——將以下內容新增到程式碼底部。

    js
    ctx.fillStyle = "blue";
    ctx.beginPath();
    ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);
    ctx.fill();
    

    arc() 接受六個引數。前兩個引數分別指定弧線中心的 X 和 Y 位置。第三個是圓的半徑,第四個和第五個是繪製圓的起始和結束角度(因此指定 0 度和 360 度會得到一個完整的圓),第六個引數定義圓是逆時針(anticlockwise)還是順時針(false 為順時針)繪製。

    注意:0 度是水平向右的。

  2. 讓我們嘗試新增另一個弧線。

    js
    ctx.fillStyle = "yellow";
    ctx.beginPath();
    ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true);
    ctx.lineTo(200, 106);
    ctx.fill();
    

    這裡的模式非常相似,但有兩個不同之處。

    • 我們將 arc() 的最後一個引數設定為 true,這意味著弧線是逆時針繪製的,這意味著儘管弧線指定為從 -45 度開始到 45 度結束,但我們繪製的是圍繞 270 度的弧線,而不是這部分內部。如果你將 true 更改為 false 然後重新執行程式碼,則只繪製圓的 90 度切片。
    • 在呼叫 fill() 之前,我們畫一條線到圓心。這意味著我們得到一個相當不錯的吃豆人風格的切口。如果你刪除這條線(試試看!)然後重新執行程式碼,你只會得到圓在弧線的起點和終點之間被切掉的一段邊緣。這說明了 Canvas 的另一個重要點——如果你嘗試填充一個不完整的路徑(即未閉合的路徑),瀏覽器將在起點和終點之間填充一條直線,然後將其填充。

就目前而言就是這樣;你的最終示例應該看起來像這樣。

你可以按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。

注意:要了解有關貝塞爾曲線等高階路徑繪製功能的更多資訊,請檢視我們的 使用 Canvas 繪製形狀教程。

文字

Canvas 還具有繪製文字的功能。讓我們簡要地探討一下這些功能。首先,複製你的 Canvas 模板,以便在其中繪製新示例。

文字使用兩種方法繪製:

在它們的基本用法中,這兩個方法都接受三個屬性:要繪製的文字字串,以及開始繪製文字的點的 X 和 Y 座標。這實際上是文字框(字面上是包圍你繪製的文字的框)的左下角,這可能會讓你感到困惑,因為其他繪圖操作通常從左上角開始——請記住這一點。

還有許多屬性可幫助控制文字渲染,例如 font,它允許你指定字體系列、大小等。其值採用與 CSS font 屬性相同的語法。

螢幕閱讀器無法訪問 Canvas 內容。繪製到 Canvas 上的文字無法透過 DOM 獲取,但必須使其可訪問。在此示例中,我們將文字作為 aria-label 的值包含。

嘗試將以下程式碼塊新增到 JavaScript 的底部。

js
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.font = "36px arial";
ctx.strokeText("Canvas text", 50, 50);

ctx.fillStyle = "red";
ctx.font = "48px georgia";
ctx.fillText("Canvas text", 50, 150);

canvas.setAttribute("aria-label", "Canvas text");

這裡我們繪製了兩行文字,一行是輪廓,另一行是描邊。示例應該如下所示。

按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。玩一玩,看看你能做出什麼!你可以在 繪製文字中找到有關 Canvas 文字可用選項的更多資訊。

在 Canvas 上繪製圖像

可以將外部影像渲染到 Canvas 上。這些可以是簡單的影像、影片幀或其他 Canvas 的內容。目前我們只討論在 Canvas 上使用一些簡單影像的情況。

  1. 和以前一樣,再次複製你的 Canvas 模板,以便在其中繪製新示例。

    影像使用 drawImage() 方法繪製到 Canvas 上。最簡單的版本接受三個引數——要渲染的影像的引用,以及影像左上角的 X 和 Y 座標。

  2. 讓我們首先獲取一個影像源以嵌入到我們的 Canvas 中。將以下行新增到你的 JavaScript 底部。

    js
    const image = new Image();
    image.src =
      "https://mdn.github.io/shared-assets/images/examples/fx-nightly-512.png";
    

    在這裡,我們使用 Image() 建構函式建立一個新的 HTMLImageElement 物件。返回的物件型別與你獲取對現有 <img> 元素的引用時返回的型別相同。然後我們將它的 src 屬性設定為等於我們的 Firefox 標誌影像。此時,瀏覽器開始載入影像。

  3. 我們現在可以嘗試使用 drawImage() 嵌入影像,但我們需要確保影像檔案已首先載入,否則程式碼將失敗。我們可以使用 load 事件來實現這一點,該事件只會在影像載入完成後觸發。將以下程式碼塊新增到前一個程式碼塊下方。

    js
    image.addEventListener("load", () => ctx.drawImage(image, 20, 20));
    

    如果你現在在瀏覽器中載入你的示例,你應該會看到影像嵌入在 Canvas 中,儘管尺寸相當大。

  4. 但還有更多!如果只顯示影像的一部分,或者調整它的大小怎麼辦?我們可以使用 drawImage() 的更復雜版本來完成這兩項任務。像這樣更新你的 ctx.drawImage() 行。

    js
    ctx.drawImage(image, 0, 0, 512, 512, 50, 40, 185, 185);
    
    • 第一個引數是影像引用,和以前一樣。
    • 引數 2 和 3 定義了你想從載入影像中剪切出來的區域的左上角座標,相對於影像本身的左上角。第一個引數左側或第二個引數上方的任何內容都不會被繪製。
    • 引數 4 和 5 定義了我們想從載入的原始影像中剪切出來的區域的寬度和高度。
    • 引數 6 和 7 定義了你想要在 Canvas 上繪製圖像剪下部分左上角的座標,相對於 Canvas 的左上角。
    • 引數 8 和 9 定義了影像剪下區域的繪製寬度和高度。在這種情況下,我們指定了與原始切片相同的尺寸,但你可以透過指定不同的值來調整大小。
  5. 當影像有意義地更新時,描述也必須更新。

    js
    canvas.setAttribute("aria-label", "Firefox Logo");
    

最終的示例應該如下所示。

按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。

迴圈與動畫

到目前為止,我們已經介紹了 2D Canvas 的一些非常基本的用法,但如果你不以某種方式更新或動畫它,你真的無法體驗到 Canvas 的全部功能。畢竟,Canvas 確實提供了可指令碼化的影像!如果你不打算更改任何內容,那麼你不如只使用靜態影像,省去所有工作。

建立一個迴圈

在 Canvas 中使用迴圈非常有趣——你可以在 for(或其他型別)迴圈中執行 Canvas 命令,就像任何其他 JavaScript 程式碼一樣。

讓我們構建一個示例。

  1. 再複製一份你的 Canvas 模板。

  2. 將以下行新增到你的 JavaScript 底部。這包含一個新方法,translate(),它會移動 Canvas 的原點。

    js
    ctx.translate(width / 2, height / 2);
    

    這將使座標原點 (0, 0) 移動到 Canvas 的中心,而不是在左上角。這在許多情況下非常有用,例如我們希望設計相對於 Canvas 中心繪製的情況。

  3. 現在將以下程式碼新增到 JavaScript 底部。

    js
    function degToRad(degrees) {
      return (degrees * Math.PI) / 180;
    }
    
    function rand(min, max) {
      return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    
    let length = 250;
    let moveOffset = 20;
    

    這裡我們實現了在上面三角形示例中看到的相同 degToRad() 函式,一個返回給定上下限之間隨機數的 rand() 函式,以及 lengthmoveOffset 變數(我們稍後會詳細瞭解)。

  4. 這裡的想法是,我們將在 for 迴圈中在 Canvas 上繪製一些東西,並每次對其進行迭代,以便我們可以建立一些有趣的東西。將以下程式碼新增到你的 for 迴圈中。

    js
    for (let i = 0; i < length; i++) {
      ctx.fillStyle = `rgb(${255 - length} 0 ${255 - length} / 90%)`;
      ctx.beginPath();
      ctx.moveTo(moveOffset, moveOffset);
      ctx.lineTo(moveOffset + length, moveOffset);
      const triHeight = (length / 2) * Math.tan(degToRad(60));
      ctx.lineTo(moveOffset + length / 2, moveOffset + triHeight);
      ctx.lineTo(moveOffset, moveOffset);
      ctx.fill();
    
      length--;
      moveOffset += 0.7;
      ctx.rotate(degToRad(5));
    }
    

    因此,在每次迭代中,我們:

    • fillStyle 設定為稍微透明的紫色陰影,每次根據 length 的值變化。正如你稍後將看到的,每次迴圈執行時 length 都會變小,因此這裡的效果是每次繪製的三角形顏色都會變亮。
    • 開始路徑。
    • 將筆移動到座標 (moveOffset, moveOffset);此變數定義了每次繪製新三角形時我們要移動的距離。
    • 畫一條線到座標 (moveOffset+length, moveOffset)。這將繪製一條與 X 軸平行的長度為 length 的線。
    • 像以前一樣計算三角形的高度。
    • 畫一條線到三角形的向下指向的角,然後畫一條線回到三角形的起點。
    • 呼叫 fill() 來填充三角形。
    • 更新描述三角形序列的變數,以便我們準備繪製下一個三角形。我們將 length 值減 1,因此三角形每次都會變小;將 moveOffset 增加少量,使每個連續的三角形稍微遠一點,並使用另一個新函式 rotate(),它允許我們旋轉整個 Canvas!我們在繪製下一個三角形之前將其旋轉 5 度。

就這樣!最終的例子應該像這樣。

按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。我們鼓勵你玩轉示例並使其成為你自己的作品!例如:

  • 繪製矩形或弧線而不是三角形,甚至嵌入影像。
  • 玩轉 lengthmoveOffset 值。
  • 使用我們上面包含但未使用的 rand() 函式引入一些隨機數。

動畫

我們上面構建的迴圈示例很有趣,但對於任何嚴肅的 Canvas 應用程式(例如遊戲和即時視覺化),你確實需要一個持續執行的迴圈。如果你將 Canvas 想象成一部電影,你確實希望顯示在每一幀都更新以顯示更新的檢視,理想的重新整理率為每秒 60 幀,這樣運動在人眼看來會流暢。

有幾個 JavaScript 函式可以讓你每秒重複執行函式多次,這裡最適合我們目的是 window.requestAnimationFrame()。它接受一個引數——你希望在每一幀執行的函式的名稱。瀏覽器下次準備更新螢幕時,你的函式將被呼叫。如果該函式繪製了動畫的新更新,然後在函式結束前再次呼叫 requestAnimationFrame(),動畫迴圈將繼續執行。當你停止呼叫 requestAnimationFrame(),或者在呼叫 requestAnimationFrame() 之後但在幀被呼叫之前呼叫 window.cancelAnimationFrame() 時,迴圈結束。

注意:當你完成動畫使用後,從主程式碼中呼叫 cancelAnimationFrame() 是一個好習慣,以確保沒有更新仍在等待執行。

瀏覽器處理複雜的細節,例如使動畫以一致的速度執行,並且不浪費資源來動畫化不可見的事物。

為了瞭解它的工作原理,讓我們快速再次看看我們的彈跳球示例。使一切運動的迴圈程式碼如下所示:

js
function loop() {
  ctx.fillStyle = "rgb(0 0 0 / 25%)";
  ctx.fillRect(0, 0, width, height);

  for (const ball of balls) {
    ball.draw();
    ball.update();
    ball.collisionDetect();
  }

  requestAnimationFrame(loop);
}

loop();

我們在程式碼底部執行 loop() 函式一次以啟動迴圈,繪製第一個動畫幀;然後 loop() 函式負責一遍又一遍地呼叫 requestAnimationFrame(loop) 來執行動畫的下一幀。

請注意,在每一幀中,我們都會完全清除 Canvas 並重新繪製所有內容。對於每一個存在的球,我們都會繪製它,更新其位置,並檢查它是否與其他球發生碰撞。一旦你將圖形繪製到 Canvas 上,就沒有辦法像處理 DOM 元素那樣單獨操作該圖形。你無法在 Canvas 上移動每個球,因為一旦它被繪製,它就是 Canvas 的一部分,而不是一個單獨的可訪問元素或物件。相反,你必須擦除並重繪,要麼透過擦除整個幀並重繪所有內容,要麼透過程式碼精確地知道需要擦除哪些部分,並僅擦除和重繪 Canvas 的最小區域。

最佳化圖形動畫是程式設計的一個完整專業領域,有許多巧妙的技術可用。不過,這些都超出了我們示例的需求!

通常,Canvas 動畫的過程涉及以下步驟:

  1. 清除 Canvas 內容(例如,使用 fillRect()clearRect())。
  2. 儲存狀態(如有必要)使用 save()——當你想要儲存你在 Canvas 上更新的設定然後繼續時,這是必需的,這對於更高階的應用程式很有用。
  3. 繪製你正在動畫的圖形。
  4. 使用 restore() 恢復你在第 2 步中儲存的設定。
  5. 呼叫 requestAnimationFrame() 來安排繪製動畫的下一幀。

注意:我們不會在這裡介紹 save()restore(),但在我們的變換教程(以及後續的教程)中對它們有很好的解釋。

行走物件動畫

現在讓我們建立自己的簡單動畫——我們將使用精靈表在螢幕上動畫化一個移動物件。

  1. 複製一份我們的 Canvas 模板,並在你的程式碼編輯器中開啟它。

  2. 更新備用 HTML 以反映影像。

    html
    <canvas class="myCanvas">
      <p>A cat walking.</p>
    </canvas>
    
  3. 這次,我們不會將背景顏色設定為黑色。因此,在獲取 ctx 變數後,將背景顏色塗成淺灰色。

    js
    ctx.fillStyle = "#e5e6e9";
    ctx.fillRect(0, 0, width, height);
    
  4. 在 JavaScript 的底部,新增以下行以再次將座標原點放置在 Canvas 的中間。

    js
    ctx.translate(width / 2, height / 2);
    
  5. 現在讓我們建立一個新的 HTMLImageElement 物件,將其 src 設定為我們要載入的影像,並新增一個 onload 事件處理程式,該處理程式將在影像載入時觸發 draw() 函式。

    js
    const image = new Image();
    image.src =
      "https://mdn.club.tw/shared-assets/images/examples/web-animations/cat_sprite.png";
    image.onload = draw;
    
  6. 現在我們將新增一些變數來跟蹤精靈在螢幕上繪製的位置,以及我們要顯示的精靈編號。

    js
    let spriteIndex = 0;
    let posX = 0;
    const spriteWidth = 300;
    const spriteHeight = 150;
    const totalSprites = 12;
    

    精靈圖片由 Rachel Nabors 提供並共享,用於其在 Web Animations API 上的文件工作。它看起來像這樣。

    A sprite sheet with three columns, each column containing a sequence of images of a black cat moving to the left at different paces. Each sprite is 300 pixels wide and 150 pixels high.

    它有三列。每列都是一個序列,表示貓以不同的速度移動(步行、小跑和疾馳)。每個序列包含 12 或 13 個精靈——每個精靈寬 300 畫素,高 150 畫素。我們將使用最左側的步行序列,其中包含 12 個精靈。為了清晰地顯示每個精靈,我們必須使用 drawImage() 從精靈表中剪切出單個精靈影像並僅顯示該部分,就像我們上面使用 Firefox 標誌所做的那樣。切片的 X 和 Y 座標必須分別是 spriteWidthspriteHeight 的倍數;因為我們使用的是最左側的序列,所以 X 座標始終為 0。切片大小始終為 spriteWidth x spriteHeight

  7. 現在,讓我們在程式碼底部插入一個空的 draw() 函式,準備好填充一些程式碼。

    js
    function draw() {}
    
  8. 本節的其餘程式碼位於 draw() 內部。首先,新增以下行,它會清除 Canvas 以準備繪製每一幀。請注意,我們必須將矩形的左上角指定為 -(width / 2), -(height / 2),因為我們之前將原點位置指定為 width/2, height/2

    js
    ctx.fillRect(-(width / 2), -(height / 2), width, height);
    
  9. 接下來,我們將使用 drawImage——9 引數版本繪製圖像。新增以下內容。

    js
    ctx.drawImage(
      image,
      0,
      spriteIndex * spriteHeight,
      spriteWidth,
      spriteHeight,
      0 + posX,
      -spriteHeight / 2,
      spriteWidth,
      spriteHeight,
    );
    

    正如你所看到的:

    • 我們將 image 指定為要嵌入的影像。
    • 引數 2 和 3 指定了要從源影像中剪切出的切片的左上角,其中 X 值為 0(用於最左側的列),Y 值迴圈為 spriteHeight 的倍數。你可以將 X 值替換為 spriteWidth2 * spriteWidth 以選擇其他列。
    • 引數 4 和 5 指定要剪切出的切片的大小——spriteWidthspriteHeight
    • 引數 6 和 7 指定了在 Canvas 上繪製切片框的左上角座標——X 位置是 0 + posX,這意味著我們可以透過改變 posX 值來改變繪製位置。Y 位置是 -spriteHeight / 2,這意味著影像將在 Canvas 上垂直居中。
    • 引數 8 和 9 指定影像在 Canvas 上的大小。我們只想保持其原始大小,因此我們將 spriteWidthspriteHeight 指定為寬度和高度。
  10. 現在,我們將在每次繪製後修改 spriteIndex 值——好吧,至少是其中的一些。將以下程式碼塊新增到 draw() 函式的底部。

    js
    if (posX % 11 === 0) {
      if (spriteIndex === totalSprites - 1) {
        spriteIndex = 0;
      } else {
        spriteIndex++;
      }
    }
    

    我們將整個程式碼塊包裝在 if (posX % 11 === 0) { } 中。我們使用模數 (%) 運算子(也稱為餘數運算子)來檢查 posX 值是否可以被 11 整除,沒有餘數。如果是,我們透過遞增 spriteIndex(在處理完最後一個精靈後將其重置為 0)來切換到下一個精靈。這實際上意味著我們只在每第 11 幀更新精靈,大約每秒 6 幀(requestAnimationFrame() 如果可能的話會以每秒高達 60 幀的速度呼叫我們)。我們故意降低幀速率,因為我們只有 12 個精靈可以使用,如果每秒 60 分之一秒顯示一個精靈,我們的物件將移動得太快!

    在外部塊內部,我們使用 if...else 語句檢查 spriteIndex 值是否在最後一個。如果已經顯示了最後一個精靈,我們將 spriteIndex 重置為 0;如果不是,我們只是將其遞增 1。

  11. 接下來,我們需要計算每幀如何改變 posX 值——在你的上一個程式碼塊下方新增以下程式碼塊。

    js
    if (posX < -width / 2 - spriteWidth) {
      const newStartPos = width / 2;
      posX = Math.ceil(newStartPos);
    } else {
      posX -= 2;
    }
    

    我們正在使用另一個 if...else 語句來檢視 posX 的值是否已小於 -width/2 - spriteWidth,這意味著我們的貓已經走出了螢幕的左邊緣。如果是這樣,我們計算一個位置,使貓剛好在螢幕右側的右側。

    如果我們的貓還沒有走出螢幕邊緣,我們將 posX 減 2。這將使其在下次繪製時稍微向左移動。

  12. 最後,我們需要透過在 draw() 函式底部呼叫 requestAnimationFrame() 來使動畫迴圈。

    js
    window.requestAnimationFrame(draw);
    

就這樣!最終的例子應該像這樣。

你可以按“播放”按鈕在 MDN Playground 中開啟示例並編輯原始碼。

一個簡單的繪圖應用程式

作為最後一個動畫示例,我們想向你展示一個非常簡單的繪圖應用程式,以說明動畫迴圈如何與使用者輸入(在這種情況下是滑鼠移動)結合使用。我們不會讓你一步步構建這個應用程式;我們只探索程式碼中最有趣的部分。

你可以在下面即時玩這個示例;你也可以點選“播放”按鈕在 MDN Playground 中開啟它,在那裡你可以編輯原始碼。

讓我們看看最有趣的部分。首先,我們用三個變數 curXcurYpressed 跟蹤滑鼠的 X 和 Y 座標以及是否正在點選。當滑鼠移動時,我們觸發一個設定為 onmousemove 事件處理程式的函式,該函式捕獲當前的 X 和 Y 值。我們還使用 onmousedownonmouseup 事件處理程式在按下滑鼠按鈕時將 pressed 的值更改為 true,並在釋放時再次更改為 false

js
let curX;
let curY;
let pressed = false;

// update mouse pointer coordinates
document.addEventListener("mousemove", (e) => {
  curX = e.pageX;
  curY = e.pageY;
});

canvas.addEventListener("mousedown", () => (pressed = true));

canvas.addEventListener("mouseup", () => (pressed = false));

當按下“清除畫布”按鈕時,我們執行一個簡單的函式,將整個畫布清除為黑色,就像我們之前看到的那樣。

js
clearBtn.addEventListener("click", () => {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, width, height);
});

這次的繪圖迴圈非常簡單——如果 pressedtrue,我們繪製一個圓,其填充樣式等於顏色選擇器中的值,半徑等於範圍輸入中設定的值。我們必須將圓繪製在我們測量它的位置上方 85 畫素處,因為垂直測量是從視口頂部進行的,但我們繪製圓是相對於畫布頂部,它從 85 畫素高的工具欄下方開始。如果我們只使用 curY 作為 y 座標繪製它,它將出現在滑鼠位置下方 85 畫素處。

js
function draw() {
  if (pressed) {
    ctx.fillStyle = colorPicker.value;
    ctx.beginPath();
    ctx.arc(
      curX,
      curY - 85,
      sizePicker.value,
      degToRad(0),
      degToRad(360),
      false,
    );
    ctx.fill();
  }

  requestAnimationFrame(draw);
}

draw();

所有 <input> 型別都得到了很好的支援。如果瀏覽器不支援某種輸入型別,它將回退到純文字欄位。

WebGL

現在是時候告別 2D,快速瀏覽一下 3D Canvas 了。3D Canvas 內容使用 WebGL API 指定,這是一個完全獨立的 API,與 2D Canvas API 不同,儘管它們都渲染到 <canvas> 元素上。

WebGL 基於 OpenGL(Open Graphics Library),允許你直接與計算機的 GPU 通訊。因此,編寫原始 WebGL 更接近 C++ 等低階語言,而不是常規 JavaScript;它非常複雜但功能強大。

使用庫

由於其複雜性,大多數人使用第三方 JavaScript 庫(例如 Three.jsPlayCanvasBabylon.js)編寫 3D 圖形程式碼。這些庫大多數以類似的方式工作,提供建立基本和自定義形狀、定位視角攝像機和燈光、用紋理覆蓋表面等功能。它們為你處理 WebGL,讓你在更高的層次上工作。

是的,使用其中一個意味著學習另一個新 API(在這種情況下是第三方 API),但它們比編寫原始 WebGL 程式碼簡單得多。

一個旋轉的立方體

讓我們看一個如何使用 WebGL 庫建立內容的示例。我們選擇 Three.js,因為它是最受歡迎的庫之一。在本教程中,我們將建立一個 3D 旋轉立方體。

  1. 首先,在你的本地硬碟上建立一個名為 webgl-cube 的新資料夾。

  2. 在其中,建立一個名為 index.html 的新檔案,並向其中新增以下內容。

    html
    <!doctype html>
    <html lang="en-US">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
        <title>Three.js basic cube example</title>
    
        <script src="https://cdn.jsdelivr.net/npm/three-js@79.0.0/three.min.js"></script>
        <script src="script.js" defer></script>
        <link href="style.css" rel="stylesheet" />
      </head>
    
      <body></body>
    </html>
    
  3. 接下來,建立另一個名為 script.js 的新檔案,同樣在同一個資料夾中。暫時將其留空。

  4. 現在建立另一個名為 style.css 的新檔案,同樣在同一個資料夾中,並向其中新增以下內容。

    css
    html,
    body {
      margin: 0;
    }
    
    body {
      overflow: hidden;
    }
    
  5. 我們已在頁面中包含了 three.js(這是我們 HTML 中的第一個 <script> 元素的作用),所以現在我們可以開始在 script.js 中編寫利用它的 JavaScript 程式碼了。讓我們從建立一個新場景開始——將以下內容新增到你的 script.js 檔案中。

    js
    const scene = new THREE.Scene();
    

    Scene() 建構函式建立一個新場景,它表示我們要顯示的整個 3D 世界。

  6. 接下來,我們需要一個攝像機才能看到場景。在 3D 影像術語中,攝像機表示觀察者在世界中的位置。要建立攝像機,接下來新增以下行。

    js
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000,
    );
    camera.position.z = 5;
    

    PerspectiveCamera() 建構函式接受四個引數:

    • 視野:攝像機前方可見區域的寬度,以度為單位。
    • 縱橫比:通常是場景寬度除以場景高度的比率。使用其他值會使場景變形(這可能正是你想要的,但通常不是)。
    • 近平面:物體距離攝像機有多近時停止渲染到螢幕。想想當你將指尖越來越靠近眼睛之間的空間時,最終你再也看不見它了。
    • 遠平面:物體距離攝像機有多遠時停止渲染。

    我們還將攝像機的位置設定為沿 Z 軸向外 5 個距離單位,這與 CSS 中一樣,是螢幕向您(觀看者)的方向。

  7. 第三個重要成分是渲染器。這是一個渲染給定場景的物件,透過給定攝像機檢視。我們現在將使用 WebGLRenderer() 建構函式建立一個,但我們稍後才會使用它。接下來新增以下幾行。

    js
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    

    第一行建立一個新的渲染器,第二行設定渲染器繪製攝像機檢視的大小,第三行將渲染器建立的 <canvas> 元素附加到文件的 <body> 中。現在渲染器繪製的任何內容都將顯示在我們的視窗中。

  8. 接下來,我們想建立將在 Canvas 上顯示的立方體。將以下程式碼塊新增到 JavaScript 的底部。

    js
    let cube;
    
    const loader = new THREE.TextureLoader();
    
    loader.load(
      "https://mdn.github.io/shared-assets/images/examples/learn/metal003.png",
      (texture) => {
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(2, 2);
    
        const geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
        const material = new THREE.MeshLambertMaterial({ map: texture });
        cube = new THREE.Mesh(geometry, material);
        scene.add(cube);
    
        draw();
      },
    );
    

    這裡還有一些需要理解的地方,讓我們分階段進行:

    • 我們首先建立一個 cube 全域性變數,以便我們可以從程式碼中的任何地方訪問我們的立方體。
    • 接下來,我們建立一個新的 TextureLoader 物件,然後在它上面呼叫 load()load() 在這種情況下接受兩個引數(儘管它可以接受更多):我們想要載入的紋理(一個 PNG),以及紋理載入完成後將執行的函式。
    • 在這個函式內部,我們使用 texture 物件的屬性來指定我們想要將影像以 2 x 2 的重複方式包裹在立方體的所有側面。接下來,我們建立一個新的 BoxGeometry 物件和一個新的 MeshLambertMaterial 物件,並將它們組合在一個 Mesh 中以建立我們的立方體。一個物件通常需要幾何體(它的形狀是什麼)和材質(它的表面看起來像什麼)。
    • 最後,我們將我們的立方體新增到場景中,然後呼叫我們的 draw() 函式來啟動動畫。
  9. 在我們定義 draw() 之前,我們將在場景中新增幾盞燈,以使事物活躍起來;接下來新增以下程式碼塊。

    js
    const light = new THREE.AmbientLight("white"); // soft white light
    scene.add(light);
    
    const spotLight = new THREE.SpotLight("white");
    spotLight.position.set(100, 1000, 1000);
    spotLight.castShadow = true;
    scene.add(spotLight);
    

    AmbientLight 物件是一種柔和的光線,可以稍微照亮整個場景,就像你在戶外時的太陽一樣。SpotLight 物件則是一種定向光束,更像手電筒(或者實際上是聚光燈)。

  10. 最後,讓我們將 draw() 函式新增到程式碼底部。

    js
    function draw() {
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    
      requestAnimationFrame(draw);
    }
    

    這相當直觀;在每一幀,我們都會稍微旋轉立方體的 X 軸和 Y 軸,然後渲染透過我們的相機看到的場景,最後呼叫 requestAnimationFrame() 來安排繪製下一幀。

成品應該看起來像:

注意:在我們的 GitHub 倉庫中,你還可以找到另一個有趣的 3D 立方體示例——Three.js 影片立方體也可線上檢視)。這使用 getUserMedia() 從計算機攝像頭獲取影片流並將其作為紋理投影到立方體的側面!

總結

至此,你應該對使用 Canvas 和 WebGL 進行圖形程式設計的基礎知識以及這些 API 的用途有了有用的瞭解,並且對在哪裡可以找到更多資訊也有了一個很好的概念。玩得開心!

另見

這裡我們只介紹了 Canvas 的真正基礎——還有很多東西要學習!下面的文章將帶你更進一步。

  • Canvas 教程——一個非常詳細的教程系列,解釋了你應該瞭解的關於 2D Canvas 的更多細節。必讀。
  • WebGL 教程——一個教授原始 WebGL 程式設計基礎的系列教程。
  • 使用 Three.js 構建基本演示——基本的 Three.js 教程。我們也有 PlayCanvasBabylon.js 的等效指南。
  • 遊戲開發——MDN 上的 Web 遊戲開發著陸頁。這裡有一些關於 2D 和 3D Canvas 的非常有用的教程和技術——請參閱“技術和教程”選單選項。

示例

  • Violent theremin——使用 Web Audio API 生成聲音,並使用 Canvas 生成漂亮的視覺效果。
  • Voice change-o-matic——使用 Canvas 視覺化 Web Audio API 的即時音訊資料。