Presentamos la prueba de origen de la API de HTML-in-Canvas

Thomas Nattestad
Thomas Nattestad

Durante años, los desarrolladores web tuvieron que tomar una difícil decisión arquitectónica cuando creaban aplicaciones visuales complejas y altamente interactivas en la Web: ¿se basaban en el DOM por sus ricas funciones semánticas o renderizaban directamente en el elemento <canvas> para obtener un rendimiento gráfico de bajo nivel?

Con la nueva API de HTML en Canvas experimental, disponible ahora en la prueba de origen, no tienes que elegir. Esta API te permite dibujar contenido del DOM directamente en un lienzo 2D o en una textura de WebGL/WebGPU, a la vez que mantiene la IU interactiva, accesible y conectada a tus funciones favoritas del navegador. Si combinas HTML con el procesamiento de gráficos de bajo nivel, puedes crear experiencias que antes eran imposibles.

El DOM frente a Canvas

Para comprender el poder de esta nueva API, es útil observar las fortalezas relativas del DOM y de Canvas.

El DOM es el elemento básico de la IU web. Ofrece soluciones de diseño de texto listas para usar, con contenido comprendido semánticamente para crear interfaces enriquecidas. Esto permite a los usuarios realizar operaciones comunes en las páginas web sin problemas, cosas que a menudo damos por sentado, como destacar texto para copiarlo o hacer clic con el botón derecho en una imagen para guardarla. El DOM también se integra con funciones esenciales del navegador: herramientas de accesibilidad, traducción, búsqueda en la página, modo de lectura, extensiones, modo oscuro, zoom del navegador y autocompletar.

Por otro lado, Canvas (y WebGL/WebGPU) permite el acceso de bajo nivel para controlar una cuadrícula de píxeles para gráficos 2D y 3D muy avanzados. Los juegos y las apps web complejas (como Documentos de Google o Figma) requieren este acceso de bajo nivel y alto rendimiento. Dado que el lienzo es fundamentalmente una cuadrícula de píxeles, las funciones de compatibilidad, como el texto responsivo, solían requerir una lógica de IU personalizada compleja, lo que aumentaba drásticamente el tamaño del paquete. Fundamentalmente, todas las potentes funciones del navegador integradas en el DOM se interrumpen por completo cuando la IU queda atrapada dentro de una cuadrícula de píxeles estática del lienzo.

Ventajas de llevar el DOM a Canvas

La API de HTML en Canvas es el puente que te ofrece lo mejor de ambos mundos. Si colocas HTML dentro del elemento <canvas> y sincronizas su transformación, te aseguras de que el contenido siga siendo completamente interactivo y de que todas las integraciones del navegador funcionen automáticamente.

Esto es lo que obtienes si permites que el DOM controle tu IU dentro de un elemento <canvas>:

  • Diseño y formato de texto: Diseño y formato de texto simplificados, incluido texto bidireccional o de varias líneas con estilos CSS aplicados.
  • Controles de formularios: Controles de formularios expresivos y más fáciles de usar con amplias opciones de personalización.
  • Selección de texto, copiar y pegar, y clic con el botón derecho: Los usuarios pueden destacar texto dentro de tus escenas en 3D o hacer clic con el botón derecho en los menús contextuales de forma nativa.
  • Selección de texto, copiar y pegar, y clic con el botón derecho: Los usuarios pueden destacar texto dentro de tus escenas en 3D o hacer clic con el botón derecho en los menús contextuales de forma nativa.
  • Accesibilidad: El contenido renderizado dentro del lienzo se expone al árbol de accesibilidad. Los sistemas de accesibilidad pueden analizar la IU como lo hacen con el HTML normal y exponerla a sistemas como los lectores de pantalla.
  • Find-in-page: Los usuarios pueden usar la función de buscar en la página (Ctrl/Cmd + F) para buscar texto, y el navegador lo destacará directamente en tus texturas de WebGL.
  • Find-in-page: Los usuarios pueden usar la función de buscar en la página (Ctrl/Cmd + F) para buscar texto, y el navegador lo destacará directamente en tus texturas de WebGL.
  • Indexabilidad y compatibilidad con agentes de IA: Los rastreadores web y los agentes de IA pueden indexar y leer sin problemas el texto renderizado en tus escenas en 2D y 3D.
  • Integración de extensiones: Las extensiones del navegador funcionan de forma nativa. Por ejemplo, una extensión de reemplazo de texto actualizará automáticamente el texto renderizado en tus mallas 3D.
  • Integración de DevTools: Puedes inspeccionar el contenido de tu lienzo, incluidos los elementos de la IU de WebGL/WebGPU, directamente en las Herramientas para desarrolladores de Chrome. Ajusta un estilo CSS en el inspector y observa cómo se actualiza al instante en la textura 3D.

