使用 canvas 進行畫素操作

到目前為止,我們還沒有關注 Canvas 的實際畫素。使用 ImageData 物件,您可以直接讀取和寫入一個數據陣列來操作畫素資料。我們還將研究如何控制影像平滑(抗鋸齒),以及如何從 Canvas 儲存影像。

ImageData 物件

ImageData 物件表示 Canvas 物件區域的底層畫素資料。其 data 屬性返回一個 Uint8ClampedArray(如果請求,則為 Float16Array),可以訪問該陣列來檢視原始畫素資料;每個畫素由四個單位元組值(按順序為紅、綠、藍和 Alpha,即“RGBA”格式)表示。每種顏色分量都表示為一個介於 0 和 255 之間的整數。每個分量在陣列中被分配一個連續的索引,其中左上角畫素的紅色分量位於陣列索引 0 處。然後,畫素從左到右,再向下,依次填充整個陣列。

Uint8ClampedArray 包含 height × width × 4 位元組的資料,索引值範圍從 0 到 (height × width × 4) - 1。

例如,要讀取影像中位於第 50 行、第 200 列畫素的藍色分量值,您需要執行以下操作:

js
const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2];

如果給定一組座標(X 和 Y),您可能會執行類似以下操作:

js
const xCoord = 50;
const yCoord = 100;
const canvasWidth = 1024;

const getColorIndicesForCoord = (x, y, width) => {
  const red = y * (width * 4) + x * 4;
  return [red, red + 1, red + 2, red + 3];
};

const colorIndices = getColorIndicesForCoord(xCoord, yCoord, canvasWidth);

const [redIndex, greenIndex, blueIndex, alphaIndex] = colorIndices;

您還可以透過讀取 Uint8ClampedArray.length 屬性來訪問畫素陣列的大小(以位元組為單位)。

js
const numBytes = imageData.data.length;

建立 ImageData 物件

要建立一個新的、空的 ImageData 物件,您應該使用 createImageData() 方法。createImageData() 方法有兩個版本:

js
const myImageData = ctx.createImageData(width, height);

這會建立一個具有指定尺寸的新 ImageData 物件。所有畫素都預設為透明。

您還可以建立一個尺寸與 anotherImageData 指定的物件相同的 ImageData 物件。新物件的所有畫素都預設為透明黑色。這不會複製影像資料!

js
const myImageData = ctx.createImageData(anotherImageData);

獲取 Canvas 上下文的畫素資料

要獲取包含 Canvas 上下文畫素資料副本的 ImageData 物件,您可以使用 getImageData() 方法。

js
const myImageData = ctx.getImageData(left, top, width, height);

此方法返回一個 ImageData 物件,該物件表示 Canvas 區域的畫素資料,該區域的角點由以下點表示:(left, top)、(left+width, top)、(left, top+height)和(left+width, top+height)。座標以 Canvas 座標空間單位指定。

注意: 任何 Canvas 外部的畫素將在生成的 ImageData 物件中返回為透明黑色。

本文件 使用 Canvas 操作影片 也演示了此方法。

建立顏色選擇器

在此示例中,我們使用 getImageData() 方法來顯示滑鼠游標下的顏色。為此,我們需要滑鼠的當前位置,然後查詢 getImageData() 提供的畫素陣列中該位置的畫素資料。最後,我們使用陣列資料設定 <div> 的背景顏色和文字來顯示顏色。單擊影像將執行相同的操作,但使用選定的顏色。

html
<table>
  <thead>
    <tr>
      <th>Source</th>
      <th>Hovered color</th>
      <th>Selected color</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <canvas id="canvas" width="300" height="227"></canvas>
      </td>
      <td class="color-cell" id="hovered-color"></td>
      <td class="color-cell" id="selected-color"></td>
    </tr>
  </tbody>
</table>
js
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
  img.style.display = "none";
});
const hoveredColor = document.getElementById("hovered-color");
const selectedColor = document.getElementById("selected-color");

const pick = (event, destination) => {
  const bounding = canvas.getBoundingClientRect();
  const x = event.clientX - bounding.left;
  const y = event.clientY - bounding.top;
  const pixel = ctx.getImageData(x, y, 1, 1);
  const data = pixel.data;

  const rgbColor = `rgb(${data[0]} ${data[1]} ${data[2]} / ${data[3] / 255})`;
  destination.style.background = rgbColor;
  destination.textContent = rgbColor;

  return rgbColor;
};

canvas.addEventListener("mousemove", (event) => pick(event, hoveredColor));
canvas.addEventListener("click", (event) => pick(event, selectedColor));

將游標懸停在影像上的任何位置,即可在“懸停顏色”列中看到結果。單擊影像中的任何位置,即可在“選中顏色”列中看到結果。

將畫素資料繪製到 Canvas 上下文

您可以使用 putImageData() 方法將畫素資料繪製到 Canvas 上下文中。

js
ctx.putImageData(myImageData, dx, dy);

dxdy 引數指示要在 Canvas 上下文中繪製您希望繪製的畫素資料的左上角的裝置座標。

例如,要將 myImageData 表示的整個影像繪製到 Canvas 上下文的左上角,您可以執行以下操作:

js
ctx.putImageData(myImageData, 0, 0);

灰度化和反色

