Quantos pixels há realmente em uma tela?
Desde o Chrome 84, o ResizeObserver oferece suporte a uma nova medição de caixa chamada devicePixelContentBox
, que mede a dimensão do elemento em pixels físicos. Isso permite renderizar gráficos perfeitos, especialmente no contexto de telas de alta densidade.
Contexto: pixels CSS, pixels da tela e pixels físicos
Embora muitas vezes trabalhemos com unidades abstratas de comprimento, como em
, %
ou vh
, tudo se resume a pixels. Sempre que especificamos o tamanho ou a posição de um elemento no CSS, o mecanismo de layout do navegador converte esse valor em pixels (px
). Esses são os "pixels CSS", que têm muito histórico e apenas uma relação vaga com os pixels que você tem na tela.
Por muito tempo, era bastante razoável estimar a densidade de pixels da tela de qualquer pessoa com 96 dpi (pontos por polegada), o que significa que qualquer monitor teria cerca de 38 pixels por cm. Com o tempo, os monitores aumentaram e/ou encolheram ou começaram a ter mais pixels na mesma área da superfície. Combine isso com o fato de que muitos conteúdos na Web definem as dimensões, incluindo tamanhos de fonte, em px
, e você acaba com texto ilegível nessas telas de alta densidade ("HiDPI"). Como contramedida, os navegadores ocultam a densidade de pixels real do monitor e fingem que o usuário tem uma tela de 96 DPI. A unidade px
no CSS representa o tamanho de um pixel nessa tela virtual de 96 DPI, daí o nome "pixel CSS". Essa unidade é usada apenas para medição e posicionamento. Antes de qualquer renderização, ocorre uma conversão para pixels físicos.
Como passamos dessa tela virtual para a tela real do usuário? Digite devicePixelRatio
. Esse valor global informa quantos pixels físicos você precisa para formar um único pixel de CSS. Se devicePixelRatio
(dPR) for 1
, você está trabalhando em um monitor com aproximadamente 96 DPI. Se você tiver uma tela Retina, seu dPR provavelmente será 2
. Em smartphones, não é incomum encontrar valores de dPR mais altos (e mais estranhos), como 2
, 3
ou até mesmo 2.65
. É essencial observar que esse valor é exato, mas não permite que você derive o valor de DPI real do monitor. Uma dPR de 2
significa que um pixel CSS será mapeado para exatamente dois pixels físicos.
1
de acordo com o Chrome…Ela tem 3.440 pixels de largura e a área de exibição tem 79 cm de largura.
Isso leva a uma resolução de 110 DPI. Quase 96, mas não exatamente.
É por isso que um <div style="width: 1cm; height: 1cm">
não mede exatamente 1 cm na maioria das telas.
Por fim, a dPR também pode ser afetada pelo recurso de zoom do navegador. Se você aumentar o zoom, o navegador vai aumentar o dPR informado, fazendo com que tudo seja renderizado de forma maior. Se você marcar devicePixelRatio
em um console do DevTools enquanto aumenta o zoom, os valores fracionários vão aparecer.
Vamos adicionar o elemento <canvas>
à mistura. É possível especificar quantos pixels você quer que a tela tenha usando os atributos width
e height
. Portanto, <canvas width=40 height=30>
seria uma tela com 40 x 30 pixels. No entanto, isso não significa que ele será exibido com 40 x 30 pixels. Por padrão, a tela vai usar o atributo width
e height
para definir o tamanho intrínseco, mas você pode redimensionar a tela arbitrariamente usando todas as propriedades de CSS que conhece. Com tudo o que aprendemos até agora, você pode pensar que isso não é ideal em todos os cenários. Um pixel na tela pode acabar cobrindo vários pixels físicos ou apenas uma fração de um pixel físico. Isso pode levar a artefatos visuais desagradáveis.
Resumindo: os elementos da tela têm um tamanho definido para definir a área em que você pode desenhar. O número de pixels da tela é completamente independente do tamanho de exibição da tela, especificado em pixels CSS. O número de pixels CSS não é o mesmo que o número de pixels físicos.
Perfeição
Em alguns cenários, é desejável ter um mapeamento exato dos pixels da tela para os pixels físicos. Se esse mapeamento for alcançado, ele será chamado de "pixel-perfect". A renderização perfeita é crucial para a renderização legível de texto, especialmente ao usar a renderização de subpixels ou ao exibir gráficos com linhas de brilho alternado bem alinhadas.
Para conseguir algo o mais próximo possível de uma tela com pixels perfeitos na Web, esta é mais ou menos a abordagem recomendada:
<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>
O leitor perspicaz pode estar se perguntando o que acontece quando o dPR não é um valor inteiro. Essa é uma boa pergunta e exatamente onde está o problema. Além disso, se você especificar a posição ou o tamanho de um elemento usando porcentagens, vh
ou outros valores indiretos, é possível que eles sejam resolvidos em valores fracionários de pixel CSS. Um elemento com margin-left: 33%
pode resultar em um retângulo como este:
Os pixels CSS são puramente virtuais, então ter frações de um pixel é aceitável em teoria, mas como o navegador descobre o mapeamento para pixels físicos? Porque os pixels físicos fracionários não existem.
Alinhamento de pixels
A parte do processo de conversão de unidade que cuida de alinhar elementos com pixels físicos é chamada de "pixel snapping" e faz o que diz na lata: ela fixa valores de pixels fracionários em valores de pixels inteiros e físicos. A forma exata como isso acontece varia de acordo com o navegador. Se tivermos um elemento com uma largura de 791.984px
em uma tela em que a dPR é 1, um navegador pode renderizar o elemento em 792px
pixels físicos, enquanto outro pode renderizar em 791px
. Isso é apenas um único pixel, mas um único pixel pode ser prejudicial para renderizações que precisam ser perfeitas. Isso pode levar a desfoque ou até mesmo a artefatos mais visíveis, como o efeito Moiré.
devicePixelContentBox
devicePixelContentBox
fornece a caixa de conteúdo de um elemento em unidades de pixel do dispositivo (ou seja, pixel físico). Ele faz parte de ResizeObserver
. Embora o ResizeObserver tenha suporte em todos os principais navegadores desde o Safari 13.1, a propriedade devicePixelContentBox
está disponível apenas no Chrome 84 ou mais recente.
Conforme mencionado em ResizeObserver
: é como document.onresize
para elementos, a função de callback de um ResizeObserver
será chamada antes da pintura e depois do layout. Isso significa que o parâmetro entries
para o callback vai conter os tamanhos de todos os elementos observados antes de serem pintados. No contexto do problema da tela descrito acima, podemos usar essa oportunidade para ajustar o número de pixels na tela, garantindo um mapeamento exato entre pixels da tela e pixels 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']});
A propriedade box
no objeto de opções para observer.observe()
permite definir quais tamanhos você quer observar. Portanto, embora cada ResizeObserverEntry
sempre forneça borderBoxSize
, contentBoxSize
e devicePixelContentBoxSize
(desde que o navegador ofereça suporte), o callback só será invocado se alguma das métricas da caixa observada mudar.
Com essa nova propriedade, podemos até animar o tamanho e a posição da tela (garantindo efetivamente valores fracionários de pixels) e não ver nenhum efeito Moiré na renderização. Se você quiser conferir o efeito Moiré na abordagem usando getBoundingClientRect()
e como a nova propriedade ResizeObserver
permite evitá-lo, confira a demonstração no Chrome 84 ou mais recente.
Detecção de recursos
Para verificar se o navegador de um usuário tem suporte a devicePixelContentBox
, podemos observar qualquer elemento e verificar se a propriedade está presente no 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
}
Conclusão
Os pixels são um tópico surpreendentemente complexo na Web, e até agora não havia como saber o número exato de pixels físicos que um elemento ocupa na tela do usuário. A nova propriedade devicePixelContentBox
em um ResizeObserverEntry
oferece essa informação e permite que você faça renderizações perfeitas com <canvas>
. O devicePixelContentBox
é compatível com o Chrome 84 e versões mais recentes.