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

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

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

Browser Support

  • Chrome: 84.
  • Край: 84.
  • Firefox: 108.
  • Safari: не поддерживается.

Source

Фон: пиксели CSS, пиксели холста и физические пиксели.

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

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

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

Пример
Согласно Chrome, мой монитор имеет показатель dPR, равный 1 …

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

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

В инструментах разработчика отображается различное значение дробного параметра devicePixelRatio из-за масштабирования.

Давайте добавим элемент <canvas> . Вы можете указать желаемую width и height холста в пикселях, используя соответствующие атрибуты. Таким образом, <canvas width=40 height=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% может в итоге получить прямоугольник, подобный этому:

В инструментах разработчика отображаются дробные значения пикселей, полученные в результате вызова функции 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 в объекте options для observer.observe() позволяет определить, какие размеры вы хотите отслеживать . Таким образом, хотя каждый ResizeObserverEntry всегда будет предоставлять borderBoxSize , contentBoxSize и devicePixelContentBoxSize (при условии, что браузер это поддерживает), функция обратного вызова будет вызвана только в том случае, если изменится какой-либо из отслеживаемых параметров box.

Благодаря этому новому свойству мы можем даже анимировать размер и положение нашего холста (фактически гарантируя дробные значения пикселей) и не видеть никаких эффектов муара при рендеринге. Если вы хотите увидеть эффект муара при использовании метода 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+.