Durante años, los desarrolladores web han tenido que tomar una decisión arquitectónica difícil cuando crean aplicaciones visuales complejas y altamente interactivas en la Web. ¿Deberías apoyarte en el DOM por sus ricas funciones semánticas o renderizar directamente en el elemento <canvas> para obtener un rendimiento gráfico 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 del DOM directamente en un lienzo 2D o 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 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 del lienzo.
El DOM es el elemento básico de la IU web. Ofrece soluciones de diseño de texto listas para usar, con contenido que se comprende semánticamente para crear interfaces enriquecidas. Esto permite que los usuarios realicen operaciones comunes en las páginas web sin problemas, 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 autocompletado.
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. Debido a que el lienzo es, básicamente, una cuadrícula de píxeles, la compatibilidad con funciones como el texto responsivo solía 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 rompen por completo cuando la IU queda atrapada dentro de una cuadrícula de píxeles de lienzo estática.
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 cuando 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 el texto de varias líneas o bidireccional 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/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.
- Buscar en la página: Los usuarios pueden usar la función de búsqueda en la página (Ctrl/Cmd+F) para buscar texto, y el navegador lo destaca 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 actualiza automáticamente el texto renderizado en tus mallas 3D.
- Integración de las Herramientas para desarrolladores: Puedes inspeccionar el contenido de tu lienzo, incluidos los elementos de la IU de WebGL/WebGPU, directamente en las Herramientas para desarrolladores de Chrome. Modifica un estilo de CSS en el inspector y observa cómo se actualiza al instante en la textura 3D.
Casos de uso de alto nivel
Esta API ofrece un potencial increíble en varios dominios:
- Aplicaciones grandes basadas en lienzos: Las apps web pesadas ahora pueden renderizar componentes complejos de la IU de la aplicación de forma nativa en sus espacios de trabajo basados en lienzos, 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 de navegador ahora pueden colocar una IU web completamente interactiva en escenas en 3D, como un libro en 3D que usa texto del 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 de 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 prueba de origen en Chrome 148 a 150. Para probarlo en tu sitio, usa Chrome Canary 149 o una versión posterior con el parámetro de configuración chrome://flags/#canvas-draw-element habilitado. 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 prepare para mostrarlo dentro del lienzo y lo exponga 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 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 se vuelve a dibujar el elemento, por ejemplo, durante el resaltado de texto o la entrada del usuario. Es fundamental actualizar la transformación de CSS del elemento con el valor que se muestra 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, usa texElementImage2D. Funciona de manera similar a texImage2D, pero toma el elemento del 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 similar a copyExternalImageToTexture:
canvas.onpaint = () => {
root.device.queue.copyElementImageToTexture(
valueElement,
{ texture: targetTexture }
);
};
Paso 3: Actualiza la transformación de CSS
Ahora que renderizaste el elemento en el lienzo, debes actualizar el navegador para que sepa dónde se encuentra. 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, como el lugar exacto en el que el usuario hace clic o coloca el cursor, con el lugar en el que 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 use la textura de salida y no se puede deducir del contexto de renderización del lienzo. Sin embargo, si tu programa de sombreadores 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 que se muestra de drawElementImage(). Para facilitar este proceso, debes hacer lo siguiente:
- Convierte la matriz de MVP de WebGL en una matriz de DOM.
- Normaliza el elemento HTML. Los elementos HTML se miden en píxeles (por ejemplo, 200 px de ancho). Sin embargo, WebGL suele tratar los objetos como "cuadrados unitarios", por ejemplo, con un rango de 0 a 1. Si no lo haces, el 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 "cambio de escala". Estira ese cálculo matemático del espacio de unidades para que coincida con las dimensiones reales en píxeles de tu elemento
<canvas>en la pantalla. También invierte el eje Y, porque 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.. 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 mueve la capa del elemento HTML para que se ubique directamente sobre sus píxeles renderizados. Esto 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 incluyen compatibilidad con la función de HTML en Canvas.
Three.js
Actualizar las matrices de forma manual puede ser tedioso, por lo que los frameworks ya se están sumando. 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-in-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 estamos viendo soluciones creativas de la comunidad, que van 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 un diseño HTML para sus páginas. Los usuarios pueden intercambiar fuentes con CSS. Como se basa en el DOM, la traducción integrada funciona de forma instantánea y los agentes de IA pueden extraer el texto con menos complejidad.
- IUs en 3D interactivas: Un control deslizante de gelatina de WebGPU que refracta la luz según un modelo en 3D subyacente, a la vez que responde a los atributos de paso
<input type="range">de HTML estándar. - Texturas animadas: Un cartel dinámico en 3D que renderiza un lápiz SVG animado con el DOM directamente en una textura de WebGL sin necesidad de un bucle de animación personalizado.
- Superposiciones refractivas: Es una capa de tipografía interactiva distorsionada por un cursor 3D en movimiento, pero que se puede seleccionar y buscar por completo con la función de búsqueda en la página.
Consulta la colección de demostraciones que creó 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: El HTML en el lienzo se dibuja con JavaScript, lo que significa que el desplazamiento y las animaciones no se pueden actualizar de forma independiente de JavaScript, como sí se puede hacer fuera del lienzo. 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 está en la fase experimental y ayudarnos a dar forma al diseño de la API. También puedes presentar un problema para proporcionar comentarios.
Recursos
- Compatibilidad con HTML-in-Canvas en Three.js
- Demostración de HTML-in-Canvas en Three.js
- Compatibilidad con HTML-in-Canvas en PlayCanvas: Documentación para desarrolladores
- Demostración de HTML-in-Canvas en PlayCanvas
- HTML-in-Canvas: Explicación
- Modern Web Guidance para herramientas de programación con IA para HTML-in-Canvas
- Demostraciones de Chrome.dev para HTML-in-Canvas
- Increíble colección de demostraciones de HTML-in-Canvas de la comunidad