ID: 0x

|

DATE:

,

HTML-in-Canvas: renderizando HTML dentro do canvas sem perder interatividade

AUTHOR:

|

READ_TIME: ~5 MIN

Thumbnail do post HTML-in-Canvas com formulário HTML renderizado como textura em um cubo 3D no canvas

Recentemente revisitei meu post de 2010 sobre Canvas HTML5 e a coincidência foi boa. O Chrome acaba de colocar em origin trial a HTML-in-Canvas API, uma das novidades mais interessantes para quem trabalha com gráficos na web. Ela resolve um problema que existe desde que o canvas nasceu: tudo que desenhamos nele vira pixel. Ou seja, sem seleção de texto, sem acessibilidade e sem os recursos que o navegador oferece de graça para o DOM.

Neste post vou explicar o que é a API, como ela funciona e construir exemplos de forma progressiva, do “hello world” até o uso com WebGL e Three.js.

O dilema: DOM ou canvas?

Quem constrói aplicações gráficas na web sempre teve que escolher entre dois mundos:

  • DOM: rico em semântica, acessível, com seleção de texto, formulários, find-in-page (Ctrl+F) e integração com extensões. Mas com performance limitada para cenas gráficas complexas.
  • Canvas (2D, WebGL, WebGPU): performance excelente para jogos, editores e visualizações, mas tudo vira bitmap. Um botão desenhado no canvas não é um botão, é um desenho de botão.

Ferramentas como Google Docs, Figma e Miro convivem com esse dilema diariamente. Elas renderizam a área de trabalho no canvas e, por isso, precisam recriar do zero comportamentos que o DOM daria de graça. Além disso, a alternativa clássica de sobrepor HTML por cima do canvas com posicionamento absoluto quebra a imersão em cenas 3D e vira um pesadelo de sincronização.

A HTML-in-Canvas API elimina essa troca: você desenha elementos HTML reais dentro do canvas e eles continuam interativos, acessíveis e conectados aos recursos do navegador.

As três primitivas do HTML-in-Canvas

A API se apoia em três peças que trabalham juntas:

  1. Atributo layoutsubtree: aplicado na tag <canvas>, permite que os elementos filhos participem do layout e do hit-testing (cliques e toques), mesmo ficando invisíveis até serem desenhados.
  2. Método drawElementImage(): desenha um elemento filho no contexto 2D do canvas, na posição indicada. Existem variantes para WebGL (texElementImage2D()) e WebGPU (copyElementImageToTexture()).
  3. Evento paint: disparado quando a renderização de um elemento filho muda (o usuário digitou num input, selecionou um texto), avisando que é hora de redesenhar.

Exemplo 1: o hello world

Vamos começar pelo caso mais simples: desenhar uma div dentro do canvas. Repare que o elemento fica como filho da tag <canvas>, algo que antes servia apenas como fallback:

<canvas id="meu-canvas" width="400" height="200" layoutsubtree>
  <div id="mensagem">
    <h1>Olá, HTML-in-Canvas!</h1>
    <p>Este texto é HTML de verdade, selecionável e acessível.</p>
  </div>
</canvas>

<script>
  const canvas = document.getElementById("meu-canvas");
  const ctx = canvas.getContext("2d");
  const mensagem = document.getElementById("mensagem");

  canvas.onpaint = () => {
    ctx.reset();
    ctx.drawElementImage(mensagem, 0, 0);
  };
</script>Code language: HTML, XML (xml)

O fluxo lembra o loop de animação clássico do canvas: limpamos o contexto com reset() e desenhamos o elemento com drawElementImage(elemento, x, y). A diferença é que o redesenho acontece dentro do evento paint, que o navegador dispara sempre que o conteúdo HTML muda.

O resultado na tela parece um desenho comum de canvas, mas o texto pode ser selecionado, copiado e encontrado com Ctrl+F, porque o DOM continua vivo por trás dos pixels.

Exemplo 2: sincronizando a interatividade

Aqui entra o detalhe mais importante da API. O elemento HTML existe no DOM, mas quem decide onde ele aparece é o seu código de desenho. Para que cliques e toques caiam no lugar certo, o drawElementImage() retorna um transform que devemos aplicar de volta no elemento:

<canvas id="meu-canvas" width="400" height="200" layoutsubtree>
  <div id="formulario">
    <label for="nome">Nome:</label>
    <input id="nome" type="text">
  </div>
</canvas>

<script>
  const canvas = document.getElementById("meu-canvas");
  const ctx = canvas.getContext("2d");
  const formulario = document.getElementById("formulario");

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

    // desenha o formulário na posição (100, 50)
    const transform = ctx.drawElementImage(formulario, 100, 50);

    // sincroniza a posição do DOM com a posição desenhada
    formulario.style.transform = transform.toString();
  };
</script>Code language: HTML, XML (xml)

Sem essa sincronização, o formulário seria desenhado em um lugar e a área clicável ficaria em outro. Com ela, o input funciona normalmente: foco, digitação, autofill e teclado virtual no mobile.

Exemplo 3: redesenho eficiente com changedElements

