Apresentação do teste de origem da API HTML no Canvas

Thomas Nattestad
Thomas Nattestad

Por anos, os desenvolvedores da Web tiveram que fazer uma escolha arquitetônica difícil ao criar aplicativos visuais complexos e altamente interativos na Web. Você deve usar o DOM pelos recursos semânticos avançados ou renderizar diretamente no elemento <canvas> para ter um desempenho gráfico de baixo nível?

Com a nova API HTML-in-Canvas experimental, já disponível em teste de origem, você não precisa escolher. Essa API permite desenhar conteúdo DOM diretamente em uma tela 2D ou em uma textura WebGL/WebGPU, mantendo a capacidade de interação e acessibilidade da interface, além de estar conectada aos seus recursos favoritos do navegador. Ao combinar HTML com processamento de gráficos de baixo nível, é possível criar experiências que antes eram impossíveis.

O DOM e o Canvas

Para entender o poder dessa nova API, é útil analisar os pontos fortes relativos do DOM e do Canvas.

O DOM é o elemento básico da interface da Web. Ele oferece soluções de layout de texto prontas para uso, usando conteúdo semanticamente compreendido para criar interfaces avançadas. Isso permite que os usuários realizem operações comuns em páginas da Web sem problemas, como destacar texto para copiar ou clicar com o botão direito em uma imagem para salvar. O DOM também se integra a recursos essenciais do navegador: ferramentas de acessibilidade, tradução, localizar na página, modo de leitura, extensões, modo escuro, zoom do navegador e preenchimento automático.

Por outro lado, o Canvas (e o WebGL/WebGPU) permite acesso de baixo nível para acionar uma grade de pixels em gráficos 2D e 3D altamente avançados. Jogos e apps da Web complexos (como o Google Docs ou o Figma) exigem esse acesso eficiente e de baixo nível. Como o canvas é fundamentalmente uma grade de pixels, o suporte a recursos como texto responsivo exigia uma lógica de interface personalizada complexa, aumentando drasticamente o tamanho do pacote. É importante lembrar que todos os recursos avançados do navegador integrados ao DOM são completamente interrompidos quando a interface fica presa em uma grade de pixels estática da tela.

As vantagens de trazer o DOM para o Canvas

A API HTML-in-Canvas é a ponte que oferece o melhor dos dois mundos. Ao colocar HTML dentro do elemento <canvas> e sincronizar a transformação dele, você garante que o conteúdo permaneça totalmente interativo e que todas as integrações do navegador funcionem automaticamente.

Ao deixar o DOM processar sua interface dentro de um elemento <canvas>, você tem o seguinte:

  • Layout e formatação de texto: layout e formatação de texto simplificados, incluindo texto multilinha ou bidirecional com estilos CSS aplicados.
  • Controles de formulário: controles de formulário expressivos e mais fáceis de usar com várias opções de personalização.
  • Seleção, cópia/colagem e clique com o botão direito do mouse: os usuários podem destacar texto nas cenas 3D ou clicar com o botão direito do mouse nos menus de contexto de forma nativa.
  • Acessibilidade: o conteúdo renderizado no canvas é exposto à árvore de acessibilidade. Os sistemas de acessibilidade podem analisar a interface da mesma forma que o HTML normal e expô-la a sistemas como leitores de tela.
  • Encontrar na página: os usuários podem usar o recurso "Encontrar na página" (Ctrl/Cmd+F) para pesquisar texto, e o navegador o destaca diretamente nas texturas WebGL.
  • Indexabilidade e interface com agentes de IA: os rastreadores da Web e os agentes de IA podem indexar e ler o texto renderizado nas suas cenas 2D e 3D sem problemas.
  • Integração de extensões: as extensões do navegador funcionam de forma nativa. Por exemplo, uma extensão de substituição de texto atualiza automaticamente o texto renderizado nas suas malhas 3D.
  • Integração do DevTools: você pode inspecionar o conteúdo da tela, incluindo elementos de interface do WebGL/WebGPU, diretamente no Chrome DevTools. Ajuste um estilo CSS no inspetor e veja a atualização instantânea na textura 3D.

Casos de uso de alto nível

Essa API libera um potencial incrível em vários domínios:

  • Aplicativos grandes baseados em tela: agora os web apps pesados podem renderizar componentes complexos de interface do aplicativo de forma nativa nos espaços de trabalho baseados em tela, melhorando a acessibilidade e reduzindo o peso do pacote.
  • Cenas e jogos em 3D: sites de marketing, experiências imersivas do WebXR e jogos da Web agora podem colocar uma interface da Web totalmente interativa em cenas 3D, como um livro 3D que usa texto DOM real ou um terminal no jogo que oferece suporte nativo para copiar e colar.

Como usar a API

O uso da API acontece em três fases: configuração da tela, renderização na tela e atualização da transformação CSS para que o navegador saiba onde o elemento está fisicamente na tela.

Pré-requisitos

A API HTML-in-Canvas está em teste de origem no Chrome 148 a 150. Para testar no seu site, use o Chrome Canary 149 ou mais recente com a flag chrome://flags/#canvas-draw-element ativada. Para ativar a API para outros usuários, inscreva-se no teste de origem.

Etapa 1: configuração básica da tela

Primeiro, adicione o atributo layoutsubtree à tag <canvas>. Isso faz com que o navegador reconheça o conteúdo aninhado dentro da tela, preparando-o para ser exibido dentro dela e expondo-o a árvores de acessibilidade.

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

Dimensionar a grade da tela

