Пиксельный рендеринг с помощью устройстваPixelContentBox

Сколько пикселей на самом деле содержится в холсте?

Начиная с Chrome 84, ResizeObserver поддерживает новое измерение блока под названием devicePixelContentBox , которое измеряет размер элемента в физических пикселях. Это позволяет отображать графику с точностью до пикселя, особенно в контексте экранов с высокой плотностью изображения.

Поддержка браузера

  • 84
  • 84
  • 93
  • Икс

Справочная информация: пиксели CSS, пиксели холста и физические пиксели.

Хотя мы часто работаем с абстрактными единицами длины, такими как em , % или vh , все сводится к пикселям. Всякий раз, когда мы указываем размер или положение элемента в CSS, механизм компоновки браузера в конечном итоге преобразует это значение в пиксели ( px ). Это «пиксели CSS», которые имеют богатую историю и имеют лишь слабое отношение к пикселям на вашем экране.

В течение долгого времени было вполне разумно оценивать плотность пикселей экрана любого человека как 96 точек на дюйм («точек на дюйм»), что означало, что любой монитор будет иметь примерно 38 пикселей на см. Со временем мониторы росли и/или уменьшались или начинали иметь больше пикселей на одной и той же площади поверхности. Добавьте к этому тот факт, что большая часть контента в Интернете определяет свои размеры, включая размеры шрифта, в px , и в итоге мы получаем неразборчивый текст на этих экранах с высокой плотностью («HiDPI»). В качестве контрмеры браузеры скрывают фактическую плотность пикселей монитора и вместо этого делают вид, что у пользователя дисплей с разрешением 96 точек на дюйм. Единица px в CSS представляет размер одного пикселя на этом виртуальном дисплее с разрешением 96 точек на дюйм, отсюда и название «Пиксель CSS». Этот блок используется только для измерения и позиционирования. Прежде чем произойдет реальный рендеринг, происходит преобразование в физические пиксели.

Как нам перейти от этого виртуального дисплея к реальному дисплею пользователя? Введите devicePixelRatio . Это глобальное значение говорит вам, сколько физических пикселей вам нужно для формирования одного пикселя CSS. Если devicePixelRatio (dPR) равен 1 , вы работаете на мониторе с разрешением примерно 96 точек на дюйм. Если у вас сетчатка, ваш dPR, вероятно, равен 2 . На телефонах нередко можно встретить более высокие (и более странные) значения dPR, такие как 2 , 3 или даже 2.65 . Важно отметить, что это значение является точным , но не позволяет получить фактическое значение DPI монитора. Значение dPR, равное 2 означает, что 1 пиксель CSS будет соответствовать ровно двум физическим пикселям.

Пример
Мой монитор имеет dPR, равный 1 по данным Chrome…

Его ширина составляет 3440 пикселей, а ширина экрана — 79 см. Это приводит к разрешению 110 DPI. Близко к 96, но не совсем. Это также причина, по которой размер <div style="width: 1cm; height: 1cm"> не будет точно равен 1 см на большинстве дисплеев.

Наконец, на dPR также может влиять функция масштабирования вашего браузера. Если вы увеличите масштаб, браузер увеличит сообщаемый dPR, в результате чего все будет отображаться больше. Если вы проверите devicePixelRatio в консоли DevTools при масштабировании, вы увидите появление дробных значений.

DevTools показывает разнообразие дробных devicePixelRatio из-за масштабирования.

Давайте добавим к этому элемент <canvas> . Вы можете указать, сколько пикселей должно быть на холсте, используя атрибуты width и height . Таким образом <canvas width=40 height=30> будет холстом размером 40 на 30 пикселей. Однако это не значит, что оно будет отображаться с разрешением 40 на 30 пикселей. По умолчанию холст будет использовать атрибуты width и height для определения своего внутреннего размера, но вы можете произвольно изменять размер холста, используя все свойства CSS, которые вы знаете и любите. Учитывая все, что мы узнали до сих пор, вам может прийти в голову, что это не будет идеальным в каждом сценарии. Один пиксель на холсте может в конечном итоге покрывать несколько физических пикселей или только часть физического пикселя. Это может привести к неприятным визуальным артефактам.

Подводя итог: элементы Canvas имеют заданный размер, определяющий область, в которой вы можете рисовать. Количество пикселей холста полностью не зависит от размера экрана холста, указанного в пикселях CSS. Количество пикселей CSS не совпадает с количеством физических пикселей.

Пиксельное совершенство

В некоторых сценариях желательно иметь точное сопоставление пикселей холста с физическими пикселями. Если такое отображение достигается, оно называется «идеальным по пикселям». Пиксельный рендеринг имеет решающее значение для разборчивого рендеринга текста, особенно при использовании субпиксельного рендеринга или при отображении графики с плотно выровненными линиями переменной яркости.

Чтобы добиться в Интернете чего-то, максимально приближенного к идеальному по пикселям холсту, был более или менее подходящим подход:

