Rendering perfetto per i pixel con devicePixelContentBox

Quanti pixel ci sono effettivamente in una tela?

A partire da Chrome 84, ResizeObserver supporta una nuova misurazione della casella chiamata devicePixelContentBox, che misura le dimensioni dell'elemento in pixel fisici. In questo modo è possibile eseguire il rendering di grafica con una risoluzione perfetta, in particolare nel contesto di schermi ad alta densità.

Supporto dei browser

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: non supportato.

Origine

Spesso lavoriamo con unità di misura astratte di lunghezza come em, % o vh, ma alla fine tutto si riduce ai pixel. Ogni volta che specifichiamo le dimensioni o la posizione di un elemento in CSS, il motore di layout del browser alla fine convertirà quel valore in pixel (px). Si tratta di "pixel CSS", che hanno una lunga storia e hanno solo una relazione vaga con i pixel sullo schermo.

Per molto tempo, è stato abbastanza ragionevole stimare la densità di pixel dello schermo di chiunque con 96 DPI ("punti per pollice"), il che significa che qualsiasi monitor avrebbe circa 38 pixel per cm. Nel tempo, i monitor sono aumentati e/o diminuiti di dimensioni o hanno iniziato ad avere più pixel sulla stessa area. Aggiungi il fatto che molti contenuti sul web definiscono le proprie dimensioni, inclusi i caratteri, in px e ottieni un testo illeggibile su queste schermate ad alta densità ("HiDPI"). Come contromisura, i browser nascondono la densità di pixel effettiva del monitor e fingono che l'utente abbia un display a 96 DPI. L'unità px in CSS rappresenta le dimensioni di un pixel su questo display virtuale a 96 DPI, da qui il nome "pixel CSS". Questa unità viene utilizzata solo per la misurazione e il posizionamento. Prima di qualsiasi rendering effettivo, viene eseguita una conversione in pixel fisici.

Come passiamo da questo display virtuale al display reale dell'utente? Inserisci devicePixelRatio. Questo valore globale indica quanti pixel fisici sono necessari per formare un singolo pixel CSS. Se devicePixelRatio (dPR) è 1, stai lavorando su un monitor con circa 96 DPI. Se hai uno schermo Retina, la tua risoluzione in punti è probabilmente 2. Sui telefoni non è raro trovare valori di RPD più elevati (e più strani), come 2, 3 o addirittura 2.65. È essenziale notare che questo valore è esatto, ma non consente di ricavare il valore DPI effettivo del monitor. Un valore DPR pari a 2 indica che 1 pixel CSS verrà mappato esattamente a 2 pixel fisici.

Ha una larghezza di 3440 pixel e l'area di visualizzazione è larga 79 cm. Ciò corrisponde a una risoluzione di 110 DPI. Quasi 96, ma non proprio. Questo è anche il motivo per cui un <div style="width: 1cm; height: 1cm"> non misura esattamente 1 cm sulla maggior parte dei display.

Infine, il rapporto di prestazione a livello di dominio può essere influenzato anche dalla funzionalità di zoom del browser. Se aumenti lo zoom, il browser aumenta la risoluzione in pixel dichiarata, in modo che tutto venga visualizzato ingrandito. Se selezioni devicePixelRatio in una console DevTools durante lo zoom, puoi visualizzare i valori frazionari.

DevTools mostra una serie di valori frazionari devicePixelRatio a causa dello zoom.

Aggiungiamo l'elemento <canvas> al mix. Puoi specificare il numero di pixel che vuoi che abbia la tela utilizzando gli attributi width e height. Pertanto, <canvas width=40 height=30> sarà una tela con 40 x 30 pixel. Tuttavia, ciò non significa che verrà visualizzato a 40 x 30 pixel. Per impostazione predefinita, la tela utilizzerà gli attributi width e height per definire le sue dimensioni intrinseche, ma puoi ridimensionarla in modo arbitrario utilizzando tutte le proprietà CSS che conosci e ami. Con tutto ciò che abbiamo appreso finora, potresti pensare che questa non sia la soluzione ideale in ogni scenario. Un pixel sulla tela potrebbe coprire più pixel fisici o solo una frazione di un pixel fisico. Ciò può comportare artefatti visivi sgradevoli.

In sintesi: gli elementi Canvas hanno una determinata dimensione per definire l'area su cui puoi disegnare. Il numero di pixel della tela è completamente indipendente dalle dimensioni di visualizzazione della tela, specificate in pixel CSS. Il numero di pixel CSS non corrisponde al numero di pixel fisici.

Pixel perfetti

