Rendering perfetto per i pixel con devicePixelContentBox

Quanti pixel ci sono davvero in una tela?

A partire da Chrome 84, ResizeObserver supporta una nuova misurazione a scatola chiamata devicePixelContentBox, che misura le dimensioni dell'elemento in pixel fisici. Ciò consente di visualizzare graficamente i pixel perfetti, soprattutto nel contesto di schermi ad alta densità.

Supporto dei browser

  • 84
  • 84
  • 93
  • x

Sfondo: pixel CSS, pixel canvas e pixel fisici.

Anche se spesso lavoriamo con unità astratte di lunghezza come em, % o vh, il tutto si riduce in pixel. Ogni volta che specifichiamo le dimensioni o la posizione di un elemento in CSS, il motore di layout del browser convertirà il valore in pixel (px). Si tratta di "pixel CSS", che hanno una lunga cronologia e hanno solo relazioni limitate con i pixel presenti sullo schermo.

Per molto tempo, era abbastanza ragionevole stimare la densità dei pixel dello schermo di chiunque con 96 DPI ("punti per pollice"), il che significa che qualsiasi monitor avrebbe circa 38 pixel per cm. Nel corso del tempo, i monitor si sono aumentati e/o si sono ridotti o hanno iniziato ad avere più pixel sulla stessa area. Se a ciò si aggiunge il fatto che molti contenuti sul web definiscono le dimensioni, comprese quelle dei caratteri, in px, si ottiene un testo illeggibile su queste schermate ad alta densità ("HiDPI"). Come contromisura, i browser nascondono l'effettiva densità dei pixel del monitor e fanno finta che l'utente abbia un display a 96 DPI. L'unità px in CSS rappresenta la dimensione di un pixel su questo display virtuale a 96 DPI, da cui il nome "CSS Pixel". Questa unità viene utilizzata solo per la misurazione e il posizionamento. Prima che venga eseguito il rendering effettivo, avviene una conversione in pixel fisici.

Come si passa da questo display virtuale a quello reale dell'utente? Inserisci devicePixelRatio. Questo valore globale indica di 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, il tuo dPR probabilmente è 2. Sui telefoni non è raro incontrare valori dPR più elevati (e più strani) come 2, 3 o persino 2.65. È essenziale notare che questo valore è esatto, ma non consente di ricavare il valore DPI effettivo del monitor. Un dPR pari a 2 indica che 1 pixel CSS verrà mappato esattamente a 2 pixel fisici.

Esempio
Secondo Chrome, il mio monitor ha un dPR di 1...

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

Infine, la funzione di zoom del browser può influire sulla dPR. Se aumenti lo zoom, il browser aumenta la dPR segnalata, con la conseguente visualizzazione di tutti gli elementi più grandi. Se selezioni devicePixelRatio in una console DevTools durante lo zoom, puoi visualizzare i valori frazionari.

DevTools mostra una varietà di devicePixelRatio frazionari a causa dello zoom.

Aggiungiamo l'elemento <canvas> al mix. Puoi specificare il numero di pixel che deve avere il canvas utilizzando gli attributi width e height. <canvas width=40 height=30> sarebbe una tela di 40 x 30 pixel. Tuttavia, questo non significa che verrà visualizzato a 40 x 30 pixel. Per impostazione predefinita, il canvas utilizzerà gli attributi width e height per definire le sue dimensioni intrinseche, ma puoi ridimensionare arbitrariamente il canvas utilizzando tutte le proprietà CSS che conosci e apprezzi. Con tutto ciò che abbiamo appreso finora, potrebbe capitare che questo non sia ideale in tutti gli scenari. Un pixel sul canvas potrebbe coprire più pixel fisici o solo una frazione di un pixel fisico. Ciò può causare artefatti visivi fastidiosi.

Riassumendo: gli elementi di Canvas hanno una dimensione specifica per definire l'area su cui puoi disegnare. Il numero di pixel del canvas è completamente indipendente dalle dimensioni di visualizzazione del canvas, specificate in pixel CSS. Il numero di pixel CSS non corrisponde al numero di pixel fisici.

La perfezione in pixel

