Wie viele Pixel hat ein Canvas wirklich?
Seit Chrome 84 unterstützt ResizeObserver eine neue Box-Messung namens devicePixelContentBox
, mit der die Größe des Elements in physischen Pixeln gemessen wird. So können pixelgenaue Grafiken gerendert werden, insbesondere bei Bildschirmen mit hoher Pixeldichte.
Hintergrund: CSS-Pixel, Canvas-Pixel und physische Pixel
Wir arbeiten zwar oft mit abstrakten Längeneinheiten wie em
, %
oder vh
, aber im Grunde geht es immer um Pixel. Wenn wir die Größe oder Position eines Elements in CSS angeben, wandelt das Layout-Engine des Browsers diesen Wert in Pixel (px
) um. Das sind „CSS-Pixel“, die eine lange Geschichte haben und nur einen losen Bezug zu den Pixeln auf dem Bildschirm haben.
Lange Zeit war es ziemlich vernünftig, die Pixeldichte eines Bildschirms mit 96 DPI („Dots per Inch“, also „Punkte pro Zoll“) zu schätzen. Das bedeutet, dass ein bestimmter Monitor etwa 38 Pixel pro Zentimeter hat. Im Laufe der Zeit wuchsen und/oder verkleinerten die Bildschirme bzw. verkleinerten sie oder hatten mehr Pixel auf derselben Oberfläche. Wenn Sie dazu noch berücksichtigen, dass viele Inhalte im Web ihre Abmessungen, einschließlich Schriftgrößen, in px
definieren, erhalten Sie auf diesen Bildschirmen mit hoher Pixeldichte („HiDPI“) unleserlichen Text. Als Gegenmaßnahme blenden die Browser die tatsächliche Pixeldichte des Monitors aus und stellen sich stattdessen als ein 96-DPI-Display vor. Die Einheit px
in CSS steht für die Größe eines Pixels auf diesem virtuellen 96-DPI-Display, daher der Name „CSS-Pixel“. Diese Einheit wird nur zum Messen und Positionieren verwendet. Bevor das eigentliche Rendering erfolgt, wird eine Umwandlung in physische Pixel durchgeführt.
Wie gelangen wir von diesem virtuellen Display zum echten Bildschirm des Nutzers? Geben Sie devicePixelRatio
ein. Dieser globale Wert gibt an, wie viele physische Pixel Sie benötigen, um ein einzelnes CSS-Pixel zu bilden. Ist der Wert für devicePixelRatio
(dPR) auf 1
gesetzt, handelt es sich um einen Monitor mit etwa 96 DPI. Wenn Sie ein Retina-Display haben, ist Ihre dPR wahrscheinlich 2
. Auf Smartphones sind höhere (und seltsamere) dPR-Werte wie 2
, 3
oder sogar 2.65
keine Seltenheit. Dieser Wert ist genau, aber er lässt sich nicht in den tatsächlichen DPI-Wert des Monitors umrechnen. Ein dPR von 2
bedeutet, dass 1 CSS-Pixel genau 2 physischen Pixeln zugeordnet wird.
1
…Sie ist 3.440 Pixel breit und der Anzeigebereich beträgt 79 cm.
Das führt zu einer Auflösung von 110 DPI. Nah an 96, aber nicht ganz.
Das ist auch der Grund, warum <div style="width: 1cm; height: 1cm">
bei den meisten Displays nicht genau die Größe von 1 cm misst.
Außerdem kann die dPR von der Zoomfunktion Ihres Browsers beeinflusst werden. Beim Heranzoomen erhöht der Browser den gemeldeten dPR, wodurch alles größer dargestellt wird. Wenn du beim Zoomen in einer Entwicklertools-Konsole devicePixelRatio
aktivierst, werden Bruchwerte angezeigt.
Fügen Sie dem Mix nun das Element <canvas>
hinzu. Mit den Attributen width
und height
können Sie angeben, wie viele Pixel der Canvas haben soll. <canvas width=40 height=30>
wäre also ein Canvas mit 40 × 30 Pixeln. Das bedeutet jedoch nicht, dass es angezeigt wird. Standardmäßig wird die Canvas-Größe mit den Attributen width
und height
definiert. Sie können die Größe des Canvas jedoch mit allen bekannten CSS-Properties beliebig ändern. Angesichts all dessen, was wir bisher gelernt haben, ist Ihnen vielleicht klar, dass dies nicht in jedem Szenario ideal ist. Ein Pixel auf dem Canvas kann mehrere physische Pixel abdecken oder nur einen Bruchteil eines physischen Pixels. Dies kann zu unangenehmen visuellen Artefakten führen.
Zusammenfassung: Canvas-Elemente haben eine bestimmte Größe, um den Bereich zu definieren, auf dem Sie zeichnen können. Die Anzahl der Canvas-Pixel ist unabhängig von der Anzeigegröße des Canvas, die in CSS-Pixeln angegeben wird. Die Anzahl der CSS-Pixel ist nicht identisch mit der Anzahl der physischen Pixel.
Pixelgenau
In einigen Fällen ist eine genaue Zuordnung von Canvas-Pixeln zu physischen Pixeln wünschenswert. Wenn diese Zuordnung erzielt wird, spricht man von „pixel-perfect“. Ein perfektes Pixel-Rendering ist für die Lesbarkeit von Text entscheidend, insbesondere bei Subpixel-Rendering oder bei der Darstellung von Grafiken mit eng ausgerichteten Linien mit abwechselnder Helligkeit.
Um einem Canvas im Web so nahe wie möglich zu kommen, war dies mehr oder weniger der Standard-Ansatz:
<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>
Der aufmerksame Leser fragt sich vielleicht, was passiert, wenn dPR kein Ganzzahlwert ist. Das ist eine gute Frage und genau hier liegt der Kern des Problems. Wenn Sie die Position oder Größe eines Elements mithilfe von Prozentsätzen, vh
oder anderen indirekten Werten angeben, werden diese möglicherweise in Bruchteile von CSS-Pixelwerten aufgelöst. Ein Element mit margin-left: 33%
kann zu einem Rechteck wie diesem führen:
CSS-Pixel sind rein virtuell. Daher ist es theoretisch in Ordnung, Bruchteile eines Pixels zu verwenden. Aber wie findet der Browser die Zuordnung zu physischen Pixeln? Weil physische Bruchpixel nichts sind.
Pixel Snap
Der Teil des Einheitenkonvertierungsprozesses, der die Ausrichtung von Elementen an physischen Pixeln vornimmt, wird als „Pixel-Fangen“ bezeichnet. Dabei wird das, was auf der Zinn steht, ausgeführt: Es werden Bruchwerte von Pixelwerten an ganzzahlige physische Pixelwerte angedockt. Wie genau das funktioniert, unterscheidet sich von Browser zu Browser. Wenn wir ein Element mit einer Breite von 791.984px
auf einem Display mit dPR = 1 haben, kann ein Browser das Element mit 792px
Pixeln rendern, während ein anderer Browser es mit 791px
Pixeln rendert. Das ist nur ein einziges Pixel, aber ein einziges Pixel kann für Renderings, die pixelgenau sein müssen, schädlich sein. Dies kann zu Unschärfen oder sogar noch sichtbareren Artefakten wie dem Moiré-Effekt führen.
devicePixelContentBox
devicePixelContentBox
gibt den Inhaltsfeld eines Elements in Gerätepixeleinheiten an, d.h. physische Pixel. Es ist Teil von ResizeObserver
. Während ResizeObserver mittlerweile in allen gängigen Browsern unterstützt wird, ist die Eigenschaft devicePixelContentBox
vorerst nur in Chrome 84 und höher verfügbar.
Wie unter ResizeObserver
: ResizeObserver
ist wie document.onresize
für Elemente erwähnt, wird die Callback-Funktion eines ResizeObserver
vor dem Malen und nach dem Layout aufgerufen. Das bedeutet, dass der Parameter entries
für den Rückruf die Größen aller beobachteten Elemente enthält, kurz bevor sie gerendert werden. Im Zusammenhang mit dem oben beschriebenen Canvas-Problem können wir diese Gelegenheit nutzen, um die Anzahl der Pixel auf unserem Canvas anzupassen und so für eine genaue Zuordnung zwischen Canvas-Pixeln und physischen Pixeln zu sorgen.
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']});
Mit der Eigenschaft box
im Optionsobjekt für observer.observe()
können Sie definieren, welche Größen beobachtet werden sollen. Obwohl jeder ResizeObserverEntry
immer borderBoxSize
, contentBoxSize
und devicePixelContentBoxSize
bereitstellt (sofern der Browser dies unterstützt), wird der Callback nur aufgerufen, wenn sich einer der beobachteten Messwerte des Felds ändert.
Mit dieser neuen Eigenschaft können wir sogar die Größe und Position des Canvas animieren (was effektiv die Verwendung von Bruchteilpixelwerten ermöglicht) und keine Moiré-Effekte beim Rendering sehen. Wenn Sie sich den Moiré-Effekt bei der Verwendung von getBoundingClientRect()
ansehen und sehen möchten, wie Sie ihn mit der neuen Eigenschaft ResizeObserver
vermeiden können, sehen Sie sich die Demo in Chrome 84 oder höher an.
Funktionserkennung
Um zu prüfen, ob der Browser eines Nutzers devicePixelContentBox
unterstützt, können wir ein beliebiges Element beobachten und prüfen, ob die Property im ResizeObserverEntry
vorhanden ist:
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
}
Fazit
Pixel sind ein überraschend komplexes Thema im Web. Bisher gab es keine Möglichkeit, die genaue Anzahl der physischen Pixel zu ermitteln, die ein Element auf dem Bildschirm des Nutzers einnimmt. Die neue devicePixelContentBox
-Eigenschaft auf einer ResizeObserverEntry
liefert diese Informationen und ermöglicht pixelgenaue Renderings mit <canvas>
. devicePixelContentBox
wird in Chrome 84 und höher unterstützt.