HTML-in-Canvas 更新:朝着更好的 Web API 迭代

Chrome 150 和 155 对 HTML-in-Canvas API 进行了多项更改。这些更改根据社区和标准反馈意见进行,旨在改进 API 的人机工程学。这篇博文概述了这些变更,以及如何确保您的实现与这些变更保持同步。

更新包括重命名属性和方法、需要显式预分配纹理内存,以及更新 DOM 同步模型。此外, HTML-in-Canvas 源试用现已 延长。下文将说明发生了哪些变化、变化的原因以及如何更新代码库。

Chrome 150 中的更改

在 Chrome 150 中,我们更改了 WebGL 和 WebGPU 中的实参签名,以消除冗余并与现代图形惯例保持一致。

WebGL

我们从 texElementImage2D 中移除了冗余实参(level、srcFormat、destType)(如需了解详情,请参阅 WebGL 讨论):

// Old implementation (Chrome < 150)
const level = 0;
const internalFormat = gl.RGBA;
const srcFormat = gl.RGBA;
const destType = gl.UNSIGNED_BYTE;

gl.texElementImage2D(
  gl.TEXTURE_2D,
  level,
  internalFormat,
  srcFormat,
  destType,
  element
);

// New implementation (from Chrome 150)
const internalFormat = gl.RGBA8;
gl.texElementImage2D(gl.TEXTURE_2D, internalFormat, element);

WebGPU

我们将 copyElementImageToTexture 参数标准化为 sourceDict 和 destDict 对象(请参阅 GPUWeb PR#6250):

// Old implementation (Chrome < 150)
GPUQueue.copyElementImageToTexture(
  element,
  width,
  height,
  { texture: texture }
);

// New implementation (from Chrome 150)
const sourceDict = { source: element };
const destDict = {
  destination: { texture: texture },
  width: width,
  height: height
};

GPUQueue.copyElementImageToTexture(sourceDict, destDict);

Chrome 155 中的更改

Chrome 155 根据开发者的反馈意见进行了进一步改进。我们正在更新方法名称,要求显式预分配纹理内存,支持复杂的 DOM 子树,并分离 DOM 同步。

WebGPU

WebGPU 方法已从 copyElementImageToTexture 重命名为 drawElementImageToTexture。除了重命名之外,参数坐标也会扩展 (sourceX, sourceWidth),并且目的地边界会分组到标准 size (GPUExtent3D) 对象中:

// BEFORE (Chrome 150)
const sourceDict = {
  source: myElement,
  sx: 0,
  sy: 0,
  swidth: 100,
  sheight: 100
};
const destDict = {
  destination: { texture: myTexture },
  width: 300,
  height: 200
};

queue.copyElementImageToTexture(sourceDict, destDict);

// AFTER (Chrome 155+)
const sourceDict = {
  source: myElement,
  sourceX: 0,
  sourceY: 0,
  sourceWidth: 100,
  sourceHeight: 100
};
const destDict = {
  texture: myTexture,
  size: { width: 300, height: 200 }
};

queue.drawElementImageToTexture(sourceDict, destDict);

WebGL

HTML-in-Canvas 的 WebGL 更改涉及以下三个方面:

  • 方法名称从 texElementImage2D 更改为 texElementSubImage2D。
  • 方法签名发生了变化;最显著的变化是添加了 xoffset 和 yoffset 实参,以便选择性地指定在目标纹理中绘制元素时的偏移量。
  • 该方法不再自动为目标纹理分配缓冲区存储空间;必须在使用 API 之前明确分配存储空间。这与 texSubImage2D 的行为类似。
// 1. Capture natural layout size of the element
const elementImage = canvas.captureElementImage(element);

// 2. Pre-allocate texture backing once (or when element resizes)
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA8,
  Math.ceil(elementImage.width),
  Math.ceil(elementImage.height),
  0,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  null // null reserves VRAM without uploading pixels
);

// 3. Upload DOM into pre-allocated VRAM
gl.texElementSubImage2D(
  gl.TEXTURE_2D,
  0,                  // level
  0,                  // xoffset
  0,                  // yoffset
  element             // pass DOM element
);

画布属性和可绘制子元素

我们正在更新准备画布以渲染 HTML 的方式。

将 layoutsubtree 重命名为 content="drawable"

