使用影像
到目前為止,我們已經建立了自己的形狀並對其應用了樣式。<canvas> 的一個更令人興奮的功能是能夠使用影像。這些影像可用於動態照片合成、圖表背景、遊戲中的精靈等。瀏覽器支援的任何格式的外部影像都可以使用,例如 PNG、GIF 或 JPEG。您甚至可以使用同一頁面上其他 canvas 元素生成的影像作為源!
將影像匯入 canvas 基本上是一個兩步過程
- 獲取對
HTMLImageElement物件或另一個 canvas 元素作為源的引用。也可以透過提供 URL 來使用影像。 - 使用
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() 建構函式。
const img = new Image(); // Create new img element
img.src = "myImage.png"; // Set source path
當此指令碼執行時,影像開始載入,但如果您嘗試在影像載入完成之前呼叫 drawImage(),它將什麼也不做。舊的瀏覽器甚至可能丟擲異常,因此您需要確保使用 load 事件,這樣就不會在影像準備好之前將其繪製到 canvas 上。
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()。
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 編碼的字元字串,直接放在您的程式碼中。
const img = new Image(); // Create new img element
img.src =
"data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";
Data URL 的一個優點是生成的影像可以立即獲得,無需與伺服器進行另一輪通訊。另一個潛在的優點是,您還可以將所有的 CSS、JavaScript、HTML 和影像封裝在一個檔案中,使其更易於移植到其他位置。
此方法的缺點是影像未被快取,並且對於較大的影像,編碼的 URL 可能會變得非常長。
使用影片幀
您還可以使用 <video> 元素呈現的影片的幀(即使影片不可見)。例如,如果您有一個 ID 為“myVideo”的 <video> 元素,您可以這樣做。
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 的左上角。
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)-
這添加了
width和height引數,它們指定了將影像繪製到 canvas 上時要縮放到的尺寸。
示例:平鋪影像
在此示例中,我們將使用一張影像作為桌布,並將其在 canvas 上重複多次。這是透過迴圈並將縮放後的影像放置在不同位置來完成的。在下面的程式碼中,第一個 for 迴圈遍歷行。第二個 for 迴圈遍歷列。影像被縮放到其原始大小的三分之一,即 50x38 畫素。
注意: 影像在放大或縮小過多時可能會變得模糊或顆粒狀。如果您要在影像中包含需要保持清晰可讀的文字,則不建議進行縮放。
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) 為左上角、寬度和高度分別為sWidth和sHeight的矩形指定的區域,將其繪製到 canvas 上,放置在 canvas 的 (dx,dy) 位置,並將其縮放到由dWidth和dHeight指定的大小,同時保持其縱橫比。
為了真正理解這一點,看看這張圖可能會有幫助:

前四個引數定義了源影像上切片的**位置和大小**。後四個引數定義了在目標 canvas 上繪製圖像的**矩形**。
當您想要進行合成時,切片可能是一個有用的工具。您可以將所有元素放在單個影像檔案中,並使用此方法合成完整的繪圖。例如,如果您想製作圖表,您可以有一個包含所有必需文字的 PNG 影像檔案,並且根據您的資料,可以相當容易地更改圖表的比例。另一個優點是您不必單獨載入每個影像,這可以提高載入效能。
示例:為影像新增邊框
在此示例中,我們將使用與上一個示例中相同的犀牛,但我們將切出它的頭部並將其合成到相框中。相框影像是 24 位 PNG,包含陰影。因為 24 位 PNG 影像包含完整的 8 位 alpha 通道,與 GIF 和 8 位 PNG 影像不同,它可以放在任何背景上,而無需擔心背景色。
<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>
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 元素)的元素(影像)之前進行插入。
<table>
<tr>
<td><img src="gallery_1.jpg" /></td>
<td><img src="gallery_2.jpg" /></td>
<td><img src="gallery_3.jpg" /></td>
<td><img src="gallery_4.jpg" /></td>
</tr>
<tr>
<td><img src="gallery_5.jpg" /></td>
<td><img src="gallery_6.jpg" /></td>
<td><img src="gallery_7.jpg" /></td>
<td><img src="gallery_8.jpg" /></td>
</tr>
</table>
<img id="frame" src="canvas_picture_frame.png" width="132" height="150" />
以下是一些 CSS,可以讓效果更美觀:
body {
background: 0 -100px repeat-x url("bg_gallery.png") #4f191a;
margin: 10px;
}
img {
display: none;
}
table {
margin: 0 auto;
}
td {
padding: 15px;
}
將所有內容連線起來的是繪製帶邊框影像的 JavaScript:
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)),
),
);
// Loop through all images.
for (const image of document.images) {
// Don't add a canvas for the frame image
if (image.getAttribute("id") !== "frame") {
// Create canvas element
const canvas = document.createElement("canvas");
canvas.setAttribute("width", 132);
canvas.setAttribute("height", 150);
// Insert before the image
image.parentNode.insertBefore(canvas, image);
ctx = canvas.getContext("2d");
// Draw image to canvas
ctx.drawImage(image, 15, 20);
// Add frame
ctx.drawImage(document.getElementById("frame"), 0, 0);
}
}
}
draw();
控制影像縮放行為
如前所述,由於縮放過程,縮放影像可能會導致出現模糊或塊狀偽影。您可以使用繪圖上下文的 imageSmoothingEnabled 屬性來控制縮放影像時影像平滑演算法的使用。預設情況下,此值為 true,表示影像在縮放時會被平滑處理。