使用 Canvas 进行像素操作

到目前为止,我们还没有查看画布的实际像素。使用ImageData对象,您可以直接读取和写入数据数组以操纵像素数据。我们还将了解如何控制图像平滑(抗锯齿)以及如何从画布保存图像。

ImageData 对象

ImageData对象表示画布对象区域的基础像素数据。它包含以下只读属性

width

图像的宽度(以像素为单位)。

height

图像的高度(以像素为单位)。

data

一个Uint8ClampedArray,表示一个一维数组,包含按 RGBA 顺序排列的数据,整数取值范围为0255(包含)。

data属性返回一个Uint8ClampedArray,可以访问它查看原始像素数据;每个像素由四个一字节值表示(依次为红色、绿色、蓝色和 alpha;即“RGBA”格式)。每个颜色分量由 0 到 255 之间的整数表示。每个分量在数组中分配一个连续的索引,其中左上角像素的红色分量位于数组中的索引 0 处。然后,像素从左到右,然后向下,贯穿整个数组。

Uint8ClampedArray包含height×width×4 个字节的数据,索引值范围从 0 到 (height×width×4)-1。

例如,要读取图像中第 200 列、第 50 行像素的蓝色分量的值,您可以执行以下操作

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对象。所有像素都预设为透明黑色(全部为零,即 rgb(0 0 0 / 0%))。

您还可以使用anotherImageData指定的同一个对象的维度创建一个新的ImageData对象。新对象的像素都预设为透明黑色。**这不会复制图像数据!**

js
const myImageData = ctx.createImageData(anotherImageData);

获取上下文中的像素数据

要获取包含画布上下文像素数据副本的ImageData对象,可以使用getImageData()方法

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

此方法返回一个ImageData对象,表示画布区域的像素数据,该区域的角点由点 (left, top)、(left+width, top)、(left, top+height) 和 (left+width, top+height) 表示。坐标以画布坐标空间单位指定。

注意:任何画布外部的像素在生成的ImageData对象中都将返回为透明黑色。

此方法也在文章使用 Canvas 操作视频中进行了演示。

拾色器

在此示例中,我们使用getImageData()方法显示鼠标光标下的颜色。为此,我们需要鼠标的当前位置,然后在getImageData()提供的像素数组中查找该位置的像素数据。最后,我们使用数组数据设置背景颜色和<div>中的文本以显示颜色。单击图像将执行相同的操作,但会记住选定的颜色是什么。

js
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/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");

function 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));

以下实时示例演示了代码的用法

另请参阅源代码 - HTMLJavaScript

将像素数据绘制到上下文中

您可以使用putImageData()方法将像素数据绘制到上下文中

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

dxdy参数指示上下文中要绘制要绘制的像素数据左上角的设备坐标。

例如,要将myImageData表示的整个图像绘制到上下文的左上角,您可以执行以下操作

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

灰度化和反转颜色

在此示例中,我们迭代所有像素以更改其值,然后使用putImageData()将修改后的像素数组放回画布。invert 函数将每个颜色从最大值 255 中减去。灰度函数使用红色、绿色和蓝色的平均值。您还可以使用加权平均值,例如由公式x = 0.299r + 0.587g + 0.114b给出。有关详细信息,请参阅维基百科上的灰度

js
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/rhino.jpg";

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

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 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();
      default:
        return original();
    }
  });
}

以下实时示例演示了代码的用法

另请参阅源代码 - HTMLJavaScript

缩放和抗锯齿

借助drawImage()方法、第二个画布和imageSmoothingEnabled属性,我们可以放大图片并查看细节。还绘制了第三个没有imageSmoothingEnabled的画布,以便进行并排比较

我们获取鼠标的位置并裁剪左侧和上侧 5 个像素到右侧和下侧 5 个像素的图像。然后,我们将其复制到另一个画布并调整图像大小到我们想要的大小。在缩放画布中,我们将原始画布的 10×10 像素裁剪调整大小到 200×200。

js
zoomctx.drawImage(
  canvas,
  Math.min(Math.max(0, x - 5), img.width - 10),
  Math.min(Math.max(0, y - 5), img.height - 10),
  10,
  10,
  0,
  0,
  200,
  200,
);

缩放示例

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

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

  const smoothedZoomCtx = document
    .getElementById("smoothed-zoom")
    .getContext("2d");
  smoothedZoomCtx.imageSmoothingEnabled = true;

  const pixelatedZoomCtx = document
    .getElementById("pixelated-zoom")
    .getContext("2d");
  pixelatedZoomCtx.imageSmoothingEnabled = false;

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

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

以下实时示例演示了代码的用法

另请参阅源代码 - HTMLJavaScript

保存图像

HTMLCanvasElement提供了一个toDataURL()方法,在保存图像时很有用。它返回一个包含图像表示形式的数据 URL,格式由type参数指定(默认为PNG)。返回的图像的分辨率为 96 dpi。

注意:请注意,如果画布包含任何从另一个来源获取的像素,而没有使用 CORS,则画布将被污染,并且其内容将无法再读取和保存。请参阅安全性和被污染的画布

canvas.toDataURL('image/png')

默认设置。创建 PNG 图像。

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

创建 JPG 图像。可选地,您可以在 0 到 1 的范围内提供质量,其中 1 是最佳质量,而 0 几乎无法识别,但文件大小很小。

从画布生成数据 URL 后,您可以将其用作任何<img>的源,或将其放入带有download 属性的超链接中以将其保存到磁盘,例如。

您还可以从画布创建Blob

canvas.toBlob(callback, type, encoderOptions)

创建一个表示画布中包含的图像的Blob对象。

另请参阅