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 decisión arquitectónica difícil cuando compilaban aplicaciones visuales complejas y altamente interactivas en la Web: ¿se apoyan en el DOM por sus funciones semánticas enriquecidas o renderizan directamente en el elemento <canvas> para obtener un rendimiento de gráficos de bajo nivel?

Con la nueva API de HTML-in-Canvas experimental, que ya está disponible en la prueba de origen, no tienes que elegir. Esta API te permite dibujar contenido DOM directamente en un lienzo 2D o una textura WebGL/WebGPU mientras mantienes 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 Canvas.

El DOM es el elemento básico de la IU web. Ofrece soluciones de diseño de texto listas para usar, con contenido semánticamente comprendido 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 sentadas, 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.

Canvas (y WebGL/WebGPU), por otro lado, 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 Google Docs o Figma) requieren este acceso de bajo nivel y de alto rendimiento. Debido a que el lienzo es fundamentalmente una cuadrícula de píxeles, las funciones compatibles, como el texto responsivo, solían requerir una lógica de IU personalizada compleja, lo que aumentaba drásticamente el tamaño del paquete. Es fundamental que todas las potentes funciones del navegador integradas en el DOM se interrumpan por completo cuando la IU queda atrapada dentro de una cuadrícula de píxeles de lienzo estático.

Las ventajas de llevar el DOM a Canvas

La API de HTML-in-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 multilínea o bidireccional con estilos CSS aplicados.
  • Controles de formulario: Controles de formulario expresivos y más fáciles de usar con amplias opciones de personalización.
  • Selección de texto, copiar/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/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 lectores de pantalla.
  • Find-in-page: Los usuarios pueden usar la búsqueda en la página (Ctrl/Cmd+F) para buscar texto, y el navegador lo destacará directamente dentro de tus texturas WebGL.
  • Find-in-page: Los usuarios pueden usar la búsqueda en la página (Ctrl/Cmd+F) para buscar texto, y el navegador lo destacará directamente dentro de tus texturas WebGL.
  • Indexabilidad y capacidad de interfaz de agentes de IA: Los rastreadores web y los agentes de IA pueden indexar y leer sin problemas el texto renderizado en tus escenas 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 Herramientas para desarrolladores: Puedes inspeccionar el contenido del 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 instantáneamente en la textura 3D.

Casos de uso de alto nivel

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

  • Aplicaciones grandes basadas en lienzo: Las apps web pesadas, como Google Docs, 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 lienzo, lo que mejora la accesibilidad y reduce el peso del paquete.
  • Escenas y juegos en 3D: Los sitios de marketing, las experiencias envolventes de WebXR y los juegos web ahora pueden colocar una IU web completamente interactiva en escenas en 3D, como un libro en 3D que usa texto DOM real o una terminal en el juego que admite de forma nativa copiar y pegar.

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 está en la prueba de origen en Chrome 148 a 150. Para probarla 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>

Ajusta el tamaño de la cuadrícula del lienzo

Para evitar que el contenido renderizado se vea borroso, asegúrate de ajustar el tamaño de la cuadrícula del lienzo para que 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 el elemento vuelve a dibujar, 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

Para WebGL, usas 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 cola 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 de eventos, como dónde hace clic o coloca el cursor el usuario, con el lugar donde se renderiza el elemento.

En el caso del contexto 2D, aplica la transformación que muestra la llamada de renderización a la .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 de vista de modelo típica para dibujar la textura, puedes usar la nueva función de conveniencia element.getElementTransform() para calcular una transformación que se pueda usar de la misma manera que el valor de retorno de drawElementImage(). Para facilitar esto, debes hacer lo siguiente:

  • Convierte la matriz WebGL MVP a la matriz DOM.
  • Normaliza el elemento HTML. Los elementos HTML tienen un tamaño en píxeles (por ejemplo, 200 px de ancho). Sin embargo, WebGL suele tratar los objetos como "cuadrados de unidad", por ejemplo, que van de 0 a 1. Si no normalizas, el botón de 200 px se verá 200 veces más grande.
  • Asigna al viewport del lienzo. Este paso es la fase de "cambio de escala": extiende esa matemática de espacio de unidad para que coincida con las dimensiones reales de píxeles de tu <canvas> elemento en la pantalla. También invierte el eje Y, ya que, en WebGL, hacia arriba es positivo, pero en CSS, hacia abajo es positivo.
  • Calcula la transformación final. Multiplica las matrices en orden: Viewport * MVP * Normalization. Si las combinas en una transformación final, se produce un "mapa" que le indica al navegador exactamente dónde debe ubicarse esa capa de elementos HTML para alinearse con el dibujo 3D.
  • Aplica la transformación al elemento HTML. Esto mueve la capa de elementos HTML para que se ubique directamente sobre sus píxeles renderizados. De esta manera, se garantiza que, cuando un usuario haga clic en un botón o seleccione texto, presione 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 admiten 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 uniendo. Three.js tiene compatibilidad experimental con la nueva 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-in-Canvas con su API de textura:

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

Existen varias demostraciones que sirven como referencia para usar la API. Ya estamos viendo 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 diseño HTML para sus páginas. Los usuarios pueden intercambiar fuentes con CSS. Debido a que está basado en DOM, la traducción integrada funciona al instante y los agentes de IA pueden extraer el texto con menos complejidad.
  • UIs interactivas en 3D: Un control deslizante de gelatina WebGPU que refracta la luz según un modelo 3D subyacente, mientras responde a los atributos de paso HTML estándar <input type="range">.
  • Texturas animadas: Una valla publicitaria dinámica en 3D que renderiza un lápiz SVG animado con 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 con capacidad de búsqueda mediante la búsqueda en la página.

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

Limitaciones

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

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

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 está en la fase experimental para ayudarnos a definir el diseño de la API. También puedes registrar un problema para proporcionar comentarios.

Recursos