为了与 HTML 属性命名惯例保持一致,<canvas> 元素上的布尔值 layoutsubtree 属性已重命名为 content="drawable"(如需详细讨论,请参阅 WICG 问题 #169)。

后代的 drawable 属性

以前,只有画布的直接子项会被渲染。为了支持复杂的 DOM 层次结构而不需扁平化标记,您现在可以明确地将 drawable 属性添加到您打算单独绘制的任何后代元素:

<!-- BEFORE -->
<canvas layoutsubtree>
  <div id="badge">
    <h2>Player Stats</h2>
    <button id="actionBtn">Equip</button>
  </div>
</canvas>

<!-- AFTER (Chrome 155+) -->
<canvas content="drawable">
  <!-- Subtree A (Body and heading) -->
  <div id="badge" drawable>
    <h2>Player Stats</h2>
    <!-- Subtree B (Excluded from Subtree A; drawn and animated independently) -->
    <button id="actionBtn" drawable>Equip</button>
  </div>
</canvas>

具有 drawable 的元素会捕获其整个子树,但指定了 drawable 的嵌套后代除外。这样,父容器和按钮等子元素就可以在单独的绘制调用中添加动画效果和绘制。

命中测试同步

以前,开发者必须捕获绘制方法的返回值并将其传递给 element.style.transform。drawElementImage() 方法现在返回 void,同步取决于上下文。

2D 上下文:自动同步

在 2D 画布中,浏览器会自动将 DOM 命中测试边界和屏幕阅读器焦点环同步到绘制的坐标。您可以移除所有手动转换管道:

// BEFORE
const transform = ctx.drawElementImage(element, x, y);
element.style.transform = transform.toString();

// AFTER (Chrome 155+)
ctx.drawElementImage(element, x, y);
// No style update needed: position & hit-testing sync automatically

如需在 2D 中手动管理几何图形,请将 { preserveElementGeometry: true } 传递给 drawElementImage(),并使用 canvas.updateElementGeometry() 应用转换。

3D 上下文:显式 updateElementGeometry()

如需在 WebGL 或 WebGPU 上下文中同步 DOM,请构建 DOMMatrix 转换并将其传递给 canvas.updateElementGeometry():

canvas.addEventListener('paint', () => {
  // 1. Upload element snapshot to GPU texture
  device.queue.drawElementImageToTexture({ source: element }, { texture });

  // 2. Render 3D scene...
  drawScene();

  // 3. Construct transform matrix as needed
  const canvasTransform = new DOMMatrix().translate(x, y);

  // 4. Update DOM hit-testing and accessibility bounding boxes
  canvas.updateElementGeometry(element, { canvasTransform });
});

跨版本兼容性

如果您的应用在过渡期间必须支持多个 Chrome 版本中的用户,请检查其原型中是否存在更新后的方法,并相应地调整您的实现。

请确保为每个浏览器版本设置正确的 <canvas> 元素属性:

// ============================================================================
//  Cross-version canvas markup
// ============================================================================
if (!canvas.hasAttribute('content') && !canvas.hasAttribute('layoutsubtree')) {
  if ('content' in HTMLCanvasElement.prototype) {
    // Chrome 155+ (New standardized attribute)
    canvas.setAttribute('content', 'drawable');
  } else {
    // Chrome < 155 (Legacy boolean attribute)
    canvas.setAttribute('layoutsubtree', '');
  }
}

对于 2D 上下文,请检查 drawElementImage() 的返回值并检查画布原型属性,以同时支持这两个版本:

// ============================================================================
// Cross-version 2D drawing & synchronization
// ============================================================================
function draw2DElement(ctx, element, x, y) {
  // Execute the 2D draw call
  const result = ctx.drawElementImage(element, x, y);

  if (result !== undefined && typeof result?.toString === 'function') {
    // Chrome < 155: drawElementImage returned a DOMMatrix
    // Requires manual transform piping to align DOM hit-test bounds:
    element.style.transform = result.toString();
  } else {
    // Chrome 155+: drawElementImage returns void / undefined
    // The browser automatically aligns DOM positioning, clicks, and a11y bounds!
    // If you previously had element.style.transform logic here, clear it:
    if (element.style.transform) {
      element.style.transform = "";
    }
  }
}

对于 WebGPU,请使用兼容的方法签名和名称:

// ============================================================================
// WebGPU cross-version support
// ============================================================================
if ('drawElementImageToTexture' in GPUQueue.prototype) {
  // Chrome 155+ (Latest implementation)
  queue.drawElementImageToTexture(
    { source: element, sourceX: 0, sourceY: 0, sourceWidth: w, sourceHeight: h },
    { texture: myTexture, size: { width: w, height: h } }
  );
} else if (typeof queue.copyElementImageToTexture === "function") {
  // Chrome 150 - 154: Try the Chrome 150+ dictionary signature first,
  // then fall back to the Chrome < 150 signature
  try {
    queue.copyElementImageToTexture(
      { source: element },
      { destination: { texture: myTexture }, width: w, height: h }
    );
  } catch (err) {
    // Chrome < 150
    queue.copyElementImageToTexture(element, { texture: myTexture });
  }
}

请务必检查浏览器是否与 WebGL 兼容:

// ============================================================================
// WebGL cross-version support
// ============================================================================
if ('texElementSubImage2D' in WebGL2RenderingContext.prototype) {
  // Chrome 155+ (Explicit pre-allocation + sub-image)
  gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element);
} else if (gl.texElementImage2D && gl.texElementImage2D.length === 3) {
  // Chrome 150-154 (3-argument signature)
  gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, element);
} else if (gl.texElementImage2D) {
  // Chrome < 150 (6-argument signature)
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, element);
}

源试用延期

HTML-in-Canvas 源试用已延长至 Chrome 160。请务必续订源试用令牌,以便继续向用户提供 HTML-in-Canvas 功能。如需了解详情,请参阅源试用文章。

分享您的反馈

请 在 WICG GitHub 代码库中提交问题,告诉我们这些更新在您的应用中效果如何。 订阅开发者简报,及时了解 HTML-in-Canvas 的最新动态。