Skip to main content

Cuantos pixeles hay De Verdad en un lienzo?

Desde Chrome 84, ResizeObserver admite una nueva medida de caja llamada device-pixel-content-box, que mide la dimensión del elemento en físico píxeles. Esto permite renderizar gráficos con 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. Siempre que especifiquemos 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 poco precisa con los píxeles que tienes en tu pantalla.

Durante mucho tiempo, fue bastante razonable estimar la densidad de píxeles de la pantalla de cualquier persona con 96 ppp («puntos por pulgada»), lo que significa que cualquier monitor tendría aproximadamente 38 píxeles por cm. Con el tiempo, los monitores crecieron y / o encogieron o empezaron a tener más píxeles en la misma superficie. Combine eso con el hecho de que una gran cantidad de contenido en la web define sus dimensiones, incluidos los tamaños de fuente, en px, y 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 su lugar, fingen que el usuario tiene una pantalla de 96 DPI. los px unidad en CSS representa el tamaño de un píxel en este virtual Pantalla de 96 DPI, de ahí el nombre «CSS Pixel». Esta unidad solo se utiliza para medición y posicionamiento. Antes de que ocurra cualquier renderizado real, ocurre una conversión a píxeles físicos.

¿Cómo pasamos de esta pantalla virtual a la pantalla real del usuario? Entrar devicePixelRatio. Este valor global le dice cuántos físico píxeles que necesita para formar un solo píxel CSS. Si devicePixelRatio (dPR) es 1, está trabajando en un monitor con aproximadamente 96 ppp. Si tiene una pantalla de retina, probablemente su dPR sea 2. En los teléfonos, no es raro encontrar valores 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 le permite derivar el monitor real Valor de DPI. Un dPR de 2 significa que 1 píxel CSS se asignará a exactamente 2 píxeles físicos.

Mi monitor tiene un dPR de 1 según Chrome …

Tiene 3440 píxeles de ancho y el área de visualización es de 79 cm de ancho. Eso conduce a una resolución de 110 DPI. Cerca de 96, pero no del todo. Ésa es también la razón por la que <div style="width: 1cm; height: 1cm">
no medirá exactamente 1 cm de tamaño en la mayoría de las pantallas.

Finalmente, dPR también puede verse afectado por la función de zoom de su navegador. Si hace zoom, el navegador aumenta el dPR informado, lo que hace que todo se vea más grande. Si miras devicePixelRatio en una Consola de DevTools mientras hace zoom, puede ver que aparecen valores fraccionarios.

dprs-1305951
DevTools que muestra una variedad de devicePixelRatio debido al zoom.

Agreguemos el <canvas> elemento a la mezcla. Puede especificar cuántos píxeles desea que tenga el lienzo utilizando el width y height atributos. Entonces <canvas width=40 height=30> sería un lienzo de 40 por 30 píxeles. Sin embargo, esto no significa que será desplegado a 40 por 30 píxeles. De forma predeterminada, el lienzo utilizará el width y height atributo para definir su tamaño intrínseco, pero puede cambiar el tamaño del lienzo arbitrariamente utilizando todas las propiedades CSS que conoce y ama. Con todo lo que hemos aprendido hasta ahora, se le puede ocurrir que esto no será ideal en todos los escenarios. Un píxel en el lienzo puede terminar cubriendo varios píxeles físicos o solo una fracción de un píxel físico. Esto puede generar artefactos visuales desagradables.

Para resumir: Los elementos del lienzo tienen un tamaño determinado para definir el área en la que puede dibujar. El número de píxeles del lienzo es completamente independiente del tamaño de visualización del lienzo, especificado en píxeles CSS. El número de píxeles CSS no es el mismo que el número de píxeles físicos.

Perfección de píxeles

En algunos escenarios, es deseable tener un mapeo exacto de los píxeles del lienzo a los píxeles físicos. Si se logra este mapeo, se llama «píxel perfecto». La representación perfecta de píxeles es crucial para la representación legible de texto, especialmente cuando se usa representación de subpíxeles o al mostrar gráficos con líneas muy alineadas de brillo alterno.

Para lograr algo lo más cercano posible a un lienzo de píxeles perfectos en la web, este ha sido más o menos el enfoque a seguir:

<style>
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
const rectangle = cvs.getBoundingClientRect();
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
</script>

