Представляем пробную версию API HTML-in-Canvas.

Томас Наттестад
Thomas Nattestad

На протяжении многих лет веб-разработчикам приходилось делать сложный архитектурный выбор при создании сложных, высокоинтерактивных визуальных приложений в интернете: полагаться ли на DOM с его богатыми семантическими возможностями или рендерить непосредственно в элемент <canvas> для повышения производительности низкоуровневой графики?

С новым экспериментальным API HTML-in-Canvas, доступным уже в пробной версии Origin , вам не нужно выбирать. Этот API позволяет рисовать содержимое DOM непосредственно на 2D-холсте или текстуре WebGL/WebGPU, сохраняя при этом интерактивность, доступность и связь пользовательского интерфейса с вашими любимыми функциями браузера. Объединяя HTML с низкоуровневой обработкой графики, вы можете создавать возможности, которые ранее были невозможны.

DOM против Canvas

Чтобы понять возможности этого нового API, полезно сравнить относительные преимущества DOM и Canvas.

DOM — это основа веб-интерфейсов. Он предлагает готовые решения для компоновки текста, используя семантически понятный контент для создания многофункциональных интерфейсов. Это позволяет пользователям беспрепятственно выполнять распространенные операции на веб-страницах — то, что мы часто воспринимаем как должное, например, выделение текста для копирования или щелчок правой кнопкой мыши по изображению для его сохранения. DOM также интегрируется с основными функциями браузера: инструментами доступности, переводом, поиском по странице, режимом чтения, расширениями, темным режимом, масштабированием браузера и автозаполнением.

С другой стороны, Canvas (а также WebGL / WebGPU ) обеспечивает низкоуровневый доступ к управлению сеткой пикселей для высокотехнологичной 2D и 3D графики. Игры и сложные веб-приложения (например, Google Docs или Figma) требуют такого производительного низкоуровневого доступа. Поскольку Canvas по своей сути представляет собой сетку пикселей, поддержка таких функций, как адаптивный текст, раньше требовала сложной пользовательской логики интерфейса, что значительно увеличивало размер пакета. Важно отметить, что все мощные функции браузера, интегрированные в DOM, полностью перестают работать, когда пользовательский интерфейс оказывается запертым внутри статической сетки пикселей Canvas.

Преимущества переноса DOM в Canvas

API HTML-in-Canvas — это мост, который объединяет лучшие стороны обоих подходов. Размещая HTML внутри элемента <canvas> и синхронизируя его преобразование, вы гарантируете полную интерактивность контента и автоматическую работу всех интеграций с браузерами.

Вот что вы получите, позволив DOM управлять вашим пользовательским интерфейсом внутри элемента <canvas> :

  • Ввод и форматирование текста: Упрощенный ввод и форматирование текста, включая многострочный или двунаправленный текст с применением стилей CSS.
  • Элементы управления формами: Выразительные и простые в использовании элементы управления формами с широкими возможностями настройки.
  • Выделение текста, копирование/вставка и щелчок правой кнопкой мыши: пользователи могут выделять текст внутри 3D-сцен или использовать контекстные меню, вызываемые щелчком правой кнопки мыши.
  • Выделение текста, копирование/вставка и щелчок правой кнопкой мыши: пользователи могут выделять текст внутри 3D-сцен или использовать контекстные меню, вызываемые щелчком правой кнопки мыши.
  • Доступность: Содержимое, отображаемое внутри холста, доступно для дерева доступности. Системы обеспечения доступности могут анализировать пользовательский интерфейс так же, как и обычный HTML, и предоставлять к нему доступ таким системам, как программы чтения с экрана.
  • Поиск на странице: Пользователи могут использовать функцию поиска на странице ( Ctrl / Cmd + F ) для поиска текста, и браузер выделит его непосредственно в текстурах WebGL.
  • Поиск на странице: Пользователи могут использовать функцию поиска на странице ( Ctrl / Cmd + F ) для поиска текста, и браузер выделит его непосредственно в текстурах WebGL.
  • Возможность индексации и взаимодействие с агентами ИИ: веб-краулеры и агенты ИИ могут беспрепятственно индексировать и считывать текст, отображаемый в ваших 2D и 3D сценах.
  • Интеграция расширений: расширения для браузера работают нативно. Например, расширение для замены текста автоматически обновит текст, отображаемый на ваших 3D-моделях.
  • Интеграция с инструментами разработчика: вы можете проверять содержимое холста, включая элементы пользовательского интерфейса WebGL/WebGPU, непосредственно в инструментах разработчика Chrome. Измените стиль CSS в инспекторе, и он мгновенно обновится на 3D-текстуре!

