使用影像

到目前為止,我們已經建立了自己的形狀並對其應用了樣式<canvas> 的一個更令人興奮的功能是能夠使用影像。這些影像可用於動態照片合成、圖表背景、遊戲中的精靈等。瀏覽器支援的任何格式的外部影像都可以使用,例如 PNG、GIF 或 JPEG。您甚至可以使用同一頁面上其他 canvas 元素生成的影像作為源!

將影像匯入 canvas 基本上是一個兩步過程

  1. 獲取對 HTMLImageElement 物件或另一個 canvas 元素作為源的引用。也可以透過提供 URL 來使用影像。
  2. 使用 drawImage() 函式在 canvas 上繪製圖像。

讓我們來看看如何做到這一點。

獲取要繪製的影像

Canvas API 可以使用以下任何資料型別作為影像源

HTMLImageElement

這些是使用 Image() 建構函式建立的影像,以及任何 <img> 元素。

SVGImageElement

這些是使用 <image> 元素嵌入的影像。

HTMLVideoElement

使用 HTML <video> 元素作為影像源可以捕獲影片的當前幀並將其用作影像。

HTMLCanvasElement

您可以使用另一個 <canvas> 元素作為影像源。

ImageBitmap

點陣圖影像,最終會被裁剪。這類影像用於從較大的影像中提取一部分影像,即一個精靈

OffscreenCanvas

一種特殊的 <canvas>,它不會顯示,並且在不顯示的情況下進行準備。使用此類影像源允許在不向使用者顯示內容組合的情況下切換到它。

VideoFrame

表示影片單個幀的影像。

有幾種方法可以獲取用於 canvas 的影像。

使用同一頁面上的影像

我們可以透過使用以下任何一種方法來獲取 canvas 同一頁面上影像的引用

  • document.images 集合
  • document.getElementsByTagName() 方法
  • 如果您知道要使用的特定影像的 ID,則可以使用 document.getElementById() 來檢索該特定影像。

使用其他域的影像

使用 <img> 元素的 crossorigin 屬性(由 HTMLImageElement.crossOrigin 屬性反映),您可以請求允許從另一個域載入影像以用於 drawImage() 呼叫。如果託管域允許跨域訪問影像,則可以在 canvas 中使用該影像而不會使其“汙染”;否則,使用該影像將汙染 canvas

使用其他 canvas 元素

與普通影像一樣,我們透過使用 document.getElementsByTagName()document.getElementById() 方法來訪問其他 canvas 元素。請確保在使用源 canvas 繪製了某些內容之後,再將其用於目標 canvas。

其中一個更實用的用途是使用第二個 canvas 元素作為較大 canvas 的縮圖檢視。

從頭建立影像

另一個選擇是在我們的指令碼中建立新的 HTMLImageElement 物件。為此,我們方便地使用了 Image() 建構函式。

js
const img = new Image(); // Create new img element
img.src = "myImage.png"; // Set source path

當此指令碼執行時,影像開始載入,但如果您嘗試在影像載入完成之前呼叫 drawImage(),它將什麼也不做。舊的瀏覽器甚至可能丟擲異常,因此您需要確保使用 load 事件,這樣就不會在影像準備好之前將其繪製到 canvas 上。

js
const ctx = document.getElementById("canvas").getContext("2d");
const img = new Image();

img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
});

img.src = "myImage.png";

如果您使用的是一個外部影像,這可能是一個不錯的方法,但一旦您想要使用許多影像或延遲載入資源,您可能需要在繪製到 canvas 之前等待所有檔案都可用。下面處理多個影像的示例使用了一個非同步函式和 Promise.all 來等待所有影像載入完畢,然後才呼叫 drawImage()

js
async function draw() {
  // Wait for all images to be loaded:
  await Promise.all(
    Array.from(document.images).map(
      (image) =>
        new Promise((resolve) => image.addEventListener("load", resolve)),
    ),
  );

  const ctx = document.getElementById("canvas").getContext("2d");
  // call drawImage() as usual
}
draw();