Casos de uso de alto nivel

Esta API desbloquea un potencial increíble en varios dominios:

  • Aplicaciones basadas en un lienzo grande: Las apps web pesadas, como Documentos de Google, Miro o Figma, ahora pueden renderizar componentes complejos de la IU de la aplicación de forma nativa en sus espacios de trabajo basados en un lienzo, lo que mejora la accesibilidad y reduce el peso del paquete.
  • Escenas y juegos en 3D: Los sitios de marketing, las experiencias inmersivas de WebXR y los juegos web ahora pueden colocar IU web completamente interactivas en escenas en 3D, como un libro en 3D que usa texto DOM real o una terminal en el juego que admite copiar y pegar de forma nativa.

Cómo usar la API

El uso de la API se realiza en tres fases: configurar el lienzo, renderizar en el lienzo y actualizar la transformación CSS para que el navegador sepa dónde se encuentra físicamente el elemento en la pantalla.

Requisitos previos

La API de HTML-in-Canvas se encuentra en la prueba de origen en Chrome 148 a 150. Para probarlo en tu sitio, usa Chrome Canary 149 o versiones posteriores con la marca chrome://flags/#canvas-draw-element habilitada. Para habilitar la API para otros usuarios, regístrate en la prueba de origen.

Paso 1: Configuración básica de Canvas

Primero, agrega el atributo layoutsubtree a tu etiqueta <canvas>. Esto hace que el navegador conozca el contenido anidado dentro del lienzo, lo prepara para que se muestre dentro del lienzo y lo expone a los árboles de accesibilidad.

<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
  <div id="form_element">
    <label for="name">Name:</label> <input id="name" type="text">
  </div>
</canvas>

Cómo ajustar el tamaño de la cuadrícula del lienzo

Para evitar que el contenido renderizado se vea borroso, asegúrate de que el tamaño de la cuadrícula del lienzo coincida con el factor de escala del dispositivo.

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
  canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

Paso 2: Renderización

Para un contexto 2D, usa el método drawElementImage. Hazlo dentro del evento paint, que se activa cada vez que se vuelve a dibujar el elemento, por ejemplo, durante el resaltado de texto o la entrada del usuario. Es fundamental actualizar la transformación CSS del elemento con el valor de retorno para que la interactividad siga funcionando.

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

canvas.onpaint = () => {
  ctx.reset();

  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Use the transform returned later on...
};

Renderiza con WebGL

En el caso de WebGL, usa texElementImage2D. Funciona de manera similar a texImage2D, pero toma el elemento DOM como fuente.

canvas.onpaint = () => {
  if (gl.texElementImage2D) {
    gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
  }
};

Renderiza con WebGPU

WebGPU usa el método copyElementImageToTexture en la fila del dispositivo, de forma análoga a copyExternalImageToTexture:

canvas.onpaint = () => {
  root.device.queue.copyElementImageToTexture(
    valueElement,
    { texture: targetTexture }
  );
};

Paso 3: Actualiza la transformación CSS

Ahora que renderizaste el elemento en el lienzo, deberás actualizar el navegador sobre su ubicación. Esto garantiza la sincronización espacial entre el lienzo y el diseño del DOM. Esto es importante para que el navegador pueda asignar correctamente la zona del evento (por ejemplo, dónde exactamente hace clic o se desplaza el usuario) con el lugar donde se renderiza el elemento.

En el caso del contexto 2D, aplica la transformación que devuelve la llamada de renderización al .style.transform property:

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

canvas.onpaint = () => {
  ctx.reset();
  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Sync the DOM location with the drawn location
  form_element.style.transform = transform.toString();
};

Con WebGL o WebGPU, la ubicación en pantalla de un elemento depende de cómo el código del sombreador usa la textura de salida y no se puede deducir del contexto de renderización del lienzo. Sin embargo, si tu programa de sombreador usa una proyección típica de vista del modelo para dibujar la textura, puedes usar la nueva función de conveniencia element.getElementTransform() para calcular una transformación que se puede usar de la misma manera que el valor de devolución de drawElementImage(). Para facilitar esto, haz lo siguiente:

  • Convierte la matriz de MVP de WebGL en una matriz de DOM.
  • Normaliza el elemento HTML. Los elementos HTML se dimensionan en píxeles (por ejemplo, 200 px de ancho). Sin embargo, WebGL suele tratar los objetos como "cuadrados unitarios", por ejemplo, que van de 0 a 1. Si no lo haces, tu botón de 200 px se verá 200 veces más grande.
  • Asigna el mapa al viewport del lienzo. Este paso es la fase de "reescalado": estira ese cálculo del espacio de unidades para que coincida con las dimensiones de píxeles reales de tu elemento <canvas> en la pantalla. También invierte el eje Y, ya que, en WebGL, la dirección hacia arriba es positiva, pero en CSS, la dirección hacia abajo es positiva.
  • Calcula la transformación final. Multiplica las matrices en orden: Viewport * MVP * Normalization. Combinarlas en una transformación final produce un "mapa" que le indica al navegador exactamente dónde debe ubicarse esa capa de elementos HTML para alinearse con el dibujo en 3D.
  • Aplica la transformación al elemento HTML. Esto hace que la capa de elementos HTML se ubique directamente sobre sus píxeles renderizados. Esto garantiza que, cuando un usuario haga clic en un botón o seleccione texto, esté presionando el elemento HTML real.
