¿Cuántos píxeles hay realmente en un lienzo?
A partir de Chrome 84, ResizeObserver admite una nueva medición de cuadro llamada devicePixelContentBox
, que mide la dimensión del elemento en píxeles físicos. Esto permite renderizar gráficos de píxeles perfectos, especialmente en el contexto de pantallas de alta densidad.
Fondo: píxeles CSS, píxeles de lienzo y píxeles físicos
Si bien a menudo trabajamos con unidades abstractas de longitud, como em
, %
o vh
, todo se reduce a píxeles. Cada vez que especificamos el tamaño o la posición de un elemento en CSS, el motor de diseño del navegador eventualmente convertirá ese valor en píxeles (px
). Estos son "píxeles CSS", que tienen mucho historial y solo tienen una relación vaga con los píxeles que tienes en la pantalla.
Durante mucho tiempo, fue bastante razonable estimar la densidad de píxeles de la pantalla de cualquier persona con 96 DPI ("puntos por pulgada"), lo que significa que cualquier monitor tendría alrededor de 38 píxeles por cm. Con el tiempo, los monitores aumentaron o se redujeron, o comenzaron a tener más píxeles en la misma área de superficie. Si combinamos esto con el hecho de que mucho contenido en la Web define sus dimensiones, incluidos los tamaños de fuente, en px
, terminamos con texto ilegible en estas pantallas de alta densidad ("HiDPI"). Como contramedida, los navegadores ocultan la densidad de píxeles real del monitor y, en cambio, simulan que el usuario tiene una pantalla de 96 DPI. La unidad px
en CSS representa el tamaño de un píxel en esta pantalla virtual de 96 DPI. De ahí el nombre "CSS Pixel". Esta unidad solo se usa para la medición y el posicionamiento. Antes de que se realice la renderización real, se produce una conversión a píxeles físicos.
¿Cómo pasamos de esta pantalla virtual a la pantalla real del usuario? Ingresa devicePixelRatio
. Este valor global indica cuántos píxeles físicos necesitas para formar un solo píxel de CSS. Si devicePixelRatio
(dPR) es 1
, significa que estás trabajando en un monitor con aproximadamente 96 DPI. Si tienes una pantalla retina, es probable que tu dPR sea 2
. En los teléfonos, es común encontrar valores de dPR más altos (y más extraños), como 2
, 3
o, incluso, 2.65
. Es fundamental tener en cuenta que este valor es exacto, pero no te permite obtener el valor de DPI real del monitor. Una dPR de 2
significa que 1 píxel de CSS se asignará a 2 píxeles físicos exactamente.
1
según Chrome...Tiene 3440 píxeles de ancho y el área de visualización tiene 79 cm de ancho.
De esta manera, se obtiene una resolución de 110 DPI. Cerca de 96, pero no exactamente.
Esa es también la razón por la que un <div style="width: 1cm; height: 1cm">
no medirá exactamente 1 cm en la mayoría de las pantallas.
Por último, la función de zoom de tu navegador también puede verse afectada por la dPR. Si acercas la imagen, el navegador aumentará la dPR informada, lo que hará que todo se vea más grande. Si marcas devicePixelRatio
en la consola de Herramientas para desarrolladores mientras haces zoom, verás que aparecen valores fraccionarios.
Agreguemos el elemento <canvas>
a la combinación. Puedes especificar cuántos píxeles quieres que tenga el lienzo con los atributos width
y height
. Por lo tanto, <canvas width=40 height=30>
sería un lienzo de 40 por 30 píxeles. Sin embargo, esto no significa que se mostrará en 40 por 30 píxeles. De forma predeterminada, el lienzo usará los atributos width
y height
para definir su tamaño intrínseco, pero puedes cambiar el tamaño del lienzo de forma arbitraria con todas las propiedades de CSS que conoces y te encantan. Con todo lo que hemos aprendido hasta ahora, es posible que se te ocurra que esto no será lo ideal en todas las situaciones. Un píxel del lienzo puede terminar cubriendo varios píxeles físicos o solo una fracción de uno físico. Esto puede llevar a artefactos visuales desagradables.
En resumen, los elementos del lienzo tienen un tamaño determinado para definir el área en la que puedes dibujar. La cantidad de píxeles de lienzo es completamente independiente del tamaño de visualización del lienzo, el cual se especifica en píxeles CSS. La cantidad de píxeles de CSS no es la misma que la cantidad de píxeles físicos.
Píxeles perfectos
En algunas situaciones, es conveniente tener una asignación exacta de píxeles del lienzo a píxeles físicos. Si se logra esta asignación, se denomina "pixel perfect". La renderización de píxeles perfectos es fundamental para la renderización legible del texto, especialmente cuando se usa la renderización de subpíxeles o cuando se muestran gráficos con líneas alineadas de brillo alterno.
Para lograr algo lo más parecido posible a un lienzo de píxeles perfectos en la Web, este fue, más o menos, el enfoque preferido:
<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>
El lector perspicaz podría preguntarse qué sucede cuando dPR no es un valor entero. Esa es una buena pregunta y es exactamente donde reside la clave de todo este problema. Además, si especificas la posición o el tamaño de un elemento con porcentajes, vh
o algún otro valor indirecto, es posible que se resuelvan en valores de píxeles de CSS fraccionarios. Un elemento con margin-left: 33%
puede terminar con un rectángulo como el siguiente:
Los píxeles CSS son puramente virtuales, por lo que tener fracciones de un píxel está bien en teoría, pero ¿cómo determina el navegador la asignación a píxeles físicos? Porque los píxeles físicos fraccionarios no son una cosa.
Ajuste de píxeles
La parte del proceso de conversión de unidades que se encarga de alinear los elementos con los píxeles físicos se denomina "ajuste de píxeles" y hace lo que se indica en el estaño: ajusta los valores de píxeles fraccionarios a valores enteros de píxeles físicos. La forma en que esto sucede exactamente difiere de un navegador a otro. Si tenemos un elemento con un ancho de 791.984px
en una pantalla en la que la dPR es 1, un navegador podría renderizar el elemento en 792px
píxeles físicos, mientras que otro navegador podría renderizarlo en 791px
. Es solo un píxel de diferencia, pero un solo píxel puede ser perjudicial para las renderizaciones que deben ser perfectas en términos de píxeles. Esto puede provocar imágenes borrosas o incluso artefactos más visibles, como el efecto moiré.
devicePixelContentBox
devicePixelContentBox
te proporciona el cuadro de contenido de un elemento en unidades de píxeles del dispositivo (es decir, el píxel físico). Es parte de ResizeObserver
. Si bien Cambiar tamañoObserver ahora es compatible con todos los navegadores principales a partir de Safari 13.1, por el momento, la propiedad devicePixelContentBox
solo está disponible en Chrome 84 y versiones posteriores.
Como se menciona en ResizeObserver
: es como document.onresize
para elementos, se llamará a la función de devolución de llamada de un ResizeObserver
antes de la pintura y después del diseño. Eso significa que el parámetro entries
de la devolución de llamada contendrá los tamaños de todos los elementos observados justo antes de que se pinten. En el contexto del problema del lienzo que se describió anteriormente, podemos aprovechar esta oportunidad para ajustar la cantidad de píxeles en el lienzo y asegurarnos de obtener una asignación exacta uno a uno entre los píxeles del lienzo y los píxeles físicos.
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']});
La propiedad box
del objeto de opciones de observer.observe()
te permite definir qué tamaños deseas observar. Por lo tanto, aunque cada ResizeObserverEntry
siempre proporcionará borderBoxSize
, contentBoxSize
y devicePixelContentBoxSize
(siempre que el navegador lo admita), la devolución de llamada solo se invocará si cambia alguna de las métricas del cuadro observado.
Con esta nueva propiedad, incluso podemos animar el tamaño y la posición de nuestro lienzo (lo que garantiza eficazmente valores fraccionarios de píxeles) y no ver ningún efecto de Moiré en la representación. Si deseas ver el efecto de Moiré en el enfoque que usa getBoundingClientRect()
y cómo la nueva propiedad ResizeObserver
te permite evitarlo, consulta la demostración en Chrome 84 o versiones posteriores.
Detección de funciones
Para verificar si el navegador de un usuario es compatible con devicePixelContentBox
, podemos observar cualquier elemento y comprobar si la propiedad está presente en 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
}
Conclusión
Los píxeles son un tema sorprendentemente complejo en la Web y, hasta ahora, no había forma de saber la cantidad exacta de píxeles físicos que ocupa un elemento en la pantalla del usuario. La nueva propiedad devicePixelContentBox
de un objeto ResizeObserverEntry
te brinda esa información y te permite realizar renderizaciones de píxeles perfectos con <canvas>
. devicePixelContentBox
es compatible con Chrome 84 y versiones posteriores.