<style>
  /* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
  const cvs = document.querySelector('#myCanvas');
  // Get the canvas' size in CSS pixels
  const rectangle = cvs.getBoundingClientRect();
  // Convert it to real pixels. Ish.
  cvs.width = rectangle.width * devicePixelRatio;
  cvs.height = rectangle.height * devicePixelRatio;
  // Start drawing…
</script>

Проницательный читатель может задаться вопросом, что происходит, когда dPR не является целочисленным значением. Это хороший вопрос, и в чем именно заключается суть всей этой проблемы. Кроме того, если вы укажете положение или размер элемента, используя проценты, vh или другие косвенные значения, возможно, что они будут преобразованы в дробные значения пикселей CSS. Элемент с margin-left: 33% может иметь такой прямоугольник:

DevTools показывает дробные значения пикселей в результате вызова getBoundingClientRect() .

Пиксели CSS являются чисто виртуальными, поэтому теоретически иметь части пикселя — это нормально, но как браузер определяет сопоставление с физическими пикселями? Потому что дробные физические пиксели — это не вещь.

Привязка пикселей

Часть процесса преобразования единиц измерения, которая обеспечивает выравнивание элементов по физическим пикселям, называется «привязкой пикселей» и делает то, что написано на упаковке: привязывает дробные значения пикселей к целым, физическим значениям пикселей. То, как именно это происходит, отличается от браузера к браузеру. Если у нас есть элемент шириной 791.984px на дисплее, где dPR равен 1, один браузер может отображать элемент с размером физических пикселей 792px , а другой браузер может отображать его с 791px . Это всего лишь один пиксель, но один пиксель может нанести вред рендерингу, который должен быть идеальным до пикселя. Это может привести к размытости или еще более заметным артефактам, таким как эффект муара .

Верхнее изображение представляет собой растр из пикселей разного цвета. Нижнее изображение такое же, как и выше, но ширина и высота уменьшены на один пиксель с помощью билинейного масштабирования. Возникающая закономерность называется эффектом Муара.
(Возможно, вам придется открыть это изображение в новой вкладке, чтобы увидеть его без применения к нему масштабирования.)

devicePixelContentBox

devicePixelContentBox предоставляет вам поле содержимого элемента в единицах пикселей устройства (т.е. физических пикселей). Это часть ResizeObserver . Хотя ResizeObserver теперь поддерживается во всех основных браузерах, начиная с Safari 13.1, свойство devicePixelContentBox пока доступно только в Chrome 84+.

Как упоминалось в ResizeObserver : это похоже document.onresize для элементов , функция обратного вызова ResizeObserver будет вызываться перед рисованием и после макета. Это означает, что параметр entries обратного вызова будет содержать размеры всех наблюдаемых элементов непосредственно перед их отрисовкой. В контексте нашей проблемы с холстом, описанной выше, мы можем использовать эту возможность, чтобы настроить количество пикселей на нашем холсте, гарантируя, что в конечном итоге мы получим точное взаимно-однозначное сопоставление между пикселями холста и физическими пикселями.

const observer = new ResizeObserver((entries) => {
  const entry = entries.find((entry) => entry.target === canvas);
  canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
  canvas.height = entry.devicePixelContentBoxSize[0].blockSize;

  /* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});

Свойство box в объекте параметров observer.observe() позволяет вам определить, какие размеры вы хотите наблюдать . Таким образом, хотя каждый ResizeObserverEntry всегда будет предоставлять borderBoxSize , contentBoxSize и devicePixelContentBoxSize (при условии, что браузер поддерживает это), обратный вызов будет вызываться только в том случае, если какая-либо из наблюдаемых метрик блока изменится.

Благодаря этому новому свойству мы можем даже анимировать размер и положение нашего холста (фактически гарантируя дробные значения пикселей) и не видеть никаких эффектов муара при рендеринге. Если вы хотите увидеть эффект муара при использовании метода getBoundingClientRect() и то, как новое свойство ResizeObserver позволяет избежать этого, взгляните на демонстрацию в Chrome 84 или более поздней версии!

Обнаружение функций

Чтобы проверить, поддерживает ли браузер пользователя devicePixelContentBox , мы можем наблюдать за любым элементом и проверять, присутствует ли это свойство в ResizeObserverEntry :

function hasDevicePixelContentBox() {
  return new Promise((resolve) => {
    const ro = new ResizeObserver((entries) => {
      resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
      ro.disconnect();
    });
    ro.observe(document.body, {box: ['device-pixel-content-box']});
  }).catch(() => false);
}

if (!(await hasDevicePixelContentBox())) {
  // The browser does NOT support devicePixelContentBox
}

Заключение

Пиксели — удивительно сложная тема в Интернете, и до сих пор не было возможности узнать точное количество физических пикселей, которые элемент занимает на экране пользователя. Новое свойство devicePixelContentBox в ResizeObserverEntry предоставляет вам эту информацию и позволяет выполнять рендеринг с точностью до пикселя с помощью <canvas> . devicePixelContentBox поддерживается в Chrome 84+.