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ê usa o DOM pelos recursos semânticos avançados ou renderiza diretamente no elemento <canvas> para ter desempenho gráfico de baixo nível?

Com a nova API HTML-in-Canvas experimental, disponível agora no teste de origem, você não precisa escolher. Essa API permite desenhar conteúdo do DOM diretamente em uma tela 2D ou em uma textura WebGL/WebGPU, mantendo a interface interativa, acessível e conectada aos seus recursos favoritos do navegador. Ao combinar HTML com processamento gráfico de baixo nível, você pode criar experiências que antes eram impossíveis.

O DOM versus o Canvas

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

O DOM é o principal elemento 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, coisas que muitas vezes consideramos normais, como destacar texto para copiar ou clicar com o botão direito do mouse em uma imagem para salvá-la. O DOM também se integra a recursos essenciais do navegador: ferramentas de acessibilidade, tradução, pesquisa na página, modo de leitura, extensões, modo escuro, zoom do navegador e preenchimento automático.

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

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, você garante que o conteúdo permaneça totalmente interativo e que todas as integrações do navegador funcionem automaticamente.

Confira o que você ganha ao permitir que o DOM processe sua interface dentro de um elemento <canvas>:

  • 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 muitas opções de personalização.
  • Seleção de texto, copiar/colar 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 nativamente.
  • Seleção de texto, copiar/colar 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 nativamente.
  • Acessibilidade:o conteúdo renderizado dentro da tela é exposto à árvore de acessibilidade. Os sistemas de acessibilidade podem analisar a interface como fazem com o HTML normal e expô-la a sistemas como leitores de tela.
  • Find-in-page: os usuários podem usar a pesquisa na página (Ctrl/Cmd+F) para buscar texto, e o navegador vai destacá-lo diretamente nas texturas WebGL.
  • Find-in-page: os usuários podem usar a pesquisa na página (Ctrl/Cmd+F) para buscar texto, e o navegador vai destacá-lo diretamente nas texturas WebGL.
  • Indexabilidade e interface de agente de IA:os rastreadores da Web e os agentes de IA podem indexar e ler o texto renderizado nas cenas 2D e 3D sem problemas.
  • Integração de extensões:as extensões do navegador funcionam nativamente. Por exemplo, uma extensão de substituição de texto vai atualizar automaticamente o texto renderizado nas malhas 3D.
  • Integração do DevTools:é possível 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:

  • Grandes aplicativos baseados em tela:apps da Web pesados, como o Google Docs, o Miro ou o Figma, agora podem renderizar componentes complexos de interface do aplicativo nativamente nos espaços de trabalho orientados por tela, melhorando a acessibilidade e reduzindo o peso do pacote.
  • Cenas e jogos 3D:sites de marketing, experiências imersivas de WebXR e jogos da Web agora podem colocar interfaces da Web totalmente interativas 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 ocorre em três fases: configurar a tela, renderizar na tela e atualizar a 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 layoutsubtree atributo à sua <canvas> tag. 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, dimensione a grade da tela 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 é redesenhado, 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. Ele 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

O 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, será necessário atualizar o navegador sobre a localização dele. 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 o local em que 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 WebGL ou WebGPU, a localização de um elemento na tela depende de como a textura de saída é usada pelo código do sombreador e não pode ser deduzida do contexto de renderização da tela. No entanto, se o programa de sombreador usar uma projeção de visualização de modelo típica para desenhar a textura, você poderá usar 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 WebGL MVP em 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 objetos como "quadrados de unidade", 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. Esta etapa é a fase de "redimensionamento": ela estende a matemática do espaço de unidade para corresponder às dimensões reais de pixels do elemento <canvas> na tela. Ele também inverte o eixo Y, porque no WebGL, para cima é positivo, mas no CSS, para baixo é positivo.
  • Calcular 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 elementos HTML precisa estar para se alinhar ao desenho 3D.
  • Aplicar a transformação ao elemento HTML. Isso move a camada de elementos HTML para ficar diretamente sobre os pixels renderizados. Isso garante que, quando um usuário clica em um botão ou seleciona texto, ele está atingindo 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 conhecidas já oferecem suporte ao recurso HTML-in-Canvas.

Three.js

A atualização manual de matrizes pode ser tediosa, e é por isso que os frameworks já estão aderindo. 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 oferece suporte ao 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.

várias demonstrações que servem como referência para o uso da API. Já estamos vendo soluções criativas da comunidade, desde livros 3D traduzíveis até elementos de interface que refratam por sombreadores 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 é baseado em 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, enquanto ainda responde aos atributos de etapa HTML padrão <input type="range">.
  • Texturas animadas: um outdoor 3D dinâmico que renderiza 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 a pesquisa in-page.

Confira a coleção de demonstrações criadas pela comunidade. Se você quiser que sua demonstração de HTML-in-Canvas seja exibida nessa 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 de origem cruzada:por motivos de segurança e privacidade, a API não funciona com conteúdo de iframe de origem cruzada.
  • Rolagem da linha de execução principal:o HTML-in-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 podem fora da tela. Os desenvolvedores precisam considerar cuidadosamente as características de desempenho de colocar conteúdo de rolagem dentro da tela em vez de rolar a tela inteira.

Feedback

Se você estiver fazendo experimentos com a API HTML-in-Canvas, queremos ouvir você. Inscreva-se no teste de origem para ativar o recurso no seu site enquanto ele estiver na fase experimental para nos ajudar a moldar o design da API. Você também pode registrar um problema para enviar feedback.

Recursos