在此示例中,我們遍歷所有畫素以更改它們的值,然後使用 putImageData() 將修改後的畫素陣列放回 Canvas。invert 函式將每種顏色從最大值 255 中減去。grayscale 函式使用紅色、綠色和藍色的平均值。您也可以使用加權平均值,例如,由公式 x = 0.299r + 0.587g + 0.114b 給出。有關更多資訊,請參閱 Wikipedia 上的 灰度

html
<canvas id="canvas" width="300" height="227"></canvas>
<form>
  <input type="radio" id="original" name="color" value="original" checked />
  <label for="original">Original</label>

  <input type="radio" id="grayscale" name="color" value="grayscale" />
  <label for="grayscale">Grayscale</label>

  <input type="radio" id="inverted" name="color" value="inverted" />
  <label for="inverted">Inverted</label>

  <input type="radio" id="sepia" name="color" value="sepia" />
  <label for="sepia">Sepia</label>
</form>
js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
img.onload = () => {
  ctx.drawImage(img, 0, 0);
};

const original = () => {
  ctx.drawImage(img, 0, 0);
};

const invert = () => {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i]; // red
    data[i + 1] = 255 - data[i + 1]; // green
    data[i + 2] = 255 - data[i + 2]; // blue
  }
  ctx.putImageData(imageData, 0, 0);
};

const grayscale = () => {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = avg; // red
    data[i + 1] = avg; // green
    data[i + 2] = avg; // blue
  }
  ctx.putImageData(imageData, 0, 0);
};

const sepia = () => {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    let r = data[i], // red
      g = data[i + 1], // green
      b = data[i + 2]; // blue

    data[i] = Math.min(Math.round(0.393 * r + 0.769 * g + 0.189 * b), 255);
    data[i + 1] = Math.min(Math.round(0.349 * r + 0.686 * g + 0.168 * b), 255);
    data[i + 2] = Math.min(Math.round(0.272 * r + 0.534 * g + 0.131 * b), 255);
  }
  ctx.putImageData(imageData, 0, 0);
};

const inputs = document.querySelectorAll("[name=color]");
for (const input of inputs) {
  input.addEventListener("change", (evt) => {
    switch (evt.target.value) {
      case "inverted":
        return invert();
      case "grayscale":
        return grayscale();
      case "sepia":
        return sepia();
      default:
        return original();
    }
  });
}

單擊不同的選項以檢視實際效果。

縮放和抗鋸齒

藉助 drawImage() 方法、第二個 Canvas 和 imageSmoothingEnabled 屬性,我們可以放大圖片並檢視細節。還繪製了一個沒有 imageSmoothingEnabled 的第三個 Canvas,以便進行並排比較。

html
<table>
  <thead>
    <tr>
      <th>Source</th>
      <th>Smoothing enabled = true</th>
      <th>Smoothing enabled = false</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <canvas id="canvas" width="300" height="227"></canvas>
      </td>
      <td>
        <canvas id="smoothed" width="200" height="200"></canvas>
      </td>
      <td>
        <canvas id="pixelated" width="200" height="200"></canvas>
      </td>
    </tr>
  </tbody>
</table>

我們獲取滑鼠的位置,並裁剪影像,從左邊和上方裁剪 5 畫素,到右邊和下方裁剪 5 畫素。然後,我們將它複製到另一個 Canvas 上,並將影像縮放到我們想要的大小。在縮放 Canvas 中,我們將原始 Canvas 的 10x10 畫素裁剪區域縮放到 200x200。

js
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
img.onload = () => {
  draw(img);
};

function draw(image) {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  ctx.drawImage(image, 0, 0);

  const smoothCtx = document.getElementById("smoothed").getContext("2d");
  smoothCtx.imageSmoothingEnabled = true;

  const pixelatedCtx = document.getElementById("pixelated").getContext("2d");
  pixelatedCtx.imageSmoothingEnabled = false;

  const zoom = (ctx, x, y) => {
    ctx.drawImage(
      canvas,
      Math.min(Math.max(0, x - 5), image.width - 10),
      Math.min(Math.max(0, y - 5), image.height - 10),
      10,
      10,
      0,
      0,
      200,
      200,
    );
  };

  canvas.addEventListener("mousemove", (event) => {
    const x = event.layerX;
    const y = event.layerY;
    zoom(smoothCtx, x, y);
    zoom(pixelatedCtx, x, y);
  });
}

儲存影像

HTMLCanvasElement 提供了一個 toDataURL() 方法,這在儲存影像時非常有用。它返回一個 data URL,其中包含由 type 引數(預設為 PNG)指定的格式的影像表示。返回的影像解析度為 96 dpi。

注意: 請注意,如果 Canvas 包含任何未透過 CORS 獲得但源自另一個 的畫素,則 Canvas 將被汙染,其內容將無法再被讀取和儲存。請參閱 安全和被汙染的 Canvas

canvas.toDataURL('image/png')

預設設定。建立 PNG 影像。

canvas.toDataURL('image/jpeg', quality)

建立 JPG 影像。可選地,您可以提供一個介於 0 到 1 之間的質量值,其中 1 是最佳質量,0 幾乎無法識別但檔案大小很小。

一旦您從 Canvas 生成了 data URL,您就可以將其用作任何 的源,或者將其放入帶有 download 屬性 的超連結中,以便將其儲存到磁碟。

您還可以從 Canvas 建立一個 Blob

canvas.toBlob(callback, type, encoderOptions)

建立表示 Canvas 中影像的 Blob 物件。

另見