Сколько пикселей на самом деле на холсте?
Начиная с Chrome 84, ResizeObserver поддерживает новый параметр измерения размера блока — devicePixelContentBox
, который измеряет размер элемента в физических пикселях. Это позволяет отображать графику с точностью до пикселя, особенно на экранах с высокой плотностью пикселей.
Фон: пиксели CSS, пиксели холста и физические пиксели
Хотя мы часто работаем с абстрактными единицами измерения длины, такими как em
, %
или vh
, всё сводится к пикселям. Всякий раз, когда мы указываем размер или положение элемента в CSS, движок вёрстки браузера в конечном итоге преобразует это значение в пиксели ( px
). Это «CSS-пиксели», которые имеют богатую историю и лишь отдалённо связаны с пикселями на экране.
Долгое время было вполне разумно оценивать плотность пикселей экрана любого устройства в 96 точек на дюйм («точек на дюйм»), что означало, что любой монитор будет иметь примерно 38 пикселей на см. Со временем мониторы увеличивались и/или уменьшались, или же на той же площади поверхности появлялось больше пикселей. Добавьте к этому тот факт, что многие размеры веб-контента, включая размеры шрифтов, определяются в px
, и мы получаем нечитаемый текст на экранах с высокой плотностью («HiDPI»). В качестве контрмеры браузеры скрывают фактическую плотность пикселей монитора и вместо этого делают вид, что у пользователя дисплей с разрешением 96 точек на дюйм. Единица px
в CSS представляет собой размер одного пикселя на этом виртуальном дисплее с разрешением 96 точек на дюйм, отсюда и название «пиксель CSS». Эта единица используется только для измерения и позиционирования. Перед рендерингом происходит преобразование в физические пиксели.
Как перейти от этого виртуального дисплея к реальному дисплею пользователя? Введите devicePixelRatio
. Это глобальное значение показывает, сколько физических пикселей нужно для формирования одного CSS-пикселя. Если devicePixelRatio
(dPR) равно 1
, вы работаете на мониторе с разрешением примерно 96 точек на дюйм. Если у вас экран Retina, ваш dPR, вероятно, равен 2
На телефонах нередко можно встретить более высокие (и более странные) значения dPR, такие как 2
, 3
или даже 2.65
. Важно отметить, что это значение точное , но оно не позволяет получить фактическое значение DPI монитора. dPR, равное 2
означает, что 1 CSS-пиксель будет отображаться ровно в 2 физических пикселя.
1
… Ширина экрана составляет 3440 пикселей, а ширина области отображения — 79 см. Это даёт разрешение 110 точек на дюйм. Близко к 96, но не совсем. Именно поэтому размер <div style="width: 1cm; height: 1cm">
на большинстве дисплеев не будет равен 1 см.
Наконец, на dPR может влиять функция масштабирования вашего браузера. При увеличении масштаба браузер увеличивает отображаемое значение dPR, из-за чего всё отображается крупнее. Если при масштабировании проверить devicePixelRatio
в консоли 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%
может получиться в виде прямоугольника, похожего на этот:

getBoundingClientRect()
.Пиксели CSS — чисто виртуальные, поэтому в теории допустимо использовать дробные части пикселя, но как браузеру определить соответствие физическим пикселям? Потому что дробные физические пиксели не существуют.
Привязка пикселей
Часть процесса преобразования единиц измерения, отвечающая за выравнивание элементов с физическими пикселями, называется «привязкой к пикселям» (pixel snapping), и её суть кроется в её названии: она привязывает дробные значения пикселей к целочисленным физическим значениям пикселей. То, как именно это происходит, различается в разных браузерах. Если на дисплее с dPR (dPR) элемент имеет ширину 791.984px
, один браузер может отобразить его с физическими пикселями 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
(при условии, что браузер поддерживает это), обратный вызов будет вызван только при изменении какой-либо из наблюдаемых метрик 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+.