Actualizaciones de HTML-in-Canvas: Iteración hacia una mejor API web

Chrome 150 y 155 introdujeron varios cambios en la API de HTML-in-Canvas. Estos cambios respondieron a los comentarios de la comunidad y los estándares con el objetivo de mejorar la ergonomía de la API. En esta entrada de blog, se describen los cambios y cómo puedes asegurarte de que tu implementación esté actualizada con ellos.

Las actualizaciones incluyen el cambio de nombre de atributos y métodos, la necesidad de una asignación previa explícita de memoria de texturas y la actualización de los modelos de sincronización del DOM. Además, la prueba de origen de HTML-in-Canvas ahora se extendió. A continuación, se explica qué cambiará, por qué y cómo actualizar tu base de código.

Cambios en Chrome 150

En Chrome 150, cambiamos las firmas de argumentos en WebGL y WebGPU para eliminar la redundancia y alinearnos con las convenciones de gráficos modernas.

WebGL

Quitamos argumentos redundantes (level, srcFormat, destType) de texElementImage2D (consulta la discusión sobre WebGL para obtener más detalles):

// 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

Estandarizamos los parámetros de copyElementImageToTexture en objetos sourceDict y destDict (consulta la PR#6250 de GPUWeb):

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

Cambios en Chrome 155

Chrome 155 presenta más mejoras basadas en los comentarios de los desarrolladores. Actualizamos los nombres de los métodos, requerimos la asignación previa explícita de memoria de texturas, admitimos subárboles DOM complejos y desacoplamos la sincronización del DOM.

WebGPU

Se cambió el nombre del método WebGPU de copyElementImageToTexture a drawElementImageToTexture. Junto con el cambio de nombre, se expanden las coordenadas de los parámetros (sourceX, sourceWidth) y los límites del destino se agrupan en un objeto size (GPUExtent3D) estándar:

// 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

Hay tres aspectos relacionados con los cambios en WebGL para HTML-in-Canvas:

  • El nombre del método cambia de texElementImage2D a texElementSubImage2D".
  • La firma del método cambia y, lo que es más importante, agrega argumentos xoffset y yoffset para permitir, de forma opcional, especificar una compensación en la textura de destino en la que se dibujará el elemento.
  • El método ya no asigna automáticamente almacenamiento de búfer para la textura de destino. El almacenamiento debe asignarse de forma explícita antes de usar la API. Esto es similar al comportamiento de 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
);

Atributos de Canvas y elementos secundarios dibujables

Estamos actualizando la forma en que preparas tu lienzo para renderizar HTML.

Se cambió el nombre de layoutsubtree a content="drawable".

Para alinearse con las convenciones de nomenclatura de los atributos HTML, se cambió el nombre del atributo booleano layoutsubtree en el elemento <canvas> a content="drawable" (consulta el problema núm. 169 de WICG para obtener una explicación detallada).

El atributo drawable para los elementos secundarios

Anteriormente, solo se renderizaban los elementos secundarios directos de un lienzo. Para admitir jerarquías del DOM complejas sin aplanar el lenguaje de marcado, ahora puedes agregar de forma explícita el atributo drawable a cualquier elemento secundario que desees dibujar de forma independiente:

<!-- 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>

Un elemento con drawable captura todo su subárbol excepto los descendientes anidados que también especifican drawable. Esto permite que los contenedores principales y los elementos secundarios, como los botones, se animen y dibujen en llamadas de dibujo separadas.

Sincronización de pruebas de impacto

Anteriormente, los desarrolladores debían capturar el valor de devolución de los métodos de dibujo y canalizarlo en element.style.transform. El método drawElementImage() ahora devuelve void, y la sincronización depende del contexto.

Contextos 2D: Sincronización automática

En el lienzo 2D, el navegador sincroniza automáticamente los límites de la prueba de detección de clics del DOM y los anillos de enfoque del lector de pantalla con las coordenadas dibujadas. Puedes quitar todas las canalizaciones de transformación manual:

// 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

Para administrar la geometría de forma manual en 2D, pasa { preserveElementGeometry: true } a drawElementImage() y aplica transformaciones con canvas.updateElementGeometry().

Contextos 3D: Explícito updateElementGeometry()

Para sincronizar el DOM en contextos de WebGL o WebGPU, crea una transformación DOMMatrix y pásala a 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 });
});

Compatibilidad entre versiones

Si tu aplicación debe admitir usuarios en varias versiones de Chrome durante el período de transición, verifica la presencia de los métodos actualizados en sus prototipos y ajusta tu implementación en consecuencia.

Asegúrate de establecer los atributos correctos del elemento <canvas> para cada versión del navegador:

// ============================================================================
//  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', '');
  }
}

Para el contexto 2D, inspecciona el valor de devolución de drawElementImage() y verifica los atributos del prototipo de lienzo para admitir ambas versiones:

// ============================================================================
// 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 = "";
    }
  }
}

En el caso de WebGPU, usa la firma y el nombre del método compatibles:

// ============================================================================
// 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 });
  }
}

Asegúrate de verificar la compatibilidad del navegador con 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);
}

Extensión de la prueba de origen

La prueba de origen de HTML-in-Canvas se extendió hasta Chrome 160. Asegúrate de renovar el token de prueba de origen para seguir ofreciendo la función de HTML en Canvas a tus usuarios. Consulta el artículo sobre las pruebas de origen para obtener más detalles.

Comparte tus comentarios

Presenta un problema en el repositorio de GitHub del WICG para informarnos cómo funcionan estas actualizaciones en tus aplicaciones. Suscríbete al boletín informativo para desarrolladores y mantente al tanto de las novedades de HTML-in-Canvas.