In alcuni scenari, è preferibile avere una mappatura esatta dei pixel della tela ai pixel fisici. Se questa mappatura viene raggiunta, si parla di "pixel-perfect". Il rendering con pixel perfetti è fondamentale per la leggibilità del testo, in particolare quando si utilizza il rendering a livello di subpixel o quando si mostrano immagini con linee strettamente allineate di luminosità alternata.

Per ottenere qualcosa il più vicino possibile a una tela perfetta sul web, questo è stato più o meno l'approccio di riferimento:

<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>

Il lettore più esperto potrebbe chiedersi cosa succede quando il valore di dPR non è un numero intero. Questa è una buona domanda e il punto cruciale di tutto il problema. Inoltre, se specifichi la posizione o le dimensioni di un elemento utilizzando percentuali, vh o altri valori indiretti, è possibile che vengano risolti in valori frazionari in pixel CSS. Un elemento con margin-left: 33% può terminare con un rettangolo come questo:

DevTools che mostra valori di pixel frazionari a seguito di una chiamata getBoundingClientRect().

I pixel CSS sono puramente virtuali, quindi avere frazioni di pixel è accettabile in teoria, ma come fa il browser a capire la mappatura ai pixel fisici? Perché i pixel fisici frazionari non esistono.

Snap dei pixel

La parte del processo di conversione delle unità che si occupa di allineare gli elementi ai pixel fisici è chiamata "pixel snapping" e fa esattamente quello che dice: allinea i valori frazionari dei pixel ai valori interi dei pixel fisici. La modalità esatta varia da un browser all'altro. Se abbiamo un elemento con una larghezza di 791.984px su un display in cui il rapporto dPR è 1, un browser potrebbe visualizzare l'elemento a 792px pixel fisici, mentre un altro browser potrebbe visualizzarlo a 791px. Si tratta di un solo pixel fuori, ma un solo pixel può essere dannoso per i rendering che devono essere perfetti. Ciò può causare sfocature o artefatti ancora più visibili, come l'effetto moiré.

L'immagine in alto è un raster di pixel di colori diversi. L'immagine in basso è la stessa di cui sopra, ma la larghezza e l'altezza sono state ridotte di un pixel utilizzando la scalatura bilineare. Il motivo che emerge è chiamato effetto moiré.
(Potresti dover aprire questa immagine in una nuova scheda per vederla senza alcuna applicazione di scala.)

devicePixelContentBox

devicePixelContentBox indica la casella dei contenuti di un elemento in unità di pixel del dispositivo (ovvero pixel fisici). Fa parte di ResizeObserver. Anche se ResizeObserver è ora supportato in tutti i principali browser a partire da Safari 13.1, la proprietà devicePixelContentBox è disponibile solo in Chrome 84 e versioni successive.

Come indicato in ResizeObserver: è come document.onresize per gli elementi, la funzione di callback di un ResizeObserver verrà chiamata prima di paint e dopo il layout. Ciò significa che il parametro entries della chiamata di callback conterrà le dimensioni di tutti gli elementi osservati appena prima che vengano visualizzati. Nel contesto del problema della tela descritto sopra, possiamo sfruttare questa opportunità per regolare il numero di pixel sulla tela, assicurandoci di ottenere una mappatura uno a uno esatta tra i pixel della tela e i pixel fisici.

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 proprietà box nell'oggetto options per observer.observe() ti consente di definire le dimensioni da osservare. Pertanto, anche se ogni ResizeObserverEntry fornirà sempre borderBoxSize, contentBoxSize e devicePixelContentBoxSize (a condizione che il browser lo supporti), il callback verrà invocato solo se una delle metriche della casella osservata cambia.

Con questa nuova proprietà, possiamo persino animare le dimensioni e la posizione della nostra tela (garantendo in modo efficace valori frazionari dei pixel) e non vedere effetti moiré sul rendering. Se vuoi vedere l'effetto moiré nell'approccio che utilizza getBoundingClientRect() e come la nuova proprietà ResizeObserver ti consente di evitarlo, dai un'occhiata alla demo in Chrome 84 o versioni successive.

Rilevamento di funzionalità

Per verificare se il browser di un utente supporta devicePixelContentBox, possiamo osservare qualsiasi elemento e controllare se la proprietà è presente nel 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
}

Conclusione

I pixel sono un argomento sorprendentemente complesso sul web e finora non era possibile conoscere il numero esatto di pixel fisici occupati da un elemento sullo schermo dell'utente. La nuova proprietà devicePixelContentBox in un ResizeObserverEntry ti fornisce queste informazioni e ti consente di eseguire rendering con una precisione pixel con <canvas>. devicePixelContentBox è supportato in Chrome 84 e versioni successive.