Примеры использования высокого уровня

Этот API открывает невероятные возможности в различных областях:

  • Крупномасштабные приложения на основе холста: Мощные веб-приложения, такие как Google Docs, Miro или Figma, теперь могут отображать сложные компоненты пользовательского интерфейса приложения непосредственно в своих рабочих областях на основе холста, что повышает доступность и уменьшает вес пакета.
  • 3D-сцены и игры: маркетинговые сайты, иммерсивные WebXR-приложения и веб-игры теперь могут размещать полностью интерактивный веб-интерфейс в 3D-сценах — например, 3D-книгу, использующую реальный текст DOM, или внутриигровой терминал, который изначально поддерживает копирование и вставку.

Как использовать API

Использование API происходит в три этапа: настройка холста, отрисовка на холсте и обновление CSS-преобразования, чтобы браузер знал, где физически расположен элемент на экране.

Предварительные требования

API HTML-in-Canvas находится в стадии тестирования в Chrome версий 148–150. Для тестирования на вашем сайте используйте Chrome Canary 149 или более позднюю версию с включенным флагом chrome://flags/#canvas-draw-element . Чтобы включить API для других пользователей, зарегистрируйтесь для участия в пробной версии .

Шаг 1: Базовая настройка Canvas

Во-первых, добавьте атрибут layoutsubtree к вашему тегу <canvas> . Это позволит браузеру распознать содержимое, вложенное в canvas, подготовив его к отображению внутри canvas и сделав его доступным для деревьев доступности.

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

Измерьте размер сетки холста

Чтобы избежать размытия отображаемого контента, убедитесь, что размер сетки холста соответствует масштабному коэффициенту устройства.

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

Шаг 2: Рендеринг

Для двумерного контекста используйте метод drawElementImage . Делайте это внутри события paint , которое срабатывает всякий раз, когда элемент перерисовывается — например, при выделении текста или вводе данных пользователем. Крайне важно обновлять CSS-преобразование элемента с помощью возвращаемого значения, чтобы интерактивность продолжала работать.

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

Рендеринг с использованием WebGL

Для WebGL используется texElementImage2D . Он работает аналогично texImage2D , но в качестве источника принимает элемент DOM.

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

Рендеринг с использованием WebGPU

WebGPU использует метод copyElementImageToTexture в очереди устройств, аналогично методу copyExternalImageToTexture :

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

Шаг 3: Обновите CSS-преобразование

Теперь, когда элемент отобразился на холсте, необходимо сообщить браузеру о его местоположении. Это обеспечивает пространственную синхронизацию между холстом и компоновкой DOM. Это важно для того, чтобы браузер мог правильно сопоставить зону событий — то есть, где именно пользователь щелкает или наводит курсор — с местом отображения элемента.

В случае двумерного контекста примените преобразование, возвращаемое вызовом рендеринга, к .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();
};