透過 data: URL 嵌入影像

包含影像的另一種可能方法是透過 data: URL。Data URL 允許您將影像完全定義為 Base64 編碼的字元字串,直接放在您的程式碼中。

js
const img = new Image(); // Create new img element
img.src =
  "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";

Data URL 的一個優點是生成的影像可以立即獲得,無需與伺服器進行另一輪通訊。另一個潛在的優點是,您還可以將所有的 CSSJavaScriptHTML 和影像封裝在一個檔案中,使其更易於移植到其他位置。

此方法的缺點是影像未被快取,並且對於較大的影像,編碼的 URL 可能會變得非常長。

使用影片幀

您還可以使用 <video> 元素呈現的影片的幀(即使影片不可見)。例如,如果您有一個 ID 為“myVideo”的 <video> 元素,您可以這樣做。

js
function getMyVideo() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  return document.getElementById("myVideo");
}

這會返回影片的 HTMLVideoElement 物件,正如前面所討論的,該物件可以用作 canvas 的影像源。

繪製圖像

一旦我們獲得了源影像物件的引用,我們就可以使用 drawImage() 方法將其渲染到 canvas 上。正如我們稍後將看到的,drawImage() 方法是過載的,並且有幾種變體。在其最基本的形式中,它看起來像這樣:

drawImage(image, x, y)

在座標 (x, y) 處繪製由 image 引數指定的影像。

注意: SVG 影像必須在根 <svg> 元素中指定寬度和高度。

示例:一個簡單的折線圖

在下面的示例中,我們將使用外部影像作為簡單折線圖的背景。使用背景可以使您的指令碼顯著變小,因為我們可以避免編寫生成背景的程式碼。在此示例中,我們只使用一個影像,因此我使用影像物件的 load 事件處理程式來執行繪圖語句。drawImage() 方法將背景放置在座標 (0, 0) 處,這是 canvas 的左上角。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");
  const img = new Image();
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    ctx.beginPath();
    ctx.moveTo(30, 96);
    ctx.lineTo(70, 66);
    ctx.lineTo(103, 76);
    ctx.lineTo(170, 15);
    ctx.stroke();
  };
  img.src = "backdrop.png";
}

draw();

生成的圖表如下所示:

縮放

drawImage() 方法的第二個變體添加了兩個新引數,允許我們在 canvas 上放置縮放後的影像。

drawImage(image, x, y, width, height)

這添加了 widthheight 引數,它們指定了將影像繪製到 canvas 上時要縮放到的尺寸。

示例:平鋪影像

在此示例中,我們將使用一張影像作為桌布,並將其在 canvas 上重複多次。這是透過迴圈並將縮放後的影像放置在不同位置來完成的。在下面的程式碼中,第一個 for 迴圈遍歷行。第二個 for 迴圈遍歷列。影像被縮放到其原始大小的三分之一,即 50x38 畫素。

注意: 影像在放大或縮小過多時可能會變得模糊或顆粒狀。如果您要在影像中包含需要保持清晰可讀的文字,則不建議進行縮放。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");
  const img = new Image();
  img.onload = () => {
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 3; j++) {
        ctx.drawImage(img, j * 50, i * 38, 50, 38);
      }
    }
  };
  img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
}

draw();

生成的 canvas 如下所示:

切片

drawImage() 方法的第三個也是最後一個變體,除了影像源之外,還有八個引數。它允許我們從源影像中剪切出一部分,然後將其縮放並繪製到我們的 canvas 上。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

給定一個 image,此函式將源影像中由以 (sx, sy) 為左上角、寬度和高度分別為 sWidthsHeight 的矩形指定的區域,將其繪製到 canvas 上,放置在 canvas 的 (dx, dy) 位置,並將其縮放到由 dWidthdHeight 指定的大小,同時保持其縱橫比

為了真正理解這一點,看看這張圖可能會有幫助:

