Présentation de la phase d'évaluation de l'API HTML-in-Canvas

Thomas Nattestad
Thomas Nattestad

Pendant des années, les développeurs Web ont dû faire un choix architectural difficile lors de la création d'applications visuelles complexes et hautement interactives sur le Web : s'appuyer sur le DOM pour ses riches fonctionnalités sémantiques ou effectuer le rendu directement dans l'élément <canvas> pour des performances graphiques de bas niveau ?

Avec la nouvelle API HTML-in-Canvas expérimentale, désormais disponible en phase d’évaluation, vous n’avez plus à choisir. Cette API vous permet de dessiner du contenu DOM directement dans une toile 2D ou une texture WebGL/WebGPU tout en conservant l'interactivité et l'accessibilité de l'UI, et en la connectant à vos fonctionnalités de navigateur préférées. En combinant le HTML avec le traitement graphique de bas niveau, vous pouvez créer des expériences qui étaient auparavant impossibles.

Le DOM par rapport à Canvas

Pour comprendre la puissance de cette nouvelle API, il est utile d'examiner les points forts relatifs du DOM et de Canvas.

Le DOM est l'élément de base de l'UI Web. Il propose des solutions de mise en page de texte prêtes à l'emploi, en utilisant du contenu compris sémantiquement pour créer des interfaces riches. Cela permet aux utilisateurs d'effectuer des opérations courantes sur les pages Web de manière transparente. Par exemple, nous considérons souvent comme acquis le fait de pouvoir mettre en surbrillance du texte pour le copier ou d'effectuer un clic droit sur une image pour l'enregistrer. Le DOM s'intègre également aux fonctionnalités essentielles du navigateur : outils d'accessibilité, traduction, recherche dans la page, mode lecture, extensions, mode sombre, zoom du navigateur et saisie semi-automatique.

Canvas (et WebGL/WebGPU), en revanche, permet un accès de bas niveau pour piloter une grille de pixels pour des graphismes 2D et 3D très avancés. Les jeux et les applications Web complexes (comme Google Docs ou Figma) nécessitent cet accès performant de bas niveau. Comme la toile est fondamentalement une grille de pixels, la prise en charge de fonctionnalités telles que le texte adaptatif nécessitait une logique d'UI personnalisée complexe, ce qui augmentait considérablement la taille de votre bundle. Surtout, toutes les fonctionnalités puissantes du navigateur intégrées au DOM sont complètement interrompues lorsque l'UI est piégée dans une grille de pixels de toile statique.

Avantages de l'intégration du DOM à Canvas

L'API HTML-in-Canvas est le pont qui vous offre le meilleur des deux mondes. En plaçant du code HTML dans l'élément <canvas> et en synchronisant sa transformation, vous vous assurez que le contenu reste entièrement interactif et que toutes les intégrations du navigateur fonctionnent automatiquement.

Voici ce que vous obtenez en laissant le DOM gérer votre UI dans un élément <canvas> :

  • Mise en page et mise en forme du texte : mise en page et mise en forme du texte simplifiées, y compris le texte multiligne ou bidirectionnel avec des styles CSS appliqués.
  • Contrôles de formulaire : contrôles de formulaire expressifs et plus faciles à utiliser avec de nombreuses options de personnalisation.
  • Sélection de texte, copier/coller et clic droit : les utilisateurs peuvent mettre en surbrillance du texte dans vos scènes 3D ou effectuer un clic droit sur les menus contextuels de manière native.
  • Sélection de texte, copier/coller et clic droit : les utilisateurs peuvent mettre en surbrillance du texte dans vos scènes 3D ou effectuer un clic droit sur les menus contextuels de manière native.
  • Accessibilité : le contenu affiché dans la toile est exposé à l'arborescence d'accessibilité. Les systèmes d'accessibilité peuvent analyser l'UI comme ils le font pour le HTML normal et l'exposer à des systèmes tels que les lecteurs d'écran.
  • Find-in-page: : les utilisateurs peuvent utiliser la recherche dans la page (Ctrl/Cmd+F) pour rechercher du texte, et le navigateur le mettra en surbrillance directement dans vos textures WebGL.
  • Find-in-page: : les utilisateurs peuvent utiliser la recherche dans la page (Ctrl/Cmd+F) pour rechercher du texte, et le navigateur le mettra en surbrillance directement dans vos textures WebGL.
  • Indexabilité et interface d'agent d'IA : les robots d'exploration Web et les agents d'IA peuvent indexer et lire de manière transparente le texte affiché dans vos scènes 2D et 3D.
  • Intégration d'extensions : les extensions de navigateur fonctionnent de manière native. Par exemple, une extension de remplacement de texte mettra automatiquement à jour le texte affiché sur vos maillages 3D.
  • Intégration de DevTools : vous pouvez inspecter le contenu de votre toile, y compris les éléments d'UI WebGL/WebGPU directement dans les Outils pour les développeurs Chrome. Modifiez un style CSS dans l'inspecteur et regardez-le se mettre à jour instantanément sur la texture 3D.