In alcuni scenari, è auspicabile una mappatura esatta dai pixel del canvas a pixel fisici. Se viene ottenuta questa mappatura, si parla di "pixel-perfect". Un rendering perfetto per i pixel è fondamentale per una visualizzazione leggibile del testo, soprattutto quando si utilizza il rendering con subpixel o quando si visualizzano elementi grafici con linee di luminosità alternativa ben allineate.

Per ottenere qualcosa di il più vicino possibile al canvas perfetto sul web, questo è stato più o meno l'approccio ideale:

<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 astuto potrebbe chiedersi cosa succede quando dPR non è un valore intero. Questa è una buona domanda ed esattamente dove risiede il punto cruciale di questo problema. Inoltre, se specifichi la posizione o la dimensione di un elemento utilizzando percentuali, vh o altri valori indiretti, è possibile che questi vengano risolti in valori di pixel CSS frazionari. Un elemento con margin-left: 33% può avere un rettangolo come il seguente:

DevTools mostra i valori frazionari dei pixel in seguito a una chiamata a getBoundingClientRect().

I pixel CSS sono puramente virtuali, quindi in teoria va bene avere frazioni di pixel, ma come fa il browser a stabilire la mappatura ai pixel fisici? Perché i pixel fisici frazionari non sono un cosa.

Aggancio di pixel

La parte del processo di conversione delle unità che si occupa dell'allineamento degli elementi con i pixel fisici è chiamata "pixel staccatura" e ha lo stesso effetto sul barattolo: aggancia i valori frazionari dei pixel a valori interi e di pixel fisici. Ciò che accade esattamente varia da un browser all'altro. Se abbiamo un elemento con una larghezza di 791.984px su un display dove dPR è 1, un browser potrebbe eseguire il rendering dell'elemento a 792px pixel fisici, mentre un altro browser potrebbe eseguirne il rendering a 791px. Con un pixel spento, un solo pixel può essere dannoso per i rendering che devono essere perfetti. Ciò può causare sfocature o artefatti più visibili come l'effetto Moiré.

L'immagine in alto è un raster di pixel di diversi colori. L'immagine in basso è la stessa di cui sopra, ma la larghezza e l'altezza sono state ridotte di un pixel grazie al ridimensionamento bilineare. Questo modello emergente è chiamato effetto Moiré.
(Potresti dover aprire questa immagine in una nuova scheda per vederla senza applicare scalature.)

devicePixelContentBox

devicePixelContentBox fornisce il riquadro dei contenuti di un elemento in unità di pixel del dispositivo (ovvero pixel fisici). Fa parte di ResizeObserver. Anche se RidimensionaObservationr è supportata in tutti i principali browser a partire da Safari 13.1, per il momento la proprietà devicePixelContentBox è disponibile solo in Chrome 84 e versioni successive.

Come accennato in ResizeObserver: è come document.onresize per gli elementi, la funzione di callback di ResizeObserver verrà chiamata prima e dopo il layout. Ciò significa che il parametro entries del callback conterrà le dimensioni di tutti gli elementi osservati appena prima che vengano visualizzati. Nel contesto del problema del canvas descritto sopra, possiamo sfruttare questa opportunità per modificare il numero di pixel sul canvas, in modo da ottenere un'esatta mappatura one-to-one tra pixel canvas e 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 opzioni per observer.observe() ti consente di definire le dimensioni che vuoi osservare. Pertanto, sebbene ogni ResizeObserverEntry fornisca sempre borderBoxSize, contentBoxSize e devicePixelContentBoxSize (a condizione che il browser lo supporti), il callback verrà richiamato solo se una delle metriche della casella osservata cambia.

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

Rilevamento delle funzionalità

Per verificare se il browser di un utente supporta devicePixelContentBox, possiamo osservare qualsiasi elemento e verificare se la proprietà è presente nella 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 c'era modo di conoscere il numero esatto di pixel fisici occupati da un elemento sullo schermo dell'utente. La nuova proprietà devicePixelContentBox su un ResizeObserverEntry ti fornisce questa informazione e ti consente di eseguire rendering perfetti di pixel con <canvas>. L'app devicePixelContentBox è supportata in Chrome 84 e versioni successive.