Quando o canvas tem vários elementos HTML, redesenhar tudo a cada mudança desperdiça processamento. O evento paint informa quais elementos mudaram através de e.changedElements:

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

  for (const elemento of e.changedElements) {
    ctx.drawElementImage(elemento, 0, 0);
  }
};Code language: JavaScript (javascript)

É o mesmo raciocínio de dirty checking que usamos em jogos e editores: só atualiza o que mudou.

Exemplo 4: nitidez em telas de alta densidade

No post de introdução ao Canvas eu comentei sobre o risco de distorção ao dimensionar por CSS. Com HTML dentro do canvas o cuidado precisa ser redobrado, porque texto borrado fica evidente. A recomendação oficial é ajustar a grade de pixels do canvas ao devicePixelRatio usando um ResizeObserver:

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 suportaDevicePixelContentBox =
  typeof ResizeObserverEntry !== "undefined" &&
  "devicePixelContentBoxSize" in ResizeObserverEntry.prototype;

observer.observe(
  canvas,
  suportaDevicePixelContentBox ? { box: "device-pixel-content-box" } : {}
);Code language: JavaScript (javascript)

Assim o canvas sempre desenha na resolução física da tela, e o HTML renderizado sai com a mesma nitidez de um elemento comum da página.

Exemplo 5: HTML como textura em WebGL e WebGPU

A parte que mais me impressionou: a API não se limita ao contexto 2D. Em WebGL, o método texElementImage2D() funciona como o texImage2D() tradicional, mas usando um elemento DOM como fonte da textura:

canvas.onpaint = () => {
  if (gl.texElementImage2D) {
    gl.texElementImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
      formulario
    );
  }
};Code language: JavaScript (javascript)

Em WebGPU, o equivalente é copyElementImageToTexture(), análogo ao copyExternalImageToTexture():

canvas.onpaint = () => {
  device.queue.copyElementImageToTexture(
    elemento,
    { texture: texturaDestino }
  );
};Code language: JavaScript (javascript)

Na prática isso significa um formulário funcional aplicado na superfície de um objeto 3D: o usuário rotaciona a cena, clica no botão que está “colado” no objeto e ele responde. Para cenas 3D, o método canvas.getElementTransform() calcula o transform do elemento a partir da matriz model-view-projection, mantendo a área de interação alinhada com a projeção do objeto.

Exemplo 6: Three.js e HTMLTexture

Se calcular matriz na mão não é o seu objetivo, os frameworks já começaram a integrar a API. O Three.js oferece a HTMLTexture, que recebe o elemento DOM diretamente como material:

const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement);

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);Code language: JavaScript (javascript)

O PlayCanvas segue caminho parecido, escutando o evento paint do canvas para atualizar a textura. A coleção de demos em chrome.dev/html-in-canvas mostra o potencial: livros 3D com layout HTML, vídeos em superfícies 3D, gráficos D3 acessíveis e formulários com efeito de vidro fosco, tudo selecionável e pesquisável.

Como testar o HTML-in-Canvas hoje

A API está em estágio experimental, então nada de produção por enquanto. As opções para experimentar:

  • Chrome Canary 149 ou superior: ative a flag chrome://flags/#canvas-draw-element.
  • Origin trial: disponível do Chrome 148 ao 150 para testar com usuários reais no seu domínio, mediante registro no programa de origin trials.

Como todo origin trial, os detalhes da implementação podem mudar. A especificação está sendo discutida em aberto no explainer do WICG.

O que ganhamos de graça

O ponto central da proposta é devolver ao canvas tudo que o DOM já oferece:

  • Acessibilidade: o conteúdo desenhado continua exposto na árvore de acessibilidade para leitores de tela.
  • Seleção e busca: textos podem ser selecionados, copiados e encontrados com Ctrl+F, mesmo dentro de uma textura WebGL.
  • SEO e indexação: crawlers e agentes de IA enxergam o conteúdo como HTML normal.
  • DevTools: dá para inspecionar e editar o conteúdo do canvas direto no inspetor.
  • Extensões: extensões do navegador continuam funcionando sobre o conteúdo renderizado.

Limitações e pegadinhas

  • Cross-origin: conteúdo de iframes de outras origens não pode ser desenhado, por segurança e privacidade. Sem exceções.
  • Scroll na main thread: rolagem de conteúdo dentro do canvas depende do JavaScript, sem o scroll assíncrono nativo do navegador. Cuidado com áreas roláveis grandes.
  • API em movimento: nomes de métodos e comportamentos podem mudar até a estabilização. Escreva código defensivo, como o if (gl.texElementImage2D) do exemplo 5.

Conclusão

O Canvas HTML5 nasceu em 2010 como uma tela de pixels isolada do resto da página. A HTML-in-Canvas API fecha esse ciclo: o canvas ganha acesso ao melhor do DOM sem abrir mão da performance gráfica. Para quem constrói editores, jogos com UI complexa ou experiências 3D imersivas, é uma mudança de arquitetura significativa: menos UI reimplementada na mão, mais navegador trabalhando a seu favor.

Vale acompanhar o origin trial e testar nos seus projetos. Referências para ir mais fundo:


ENCODING: UTF-8

|

CHMOD: 644

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

// RELATED_ENTRIES

NEXT_READS