Cas d'utilisation généraux

Cette API offre un potentiel incroyable dans plusieurs domaines :

  • Applications volumineuses basées sur une toile : les applications Web volumineuses telles que Google Docs, Miro ou Figma peuvent désormais afficher des composants d'UI d'application complexes de manière native dans leurs espaces de travail basés sur une toile, ce qui améliore l'accessibilité et réduit le poids du bundle.
  • Scènes et jeux 3D : les sites marketing, les expériences WebXR immersives et les jeux Web peuvent désormais placer une UI Web entièrement interactive dans des scènes 3D, comme un livre 3D qui utilise du texte DOM réel ou un terminal intégré au jeu qui prend en charge de manière native le copier-coller.

Utiliser l'API

L'utilisation de l'API se déroule en trois phases : la configuration de votre toile, le rendu dans la toile et la mise à jour de la transformation CSS afin que le navigateur sache où l'élément se trouve physiquement à l'écran.

Prérequis

L'API HTML-in-Canvas est en essai Origin Trial dans Chrome 148 à 150. Pour la tester sur votre site, utilisez Chrome Canary 149 ou une version ultérieure avec l'indicateur chrome://flags/#canvas-draw-element activé. Pour activer l'API pour d'autres utilisateurs, inscrivez-vous à la phase d'évaluation.

Étape 1 : Configuration de base de la toile

Commencez par ajouter l'attribut layoutsubtree à votre balise <canvas>. Le navigateur prend ainsi connaissance du contenu imbriqué dans la toile, le prépare à être affiché dans la toile et l'expose aux arborescences d'accessibilité.

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

Dimensionner la grille de la toile

Pour éviter le flou du contenu affiché, veillez à dimensionner la grille de la toile en fonction du facteur d'échelle de l'appareil.

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

Étape 2 : Rendu

Pour un contexte 2D, utilisez la méthode drawElementImage. Effectuez cette opération dans l'événement paint, qui se déclenche chaque fois que l'élément est redessiné, par exemple lors de la mise en surbrillance de texte ou de l'entrée utilisateur. Il est essentiel de mettre à jour la transformation CSS de l'élément avec la valeur renvoyée pour que l'interactivité continue de fonctionner.

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...
};

Effectuer le rendu avec WebGL

Pour WebGL, vous utilisez texElementImage2D. Il fonctionne de la même manière que texImage2D, mais prend l'élément DOM comme source.

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

Effectuer le rendu avec WebGPU

WebGPU utilise la méthode copyElementImageToTexture dans la file d'attente de l'appareil, de la même manière que copyExternalImageToTexture :

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

Étape 3 : Mettre à jour la transformation CSS

Maintenant que vous avez affiché l'élément dans la toile, vous devez indiquer au navigateur où il se trouve. Cela garantit la synchronisation spatiale entre la toile et la mise en page du DOM. C'est important pour que le navigateur puisse mapper correctement la zone d'événement (par exemple, l'endroit exact où l'utilisateur clique ou pointe) avec l'endroit où l'élément est affiché.

Dans le cas du contexte 2D, appliquez la transformation renvoyée par l'appel de rendu à 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();
};

