Canvas 及其 Context

在 HTML5 中,Canvas(画布)是用于展现图形绘制结果的容器,它包含少量的方法用于将绘制的图像导出到其它二进制格式,比如 Blob、Data URL 等。

除了常用的HTMLCanvasElement<canvas>元素实例),比较常用的还有OffscreenCanvas,它不会在屏幕上显示,同时解耦了与 DOM 的交互,因而可以运行在 Worker 线程中,使得在执行较复杂的图形任务时不会阻塞主线程。

创建 Canvas

对于HTMLCanvasElement,可通过 HTML 创建,再通过脚本获取其 DOM 实例,也可以通过纯脚本的方式创建:

<canvas width="100" height="100"></canvas>
// 获取HTML页面中的canvas
const canvas1 = document.querySelector("canvas");

// 纯脚本方式创建canvas
const canvas2 = document.createElement("canvas");
canvas2.width = 100;
canvas2.height = 100;

HTMLCanvasElementwidthheight属性用于设置画布内容的宽高,它独立于 DOM 与 CSS 的坐标系,因而通过改变 CSS 样式的宽高会缩放 Canvas,而无法改变画布中绘图区域可用空间的大小。改变HTMLCanvasElementwidthheight属性还会导致画布内容被清空。

OffscreenCanvas只能通过脚本创建,在创建时就必须指定画布大小:

const canvas = new OffscreenCanvas(100, 100);

获取绘图上下文(Context)

主要的图形绘制接口并不在 Canvas 上,而是通过使用被称为 Context 的对象实现的。一个 Canvas 只可以创建一个对应的 Context。Context 种类有很多,本书只讨论用于二维平面图形绘制的 Context。

获取 Context 对象的方法很简单:

// 获取一个 2D Context 对象
const context = canvas.getContext("2d");

对于HTMLCanvasElement,调用getContext("2d")方法返回CanvasRenderingContext2D,而在OffscreenCanvas上调用则返回OffscreenCanvasRenderingContext2D,后者是前者的扩展。

将已有的图像资源绘制到画布上

canvasRenderingContext2D.drawImage()方法可以将 Web 页面中现有的图像资源绘制到画布上,语法:

context.drawImage(image, dx, dy);
context.drawImage(image, dx, dy, dw, wh);
context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);

其中image为图像来源,可以是HTMLCanvasElement<canvas>标签实例)、OffscreenCanvasHTMLImageElement<img>标签实例)、HTMLVideoElement<video>标签实例)或ImageBitmap。例如,将源图像左上角四分之一区域绘制到画布右下角:

const img = document.querySelector("img");
const srcW = Math.floor(img.naturalWidth / 2);
const srcH = Math.floor(img.naturalHeight / 2);

const canvas = document.querySelector("canvas");
const canvasW = canvas.width;
const canvasH = canvas.height;

const context = canvas.getContext("2d");
context.drawImage(
  img,
  0,
  0,
  srcW,
  srcH,
  canvasW - srcW,
  canvasH - srcH,
  srcW,
  srcH
);

HTMLCanvasElement 和二进制操作有关的方法

注意:如果 Canvas 上绘制了来自第三方源的图像,而没有配置跨源授权,一些转化方法会执行失败。例如,调用context.drawImagesrc指向第三方源的<img>绘制图像到 Canvas 上,那么canvas.toDataURL()canvas.toBlob()会失败并抛出错误:Tainted canvases may not be exported。

toDataURL()转化图像为 Data URL

toDataURL()方法将 Canvas 上的图像转化成 Data URL,语法:

canvas.toDataURL();
canvas.toDataURL(type);
canvas.toDataURL(type, quality);

type指定要转化的图片 MIME 类型,默认为image/png,可以设置如image/jpegimage/webp这样的类型,视浏览器的支持情况而定。当浏览器不支持指定的类型时,会使用默认的类型image/png

quality用于指定有损压缩格式的图像质量,介于01之间。

const url = canvas.toDataURL();
// data:image/png;base64,...

toBlob()转化图像为 Blob

语法:

canvas.toDataURL(callback);
canvas.toDataURL(callback, type);
canvas.toDataURL(callback, type, quality);

toDataURL()不同,toBlob()是一个异步回调方法,回调函数callback接受一个参数,其值为 Blob 类型,或者在转化过程中由于任何原因导致失败,会传入null

canvas.toDataURL(
  blob => {
    // if (blob) { ... }
  },
  "image/webp",
  0.9
);