Para evitar o desfoque do conteúdo renderizado, ajuste o tamanho da grade do canvas para corresponder ao fator de escala do 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);

Etapa 2: renderização

Para um contexto 2D, use o método drawElementImage. Faça isso dentro do evento paint, que é acionado sempre que o elemento é renderizado novamente, por exemplo, durante o destaque de texto ou a entrada do usuário. É fundamental atualizar a transformação CSS do elemento com o valor de retorno para que a interatividade continue 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...
};

Renderizar com WebGL

Para WebGL, use texElementImage2D. Ela funciona de maneira semelhante a texImage2D, mas usa o elemento DOM como origem.

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

Renderizar com WebGPU

A WebGPU usa o método copyElementImageToTexture na fila do dispositivo, análogo a copyExternalImageToTexture:

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

Etapa 3: atualizar a transformação CSS

Agora que você renderizou o elemento na tela, é necessário atualizar o navegador sobre onde ele está localizado. Isso garante a sincronização espacial entre a tela e o layout do DOM. Isso é importante para que o navegador possa mapear corretamente a zona de eventos, como onde exatamente o usuário clica ou passa o cursor, com onde o elemento é renderizado.

Para o caso de contexto 2D, aplique a transformação retornada pela chamada de renderização ao .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();
};

Com o WebGL ou o WebGPU, a localização na tela de um elemento depende de como a textura de saída é usada pelo código do shader e não pode ser deduzida do contexto de renderização da tela. No entanto, se o programa de shader usar uma projeção de visualização de modelo típica para desenhar a textura, use a nova função de conveniência element.getElementTransform() para calcular uma transformação que pode ser usada da mesma forma que o valor de retorno de drawElementImage(). Para facilitar isso, faça o seguinte:

  • Converter a matriz de MVP do WebGL em uma matriz DOM.
  • Normalizar o elemento HTML. Os elementos HTML são dimensionados em pixels (por exemplo, 200 px de largura). No entanto, o WebGL geralmente trata os objetos como "quadrados unitários", por exemplo, variando de 0 a 1. Se você não normalizar, o botão de 200 px vai parecer 200 vezes maior.
  • Mapear para a janela de visualização da tela. Essa etapa é a fase de "readequação de escala". Ele estica esse cálculo de espaço da unidade para corresponder às dimensões reais de pixel do elemento <canvas> na tela. Ele também inverte o eixo Y, porque no WebGL, para cima é positivo, mas no CSS, para baixo é positivo.
  • Calcule a transformação final. Multiplique as matrizes na ordem: Viewport * MVP * Normalization.. A combinação delas em uma transformação final produz um "mapa" que informa ao navegador exatamente onde a camada de elemento HTML deve ficar para se alinhar ao desenho 3D.
  • Aplique a transformação ao elemento HTML. Isso move a camada de elementos HTML para ficar diretamente acima dos pixels renderizados. Isso garante que, quando um usuário clicar em um botão ou selecionar um texto, ele vai atingir o 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();
  }
}

Suporte a bibliotecas e frameworks

Algumas das bibliotecas mais usadas já oferecem suporte ao recurso HTML na tela.

Three.js

Atualizar matrizes manualmente pode ser cansativo. Por isso, os frameworks já estão entrando nessa onda. O Three.js tem suporte experimental usando o novo 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

O PlayCanvas também é compatível com HTML-in-Canvas usando a 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();

Demonstrações

Antes de testar as demonstrações, verifique se o ambiente está configurado corretamente.

Há várias demonstrações que servem como referência para usar a API. Já estamos vendo soluções criativas da comunidade, desde livros 3D traduzíveis até elementos de interface que refratam por shaders de vidro:

  • O livro 3D: um livro 3D renderizado em WebGL que usa layout HTML para as páginas. Os usuários podem trocar fontes com CSS. Como é baseada no DOM, a tradução integrada funciona instantaneamente, e os agentes de IA podem extrair o texto com menos complexidade.
  • Interfaces 3D interativas: um controle deslizante de gelatina WebGPU que refrata a luz com base em um modelo 3D subjacente, mas ainda responde aos atributos de etapa <input type="range"> HTML padrão.
  • Texturas animadas: um outdoor 3D dinâmico renderizando um lápis SVG animado usando o DOM diretamente em uma textura WebGL sem precisar de um loop de animação personalizado.
  • Sobreposições refrativas: uma camada de tipografia interativa distorcida por um cursor 3D em movimento, mas totalmente selecionável e pesquisável usando o recurso "Encontrar na página".

Confira a coleção de demonstrações criadas pela comunidade. Se quiser que sua demonstração de HTML no Canvas apareça nesta coleção, crie uma solicitação de pull para adicioná-la.

Limitações

Embora seja eficiente, a API tem algumas limitações conscientes:

  • Conteúdo entre origens: por motivos de segurança e privacidade, a API não funciona com conteúdo de iframe entre origens.
  • Rolagem da linha de execução principal: o HTML no canvas é desenhado com JavaScript, o que significa que a rolagem e as animações não podem ser atualizadas de forma independente do JavaScript, como acontece fora do canvas. Os desenvolvedores precisam considerar cuidadosamente as características de desempenho de colocar conteúdo rolável dentro do canvas em vez de rolar o canvas inteiro.

Feedback

Se você estiver testando a API HTML-in-Canvas, queremos saber sua opinião. Você pode se inscrever no teste de origem para ativar o recurso no seu site enquanto ele está na fase experimental e nos ajudar a moldar o design da API. Você também pode registrar um problema para enviar feedback.

Recursos