Avec WebGL ou WebGPU, l'emplacement d'un élément à l'écran dépend de la façon dont la texture de sortie est utilisée par le code du nuanceur et ne peut pas être déduit du contexte de rendu du canevas. Toutefois, si votre programme de shader utilise une projection de vue de modèle typique pour dessiner la texture, vous pouvez utiliser la nouvelle fonction pratique element.getElementTransform() pour calculer une transformation qui peut être utilisée de la même manière que la valeur renvoyée par drawElementImage(). Pour ce faire, vous devez procéder comme suit :

  • Convertir la matrice WebGL MVP en matrice DOM.
  • Normaliser l'élément HTML. Les éléments HTML sont dimensionnés en pixels (par exemple, 200 px de large). Toutefois, WebGL traite généralement les objets comme des "carrés unitaires", par exemple, allant de 0 à 1. Si vous ne normalisez pas, votre bouton de 200 px sera 200 fois plus grand.
  • Mapper à la fenêtre d'affichage de la toile. Cette étape est la phase de "redimensionnement" : elle étend à nouveau les mathématiques de l'espace unitaire pour correspondre aux dimensions réelles en pixels de votre <canvas> élément à l'écran. Elle inverse également l'axe Y, car dans WebGL, le haut est positif, mais en CSS, le bas est positif.
  • Calculer la transformation finale. Multipliez les matrices dans l'ordre suivant : Viewport * MVP * Normalization. En les combinant en une seule transformation finale, vous obtenez une "carte" qui indique au navigateur l'emplacement exact où la couche d'élément HTML doit se trouver pour s'aligner sur le dessin 3D.
  • Appliquer la transformation à l'élément HTML. Cela déplace la couche d'élément HTML pour qu'elle se trouve directement au-dessus de ses pixels affichés. Ainsi, lorsqu'un utilisateur clique sur un bouton ou sélectionne du texte, il touche l'élément HTML réel.
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();
  }
}

Compatibilité avec les bibliothèques et les frameworks

Certaines bibliothèques populaires sont déjà compatibles avec la fonctionnalité HTML-in-Canvas.

Three.js

La mise à jour manuelle des matrices peut être fastidieuse. C'est pourquoi les frameworks sont déjà en train de s'y mettre. Three.js est compatible avec la version expérimentale à l'aide du nouveau 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 est également compatible avec HTML-in-Canvas à l'aide de son API de texture :

// 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();

Démonstrations

Avant d'essayer les démonstrations, assurez-vous que votre environnement est correctement configuré.

Plusieurs démonstrations servent de référence pour l'utilisation de l'API. Nous constatons déjà des solutions créatives de la part de la communauté, allant des livres 3D traduisibles aux éléments d'UI qui se réfractent à travers des shaders de verre :

  • Le livre 3D : un livre 3D affiché en WebGL qui utilise une mise en page HTML pour ses pages. Les utilisateurs peuvent échanger des polices avec CSS. Comme il est basé sur le DOM, la traduction intégrée fonctionne instantanément et les agents d'IA peuvent extraire le texte avec moins de complexité.
  • Interfaces utilisateur 3D interactives : un curseur de gelée WebGPU qui réfracte la lumière en fonction d'un modèle 3D sous-jacent, tout en répondant aux attributs d'étape HTML standards <input type="range">.
  • Textures animées : un panneau d'affichage 3D dynamique affichant un crayon SVG animé à l'aide du DOM directement dans une texture WebGL sans avoir besoin d'une boucle d'animation personnalisée.
  • Superpositions réfractives : une couche de typographie interactive déformée par un curseur 3D en mouvement, mais entièrement sélectionnable et consultable à l'aide de la recherche dans la page.

Consultez la collection de démonstrations créées par la communauté. Si vous souhaitez que votre démonstration HTML-in-Canvas figure dans cette collection, créez une demande d'extraction pour l'ajouter.

Limites

Bien qu'elle soit puissante, l'API présente quelques limites conscientes :

  • Contenu multi-origine : pour des raisons de sécurité et de confidentialité, l'API ne fonctionne pas avec le contenu d'iframe multi-origine.
  • Défilement du thread principal : le code HTML-in-canvas est dessiné avec JavaScript, ce qui signifie que le défilement et les animations ne peuvent pas être mis à jour indépendamment de JavaScript, comme c'est le cas en dehors de la toile. Les développeurs doivent examiner attentivement les caractéristiques de performances de l'intégration de contenu défilant dans une toile par rapport au défilement de la toile entière.

Commentaires

Si vous testez l'API HTML-in-Canvas, nous aimerions avoir votre avis. Vous pouvez vous inscrire à la phase d'évaluation pour activer la fonctionnalité sur votre site pendant la phase expérimentale afin de nous aider à concevoir l'API. Vous pouvez également signaler un problème pour nous faire part de vos commentaires.

Ressources