captureStream()抓取为视频流

如在 Canvas 上绘制动画,即不停地重绘画布,可以通过captureStream()获取一个MediaStream流对象,其中包含了视频轨MediaStreamTrack

语法:

canvas.captureStream([frameRate]);

可选的frameRate用于指定视频采集帧率。如不提供该值,则只在每次 Canvas 发生变化时采集一帧;如设置为0,则不会自动采集帧信息,而是需要手动调用canvasCaptureMediaStreamTrack.requestFrame()触发采集动作。

const stream = canvas.captureStream();

const video = document.querySelector("video");
video.srcObject = stream;

transferControlToOffscreen()将控制权转移到 OffscreenCanvas

transferControlToOffscreen()将绘图控制权转移到新的OffscreenCanvas上,通过在OffscreenCanvas上创建 Context 并调用对应的绘图方法来绘图,最终会绘制在原始 Canvas 的画布上。注意原始 Canvas 不能创建 Context,否则调用此方法会失败。

调用此方法后产生的OffscreenCanvas可以传送到 Worker 线程中,执行计算量较大的操作,而不会阻塞主线程。

// 主线程
const canvas = document.querySelector("canvas");
const osCanvas = canvas.transferControlToOffscreen();
const worker = new Worker("worker.js");
worker.postMessage({ canvas: osCanvas }, [osCanvas]);
// Worker线程
addEventListener("message", ({ data: { canvas } }) => {
  const context = canvas.getContext("2d");
  context.strokeRect(10, 10, 50, 50);
});

OffscreenCanvas 和二进制操作有关的方法

convertToBlob()转化为 Blob

convertToBlob()将 OffscreenCanvas 的图像转化成 Blob 导出,返回Promise<Blob>。语法:

canvas.convertToBlob();
canvas.convertToBlob(options);

可选的options对象可以指定导出格式type和有损压缩的质量quality0~1之间)。

transferToImageBitmap()转出到 ImageBitmap

transferToImageBitmap()将当前 OffscreenCanvas 图像转出到 ImageBitmap 对象,然后创建新的空白画布用于后续绘图。

示例:图像格式转换

本示例接受用户选择的图片文件,然后按照用户选择的目标格式进行转化,并提供下载。其最核心的代码便是 Canvas 所提供的将图像转化为 Blob 的方法。

先准备一个 HTML 页面,除了提供选择文件所用的输入框,还有显示原始图像的img标签,以及控制转化输出选项和下载链接的容器元素,默认情况下不显示出来:

<input id="fileinput" type="file" accept="image/*" />
<img id="preview" />
<section id="action" style="display: none">
  <select id="imagetype">
    <option value="image/png">png</option>
    <option value="image/webp">webp</option>
    <option value="image/jpeg">jpeg</option>
  </select>
  <input
    id="imagequality"
    type="number"
    min="0"
    max="1"
    step="0.05"
    value="0.9"
  />
  <a id="download" download>Download</a>
</section>

先实现接受用户选择文件的机制,用户通过文件框选择图像文件,或者拖放图像文件到页面上,都将把该图像显示出来,也作为后续图像转换的来源:

fileinput.addEventListener("change", e => {
  const file = e.target.files[0];
  updatePreview(file);
});

document.addEventListener("dragover", e => e.preventDefault());
document.addEventListener("drop", e => {
  e.preventDefault();
  const file = e.dataTransfer.files?.[0];
  if (file.type.startsWith("image/")) {
    updatePreview(file);
  }
});

const updatePreview = file => {
  if (!file) return;

  preview.src = URL.createObjectURL(file);
  action.style.display = "block";
};

然后,根据用户所选择的转换格式不同,更新下载链接:

const updateDownload = async () => {
  const canvas = new OffscreenCanvas(
    preview.naturalWidth,
    preview.naturalHeight
  );
  const ctx = canvas.getContext("2d");
  ctx.drawImage(preview, 0, 0);
  try {
    const blob = await canvas.convertToBlob({
      type: imagetype.value,
      quality: imagequality.valueAsNumber,
    });
    download.href = URL.createObjectURL(blob);
  } catch (err) {
    download.removeAttribute("href");
  }
};
imagetype.addEventListener("change", updateDownload);
imagequality.addEventListener("change", updateDownload);
updateDownload();

通过使用offscreenCanvas.convertToBlob({type, quality}),我们得到了转化后的 Blob 对象,对其创建 Object URL 后赋到下载链接上,便可用于下载。