if (canvas.getElementTransform) {
  // 1. Convert WebGL MVP Matrix to DOM Matrix
  const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));

  // 2. Normalize the HTML element (pixels -> 1x1 unit square)
  const width = targetHTMLElement.offsetWidth;
  const height = targetHTMLElement.offsetHeight;

  const cssToUnitSpace = new DOMMatrix()
    .scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
    .translate(-width / 2, -height / 2); // Center the element

  // 3. Map to the canvas viewport
  const clipToCanvasViewport = new DOMMatrix()
    .translate(canvas.width / 2, canvas.height / 2) // Move origin to center
    .scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions

  // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
  const screenSpaceTransform = clipToCanvasViewport
      .multiply(mvpDOM)
      .multiply(cssToUnitSpace);

  // 5. Apply to the transform
  const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
  if (computedTransform) {
    targetHTMLElement.style.transform = computedTransform.toString();
  }
}

Compatibilidad con bibliotecas y frameworks

Algunas de las bibliotecas populares ya incluyen compatibilidad con la función HTML-in-Canvas.

Three.js

Actualizar las matrices de forma manual puede ser tedioso, por lo que los frameworks ya se están sumando a esta tendencia. Three.js tiene compatibilidad experimental con el nuevo THREE.HTMLTexture:

const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

PlayCanvas

PlayCanvas también admite HTML en Canvas con su API de texturas:

// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
    htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();

// Keep up to date
canvas.addEventListener('paint', onPaintUpload);

const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();

Demostraciones

Antes de probar las demostraciones, asegúrate de que tu entorno esté configurado correctamente.

Hay varias demostraciones que sirven como referencia para usar la API. Ya vemos soluciones creativas de la comunidad, desde libros en 3D traducibles hasta elementos de la IU que se refractan a través de sombreadores de vidrio:

  • El libro en 3D: Un libro en 3D renderizado con WebGL que usa el diseño HTML para sus páginas. Los usuarios pueden intercambiar fuentes con CSS. Debido a que se basa en el DOM, la traducción integrada funciona de inmediato y los agentes de IA pueden extraer el texto con menos complejidad.
  • IU interactivas en 3D: Un control deslizante de gelatina de WebGPU que refracta la luz según un modelo 3D subyacente y, al mismo tiempo, responde a los atributos de paso <input type="range"> estándar de HTML.
  • Texturas animadas: Una valla publicitaria 3D dinámica que renderiza un lápiz SVG animado usando el DOM directamente en una textura WebGL sin necesidad de un bucle de animación personalizado.
  • Superposiciones refractivas: Una capa de tipografía interactiva distorsionada por un cursor 3D en movimiento, pero completamente seleccionable y apta para búsquedas con la función de búsqueda en la página.

Consulta la colección de demostraciones creadas por la comunidad. Si quieres que tu demostración de HTML en Canvas aparezca en esta colección, crea una solicitud de extracción para agregarla.

Limitaciones

Si bien es potente, la API tiene algunas limitaciones deliberadas:

  • Contenido de origen cruzado: Por razones de seguridad y privacidad, la API no funciona con contenido de iframe de origen cruzado.
  • Desplazamiento del subproceso principal: El HTML en Canvas se dibuja con JavaScript, lo que significa que el desplazamiento y las animaciones no se pueden actualizar de forma independiente de JavaScript, como sí pueden hacerlo fuera de Canvas. Los desarrolladores deben considerar cuidadosamente las características de rendimiento de colocar contenido desplazable dentro del lienzo en comparación con tener todo el lienzo desplazable.

Comentarios

Si estás experimentando con la API de HTML-in-Canvas, queremos conocer tu opinión. Puedes registrarte en la prueba de origen para habilitar la función en tu sitio mientras se encuentra en la fase experimental y ayudarnos a definir el diseño de la API. También puedes informar un problema para proporcionar comentarios.

Recursos