El lector astuto podría preguntarse qué sucede cuando dPR no es un valor entero. Esa es una buena pregunta y exactamente dónde radica el meollo de todo este problema. Además, si especifica la posición o el tamaño de un elemento mediante porcentajes, vhu otros valores indirectos, 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 este:

fractional-pixels-9178262
DevTools que muestra valores de píxeles fraccionarios como resultado de una getBoundingClientRect() llamada.

Los píxeles CSS son puramente virtuales, por lo que tener fracciones de un píxel está bien en teoría, pero ¿cómo calcula el navegador la asignación a los píxeles físicos? Porque fraccional físico los píxeles no son una cosa.

Ajuste de píxeles

La parte del proceso de conversión de unidades que se encarga de alinear elementos con píxeles físicos se denomina «ajuste de píxeles», y hace lo que dice en la lata: ajusta valores de píxeles fraccionarios a valores de píxeles físicos enteros. La forma en que esto sucede exactamente es diferente de un navegador a otro. Si tenemos un elemento con un ancho de 791.984px en una pantalla donde dPR es 1, un navegador puede representar el elemento en 792px píxeles físicos, mientras que otro navegador puede representarlo en 791px. Eso es solo un píxel menos, pero un solo píxel puede ser perjudicial para las representaciones que deben ser perfectas en píxeles. Esto puede provocar borrosidad o incluso artefactos más visibles como el Efecto moiré.

side-by-side-5944113
La imagen superior es una trama de píxeles de diferentes colores. La imagen inferior es la misma que la anterior, pero el ancho y la altura se han reducido en un píxel mediante la escala bilineal. El patrón emergente se llama efecto Moiré.
(Puede que tenga que abrir esta imagen en una nueva pestaña para verla sin aplicarle ninguna escala).

devicePixelContentBox

devicePixelContentBox le ofrece el cuadro de contenido de un elemento en unidades de píxeles del dispositivo (es decir, píxeles físicos). Es parte de ResizeObserver. Mientras ResizeObserver ahora es compatible con todos los navegadores principales desde Safari 13.1, el devicePixelContentBox La propiedad solo está en Chrome 84+ por ahora.

Como se menciona en ResizeObserver: es como document.onresize para elementos, la función de devolución de llamada de un ResizeObserver se llamará antes de pintar y después del diseño. Eso significa que el entries El parámetro de la devolución de llamada contendrá los tamaños de todos los elementos observados justo antes de que se pinten. En el contexto de nuestro problema de lienzo descrito anteriormente, podemos aprovechar esta oportunidad para ajustar la cantidad de píxeles en nuestro lienzo, asegurándonos de que terminemos con un mapeo exacto 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;


});
observer.observe(canvas, {box: ['device-pixel-content-box']});

los box propiedad en el objeto de opciones para observer.observe() le permite definir qué tamaños desea observar. Así que mientras cada ResizeObserverEntry siempre proporcionará borderBoxSize, contentBoxSize y devicePixelContentBoxSize (siempre que el navegador lo admita), la devolución de llamada solo se invocará si alguno de los observado las métricas de caja cambian.

Todas las métricas de caja son matrices para permitir ResizeObserver para manejar la fragmentación en el futuro. En el momento de escribir este artículo, la matriz siempre tiene una longitud de 1.

Con esta nueva propiedad, incluso podemos animar el tamaño y la posición de nuestro lienzo (garantizando efectivamente valores de píxeles fraccionarios) y no ver ningún efecto Moiré en el renderizado. Si desea ver el efecto Moiré en el enfoque utilizando getBoundingClientRect()y como el nuevo ResizeObserver propiedad le permite evitarlo, eche un vistazo a la manifestación en Chrome 84 o posterior.

Detección de características

Para comprobar si el navegador de un usuario es compatible con devicePixelContentBox, podemos observar cualquier elemento y comprobar si la propiedad está presente en el 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())) {
}

Conclusión

Los píxeles son un tema sorprendentemente complejo en la web y hasta ahora no había forma de saber el número exacto de píxeles físicos que ocupa un elemento en la pantalla del usuario. El nuevo devicePixelContentBox propiedad en una ResizeObserverEntry le brinda esa información y le permite hacer representaciones perfectas en píxeles con <canvas>. devicePixelContentBox es compatible con Chrome 84+.