Многие годы веб-разработчикам приходилось делать сложный выбор при создании сложных интерактивных визуальных приложений. Что использовать: DOM с его богатыми семантическими функциями или элемент <canvas> для низкоуровневой графики?
С новым экспериментальным HTML-in-Canvas API, который сейчас доступен в рамках эксперимента с источником, вам не придется выбирать. Этот API позволяет рисовать контент DOM непосредственно на холсте 2D или текстуре WebGL/WebGPU, сохраняя при этом интерактивность, доступность и связь с вашими любимыми функциями браузера. Сочетание HTML с низкоуровневой обработкой графики позволяет создавать продукты, которые раньше были невозможны.
DOM и Canvas
Чтобы понять возможности этого нового API, полезно взглянуть на относительные сильные стороны как DOM, так и Canvas.
DOM – это основа веб-интерфейса. Он предлагает готовые решения для макетов текста, используя семантически понятный контент для создания многофункциональных интерфейсов. Это позволяет пользователям выполнять обычные действия на веб-страницах, например выделять текст для копирования или нажимать правой кнопкой мыши на изображение, чтобы сохранить его. DOM также интегрируется с основными функциями браузера: инструментами специальных возможностей, переводом, поиском на странице, режимом чтения, расширениями, тёмной темой, масштабированием и автозаполнением.
Canvas (а также WebGL и WebGPU), напротив, позволяет управлять сеткой пикселей на низком уровне, чтобы создавать сложную 2D- и 3D-графику. Такой низкоуровневый доступ необходим играм и сложным веб-приложениям, например Google Документам или Figma. Поскольку холст представляет собой сетку пикселей, для поддержки таких функций, как адаптивный текст, требовалась сложная логика пользовательского интерфейса, что значительно увеличивало размер пакета. Что важно, все мощные функции браузера, интегрированные в DOM, полностью ломаются, когда пользовательский интерфейс оказывается заперт внутри статической пиксельной сетки холста.
Преимущества переноса DOM в Canvas
HTML-in-Canvas API – это мост, который позволяет использовать лучшее из обоих миров. Размещая HTML-код внутри элемента <canvas> и синхронизируя его преобразование, вы обеспечиваете полную интерактивность контента и автоматическую работу всех интеграций браузера.
Вот что вы получите, если позволите DOM управлять интерфейсом внутри элемента <canvas>:
- Макет и форматирование текста. Упрощенный макет и форматирование текста, включая многострочный или двунаправленный текст с примененными стилями CSS.
- Элементы управления формами. Более выразительные и простые в использовании элементы управления формами с широкими возможностями настройки.
- Выделение текста, копирование, вставка и нажатие правой кнопкой мыши. Пользователи могут выделять текст в 3D-сценах и нажимать правой кнопкой мыши, чтобы открыть контекстное меню.
- Специальные возможности. Контент, отображаемый внутри холста, доступен для дерева специальных возможностей. Системы специальных возможностей могут анализировать интерфейс так же, как обычный HTML, и передавать его таким системам, как программы чтения с экрана.
- Поиск по странице. Пользователи могут искать текст на странице (Ctrl/Cmd + F), и браузер будет выделять его прямо в текстурах WebGL.
- Индексируемость и доступность для ИИ-агентов. Поисковые роботы и ИИ-агенты могут без проблем индексировать и читать текст, отображаемый в 2D- и 3D-сценах.
- Интеграция расширений. Расширения браузера работают без дополнительных настроек. Например, расширение для замены текста автоматически обновляет текст, отображаемый на 3D-сетках.
- Интеграция с инструментами разработчика Chrome. Вы можете проверять содержимое холста, в том числе элементы интерфейса WebGL/WebGPU, прямо в инструментах разработчика Chrome. Измените стиль CSS в инспекторе и посмотрите, как он мгновенно обновится на 3D-текстуре.
Примеры использования
Этот API открывает невероятные возможности в нескольких областях:
- Крупные приложения на основе Canvas. Тяжелые веб-приложения теперь могут отображать сложные компоненты интерфейса непосредственно в рабочих областях на основе Canvas. Это улучшает доступность и уменьшает размер пакета.
- 3D-сцены и браузерные игры. На маркетинговых сайтах, в иммерсивных средах WebXR и браузерных играх теперь можно размещать полностью интерактивный веб-интерфейс в 3D-сценах, например 3D-книгу, в которой используется реальный текст DOM, или внутриигровой терминал, который изначально поддерживает копирование и вставку.
Как использовать API
Работа с API состоит из трех этапов: настройка холста, рендеринг на холсте и обновление преобразования CSS, чтобы браузер знал, где физически находится элемент на экране.
Требования
HTML-in-Canvas API проходит тестирование в Chrome 148–150. Чтобы протестировать его на своем сайте, используйте Chrome Canary 149 или более поздней версии с включенным параметром chrome://flags/#canvas-draw-element. Чтобы включить API для других пользователей, зарегистрируйтесь в эксперименте с источником.
Шаг 1. Базовая настройка Canvas
Сначала добавьте атрибут layoutsubtree в тег <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. Отрисовка
Для 2D-контекста используйте метод 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. Это важно, чтобы браузер мог правильно сопоставить зону события (например, где именно пользователь нажимает или наводит указатель мыши) с местом отображения элемента.
Для 2D-контекста примените преобразование, возвращенное вызовом отрисовки, к .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(). Для этого вам нужно сделать следующее:
- Преобразуйте матрицу MVP WebGL в матрицу 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-in-Canvas с помощью Texture 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-книга. Книга, созданная с помощью WebGL, со страницами в формате HTML. Пользователи могут менять шрифты с помощью CSS. Поскольку встроенный перевод основан на DOM, он работает мгновенно, а ИИ-агенты могут извлекать текст с меньшими сложностями.
- Интерактивные 3D-интерфейсы. Желейный ползунок WebGPU, который преломляет свет на основе базовой 3D-модели, но при этом реагирует на стандартные атрибуты шага
<input type="range">HTML. - Анимированные текстуры. Динамический 3D-билборд, отрисовывающий анимированный карандаш в формате SVG. При этом используется DOM непосредственно в текстуру WebGL, а не цикл анимации.
- Рефрактивные оверлеи. Интерактивный типографический слой, искажаемый движущимся 3D-курсором, но полностью доступный для выбора и поиска с помощью функции поиска на странице.
Посмотрите коллекцию демоверсий, созданных сообществом. Если вы хотите, чтобы ваша демонстрация HTML-in-Canvas была представлена в этой коллекции, создайте pull-запрос, чтобы добавить ее.
Ограничения
Несмотря на широкие возможности, у API есть несколько ограничений:
- Контент из разных источников. Из соображений безопасности и конфиденциальности API не работает с контентом iframe из разных источников.
- Прокрутка в основном потоке. HTML-код в элементе canvas отрисовывается с помощью JavaScript. Это означает, что прокрутка и анимация не могут обновляться независимо от JavaScript, как это происходит за пределами элемента canvas. Разработчикам следует тщательно продумать, как будет работать прокрутка контента внутри холста и прокрутка всего холста.
Как оставить отзыв
Если вы экспериментируете с HTML-in-Canvas API, мы будем рады вашим отзывам. Вы можете зарегистрироваться для участия в эксперименте с источником, чтобы включить эту функцию на своем сайте, пока она находится на стадии тестирования, и помочь нам улучшить API. Вы также можете сообщить о проблеме.
Ресурсы
- Поддержка HTML-in-Canvas в Three.js
- Демонстрация HTML-in-Canvas в Three.js
- Поддержка HTML-in-Canvas в PlayCanvas: документация для разработчиков
- Демонстрация HTML-in-Canvas в PlayCanvas
- HTML-in-Canvas: Explainer
- Modern Web Guidance для ИИ-инструментов программирования HTML-in-Canvas
- Демонстрации HTML-in-Canvas на сайте Chrome.dev
- Потрясающая коллекция демоверсий HTML-in-Canvas от сообщества