В WebGL или WebGPU экранное положение элемента зависит от того, как выходная текстура используется кодом шейдера, и не может быть определено из контекста рендеринга холста. Однако, если ваша программа шейдера использует типичную проекцию модели-представления для отрисовки текстуры, вы можете использовать новую вспомогательную функцию element.getElementTransform() для вычисления преобразования, которое можно использовать так же, как возвращаемое значение функции drawElementImage() . Для этого необходимо выполнить следующие действия:

  • Преобразование матрицы WebGL MVP в матрицу DOM .
  • Нормализуйте HTML-элемент. Размеры HTML-элементов измеряются в пикселях (например, ширина 200 пикселей). WebGL, однако, обычно рассматривает объекты как «единичные квадраты», например, от 0 до 1. Если вы не нормализуете элемент, ваша кнопка размером 200 пикселей будет выглядеть в 200 раз больше.
  • Сопоставьте с областью просмотра холста. Этот шаг — фаза «масштабирования»: он растягивает эти вычисления в единичном пространстве, чтобы они соответствовали фактическим пиксельным размерам вашего элемента <canvas> на экране. Он также переворачивает ось Y, потому что в WebGL «вверх» — положительное значение, а в CSS «вниз» — положительное.
  • Вычислите итоговое преобразование. Умножьте матрицы в указанном порядке: Viewport * MVP * Normalization. Объединение их в одно итоговое преобразование создаст «карту», ​​которая точно укажет браузеру, где должен располагаться слой HTML-элемента, чтобы выровняться с 3D-чертежом.
  • Примените преобразование к HTML-элементу. Это переместит слой HTML-элемента непосредственно поверх отображаемых пикселей. Это гарантирует, что когда пользователь нажимает кнопку или выделяет текст, он попадает на реальный HTML-элемент.
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();
  }
}

Поддержка библиотек и фреймворков

Некоторые популярные библиотеки уже поддерживают функцию HTML-in-Canvas.

Three.js

Обновление матриц вручную может быть утомительным, поэтому фреймворки уже активно внедряют эту функцию. В Three.js есть экспериментальная поддержка использования нового класса 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 также поддерживает HTML-в-холсте, используя свой API для работы с текстурами:

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

Демонстрации

Перед запуском демонстрационных версий убедитесь, что ваша среда правильно настроена .

Существует несколько демонстрационных примеров , которые служат примером использования API. Мы уже видим креативные решения от сообщества, начиная от переводимых 3D-книг и заканчивая элементами пользовательского интерфейса, которые преломляются сквозь шейдеры стекла:

  • 3D-книга : 3D-книга, созданная с помощью WebGL и использующая HTML-разметку для страниц. Пользователи могут менять шрифты с помощью CSS. Благодаря DOM-ориентированному интерфейсу, встроенный перевод работает мгновенно, а агенты ИИ могут извлекать текст с меньшей сложностью.
  • Интерактивные 3D-интерфейсы : слайдер WebGPU, который преломляет свет на основе базовой 3D-модели, при этом реагируя на стандартные атрибуты HTML <input type="range"> step.
  • Анимированные текстуры : Динамический 3D-билборд, отображающий анимированный SVG-карандаш с использованием DOM непосредственно в текстуре WebGL без необходимости создания пользовательского анимационного цикла.
  • Рефракционные наложения : интерактивный типографический слой, искаженный движущимся 3D-курсором, но при этом полностью выделяемый и доступный для поиска с помощью функции «Найти на странице».

Ознакомьтесь с коллекцией демонстрационных примеров, созданных сообществом. Если вы хотите, чтобы ваш пример HTML-in-Canvas был включен в эту коллекцию, создайте запрос на добавление (pull request ).

Ограничения

Несмотря на свою мощь, API имеет ряд осознанных ограничений:

  • Контент из других источников: В целях безопасности и конфиденциальности API не работает с контентом iframe из других источников.
  • Прокрутка в основном потоке: HTML-контент внутри холста отрисовывается с помощью JavaScript, а это значит, что прокрутка и анимация не могут обновляться независимо от JavaScript, как это возможно вне холста. Разработчикам следует тщательно учитывать характеристики производительности при размещении прокручиваемого контента внутри холста по сравнению с прокруткой всего холста.

Обратная связь

Если вы экспериментируете с API HTML-in-Canvas, мы хотим услышать ваше мнение! Вы можете зарегистрироваться для участия в пробной версии , чтобы включить эту функцию на своем сайте, пока она находится на экспериментальной стадии, и помочь нам в разработке дизайна API. Вы также можете отправить запрос на исправление ошибки , чтобы оставить свой отзыв.

Ресурсы