The rectangular source image top left coordinates are sx and sy with a width and height of sWidth and sHeight respectively. The source image is translated to the destination canvas where the top-left corner coordinates are dx and dy, with a width and height of dWidth and dHeight respectively.

前四個引數定義了源影像上切片的**位置和大小**。後四個引數定義了在目標 canvas 上繪製圖像的**矩形**。

當您想要進行合成時,切片可能是一個有用的工具。您可以將所有元素放在單個影像檔案中,並使用此方法合成完整的繪圖。例如,如果您想製作圖表,您可以有一個包含所有必需文字的 PNG 影像檔案,並且根據您的資料,可以相當容易地更改圖表的比例。另一個優點是您不必單獨載入每個影像,這可以提高載入效能。

示例:為影像新增邊框

在此示例中,我們將使用與上一個示例中相同的犀牛,但我們將切出它的頭部並將其合成到相框中。相框影像是 24 位 PNG,包含陰影。因為 24 位 PNG 影像包含完整的 8 位 alpha 通道,與 GIF 和 8 位 PNG 影像不同,它可以放在任何背景上,而無需擔心背景色。

html
<canvas id="canvas" width="150" height="150"></canvas>
<div class="hidden">
  <img
    id="source"
    src="https://mdn.github.io/shared-assets/images/examples/rhino.jpg"
    width="300"
    height="227" />
  <img id="frame" src="canvas_picture_frame.png" width="132" height="150" />
</div>
js
async function draw() {
  // Wait for all images to be loaded.
  await Promise.all(
    Array.from(document.images).map(
      (image) =>
        new Promise((resolve) => image.addEventListener("load", resolve)),
    ),
  );

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

  // Draw slice
  ctx.drawImage(
    document.getElementById("source"),
    33,
    71,
    104,
    124,
    21,
    20,
    87,
    104,
  );

  // Draw frame
  ctx.drawImage(document.getElementById("frame"), 0, 0);
}

draw();

這次我們採取了不同的方法來載入影像。我們沒有透過建立新的 HTMLImageElement 物件來載入它們,而是將它們包含在 HTML 源的 <img> 標籤中,並在繪製到 canvas 時從這些標籤中檢索影像。透過將這些影像的 CSS 屬性 display 設定為 none,使它們在頁面上隱藏。

每個 <img> 都被賦予了一個 ID 屬性,因此我們有一個用於 source,一個用於 frame,這使得使用 document.getElementById() 很容易選擇它們。我們使用 Promise.all 來等待所有影像載入完畢,然後才呼叫 drawImage()drawImage() 從第一個影像中切出犀牛,並將其縮放到 canvas 上。最後,我們使用第二個 drawImage() 呼叫繪製相框。

在本章的最後一個示例中,我們將構建一個小型藝術畫廊。畫廊由一個包含幾張影像的表格組成。頁面載入時,會為每個影像插入一個 <canvas> 元素,並在其周圍繪製一個邊框。

在這種情況下,每個影像都有固定的寬度和高度,圍繞它們繪製的邊框也是如此。您可以增強指令碼,使其使用影像的寬度和高度來使邊框完美地適合它們。

在下面的程式碼中,我們使用 Promise.all 來等待所有影像載入完畢,然後才將任何影像繪製到 canvas 上。我們迴圈遍歷 document.images 容器,併為每個影像新增新的 canvas 元素。另一件需要注意的事情是 Node.insertBefore 方法的使用。insertBefore() 是父節點(表格單元格)的一個方法,它在我們希望插入新節點(canvas 元素)的元素(影像)之前進行插入。

html

以下是一些 CSS,可以讓效果更美觀:

css

將所有內容連線起來的是繪製帶邊框影像的 JavaScript:

js

控制影像縮放行為

如前所述,由於縮放過程,縮放影像可能會導致出現模糊或塊狀偽影。您可以使用繪圖上下文的 imageSmoothingEnabled 屬性來控制縮放影像時影像平滑演算法的使用。預設情況下,此值為 true,表示影像在縮放時會被平滑處理。