Quanti pixel ci sono davvero in un canvas?
A partire da Chrome 84, ResizeObserver supporta una nuova misurazione della casella chiamata devicePixelContentBox
, che misura la dimensione dell'elemento in pixel fisici. Ciò consente di eseguire il rendering di grafiche perfette al pixel, soprattutto nel contesto di schermi ad alta densità.
Sfondo: pixel CSS, pixel canvas e pixel fisici
Anche se spesso lavoriamo con unità di lunghezza astratte come em
, %
o vh
, 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 converte quel valore in pixel (px
). Questi sono "pixel CSS", che hanno una lunga storia e hanno solo un rapporto approssimativo 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 un determinato monitor avrebbe avuto circa 38 pixel per cm. Nel tempo, i monitor sono cresciuti e/o si sono ridotti o hanno iniziato ad avere più pixel sulla stessa superficie. Se a questo aggiungiamo il fatto che molti contenuti sul web definiscono le proprie dimensioni, comprese le dimensioni dei caratteri, in px
, otteniamo un testo illeggibile su questi schermi 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 cui 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 si passa 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, il dPR è probabilmente 2
. Sugli smartphone non è raro riscontrare valori dPR più elevati (e più strani) come 2
, 3
o persino 2.65
. È importante notare che questo valore è esatto, ma non consente di derivare il valore DPI effettivo del monitor. Un dPR di 2
significa che 1 pixel CSS corrisponderà esattamente a 2 pixel fisici.
1
…Ha una larghezza di 3440 pixel e l'area di visualizzazione è larga 79 cm.
Ciò porta 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 misurerà esattamente 1 cm sulla maggior parte dei display.
Infine, il dPR può essere influenzato anche dalla funzionalità di zoom del browser. Se aumenti lo zoom, il browser aumenta il dPR segnalato, causando un rendering più grande di tutti i contenuti. Se selezioni devicePixelRatio
in una console DevTools durante lo zoom, puoi visualizzare i valori frazionari.

devicePixelRatio
frazionari a causa dello zoom.Aggiungiamo l'elemento <canvas>
. Puoi specificare il numero di pixel che vuoi che abbia il canvas utilizzando gli attributi width
e height
. Quindi <canvas width=40 height=30>
sarebbe un canvas di 40 x 30 pixel. Tuttavia, ciò non significa che verrà visualizzato a 40 x 30 pixel. Per impostazione predefinita, il canvas utilizza gli attributi width
e height
per definire le dimensioni intrinseche, ma puoi ridimensionarlo in modo arbitrario utilizzando tutte le proprietà CSS che conosci e apprezzi. Con tutto quello che abbiamo imparato finora, ti sarà chiaro che questa soluzione non è ideale in ogni scenario. Un pixel sul canvas potrebbe finire per coprire più pixel fisici o solo una frazione di un pixel fisico. Ciò può portare a artefatti visivi sgradevoli.
In sintesi, gli elementi 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.
Perfezione di Pixel
In alcuni scenari, è preferibile avere una mappatura esatta dai pixel del canvas ai pixel fisici. Se questo mapping viene raggiunto, viene definito "pixel perfetto". Il rendering perfetto al pixel è fondamentale per la leggibilità del testo, soprattutto quando si utilizza il rendering subpixel o quando si visualizzano elementi grafici con linee strettamente allineate di luminosità alternata.
Per ottenere qualcosa di simile a una tela perfetta al pixel 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 attento potrebbe chiedersi cosa succede quando dPR non è un valore intero. Questa è un'ottima domanda e il fulcro dell'intero 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 di pixel CSS frazionari. Un elemento con margin-left: 33%
può avere un rettangolo come questo:

getBoundingClientRect()
.I pixel CSS sono puramente virtuali, quindi avere frazioni di pixel è teoricamente accettabile, ma in che modo il browser determina la mappatura ai pixel fisici? Perché i pixel fisici frazionari non esistono.
Allineamento dei pixel
La parte del processo di conversione delle unità che si occupa di allineare gli elementi ai pixel fisici è chiamata "snap dei pixel" e fa esattamente ciò che dice: allinea i valori frazionari dei pixel ai valori interi dei pixel fisici. La modalità esatta di esecuzione varia a seconda del browser. Se abbiamo un elemento con una larghezza di 791.984px
su un display in cui il dPR è 1, un browser potrebbe eseguire il rendering dell'elemento a 792px
pixel fisici, mentre un altro browser potrebbe eseguirne il rendering a 791px
. Si tratta di un solo pixel di differenza, ma un singolo pixel può compromettere i rendering che devono essere perfetti. Ciò può causare sfocatura o artefatti ancora più visibili come l'effetto moiré.

(Potresti dover aprire questa immagine in una nuova scheda per visualizzarla senza alcuna scalatura.)
devicePixelContentBox
devicePixelContentBox
mostra la casella dei contenuti di un elemento in unità di pixel del dispositivo (ovvero pixel fisici). Fa parte di ResizeObserver
. Sebbene ResizeObserver sia ora supportato in tutti i principali browser a partire da Safari 13.1, la proprietà devicePixelContentBox
è disponibile solo in Chrome 84 e versioni successive per il momento.
Come indicato in ResizeObserver
: è come document.onresize
per gli elementi, la funzione di callback di un ResizeObserver
verrà chiamata prima del rendering 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 della tela descritto sopra, possiamo utilizzare questa opportunità per regolare il numero di pixel sulla tela, assicurandoci di ottenere una mappatura esatta uno a uno 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 delle opzioni per observer.observe()
ti consente di definire le dimensioni che vuoi osservare. Pertanto, anche se ogni ResizeObserverEntry
fornirà 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 del canvas (garantendo di fatto valori di pixel frazionari) e non visualizzare alcun effetto moiré nel 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 delle funzionalità
Per verificare se il browser di un utente supporta devicePixelContentBox
, possiamo osservare qualsiasi elemento e controllare se la proprietà è presente in 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
su un ResizeObserverEntry
ti fornisce queste informazioni e ti consente di eseguire rendering perfetti al pixel con <canvas>
. devicePixelContentBox
è supportato in Chrome 84 e versioni successive.