Wie viele Pixel sind tatsächlich in einem Canvas?
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. Dies 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 wurden Monitore größer und/oder kleiner oder es wurden mehr Pixel auf derselben Fläche untergebracht. 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 Auflösung („HiDPI“) unleserlichen Text. Als Gegenmaßnahme blenden Browser die tatsächliche Pixeldichte des Monitors aus und tun so, als hätte der Nutzer ein Display mit 96 DPI. Die Einheit px
in CSS entspricht der Größe eines Pixels auf diesem virtuellen Display mit 96 DPI. Daher der Name „CSS-Pixel“. Diese Einheit wird nur für Messungen und Positionierung verwendet. Bevor das eigentliche Rendering erfolgt, werden die Pixel in physische Pixel umgewandelt.
Wie gelangen wir von diesem virtuellen Display zum realen Display des Nutzers? Geben Sie devicePixelRatio
ein. Dieser globale Wert gibt an, wie viele physische Pixel Sie für ein einzelnes CSS-Pixel benötigen. Wenn devicePixelRatio
(dPR) 1
ist, arbeiten Sie auf einem 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
…Es hat eine Breite von 3.440 Pixeln und der Anzeigebereich ist 79 cm breit.
Das entspricht einer Auflösung von 110 dpi. Nah an 96, aber nicht ganz.
Das ist auch der Grund, warum ein <div style="width: 1cm; height: 1cm">
auf den meisten Bildschirmen nicht genau 1 cm groß ist.
Außerdem kann die dPR von der Zoomfunktion Ihres Browsers beeinflusst werden. Wenn Sie heranzoomen, erhöht der Browser die gemeldete dPR, wodurch alles größer dargestellt wird. Wenn Sie beim Zoomen in der DevTools-Konsole die Option devicePixelRatio
aktivieren, werden Bruchteile angezeigt.
Fügen wir dem Ganzen das Element <canvas>
hinzu. Mit den Attributen width
und height
kannst du 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 werden die Attribute width
und height
verwendet, um die Größe des Canvas zu definieren. 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 der Leinwand kann mehrere physische Pixel oder nur einen Bruchteil eines physischen Pixels abdecken. Das kann zu unschönen 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 entspricht nicht 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 erreicht wird, wird sie als „pixelgenau“ bezeichnet. Pixelgenaues Rendering ist entscheidend für die Lesbarkeit von Text, insbesondere bei der Verwendung von Subpixel-Rendering oder bei der Darstellung von Grafiken mit eng ausgerichteten Linien mit wechselnder Helligkeit.
Um im Web ein möglichst pixelgenaues Canvas zu erzielen, wurde bisher mehr oder weniger folgender Ansatz verwendet:
<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 haben. Aber wie ermittelt der Browser die Zuordnung zu physischen Pixeln? Weil es keine anteiligen physischen Pixel gibt.
Pixel Snap
Der Teil des Umwandlungsprozesses, der für die Ausrichtung von Elementen auf physische Pixel sorgt, wird als „Pixel-Snapping“ bezeichnet. Dabei werden anteilige Pixelwerte auf ganze, physische Pixelwerte gesnappt. 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. Das kann zu Unschärfe oder sogar zu stärker sichtbaren Artefakten wie dem Moiré-Effekt führen.
devicePixelContentBox
Mit devicePixelContentBox
sehen Sie das Inhaltsfeld eines Elements in Gerätepixeln (d.h. physischen Pixeln). Es ist Teil von ResizeObserver
. ResizeObserver wird seit Safari 13.1 in allen gängigen Browsern unterstützt. Die Eigenschaft devicePixelContentBox
ist derzeit nur in Chrome 84 und höher verfügbar.
Wie unter ResizeObserver
: 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 Property box
im Optionsobjekt für observer.observe()
können Sie festlegen, welche Größen beobachtet werden sollen. Jede ResizeObserverEntry
liefert zwar immer borderBoxSize
, contentBoxSize
und devicePixelContentBoxSize
(vorausgesetzt, der Browser unterstützt dies), der Rückruf wird jedoch nur aufgerufen, wenn sich einer der Messwerte im Feld observed ä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 es Ihnen, mit <canvas>
pixelgenaue Renderings zu erstellen. devicePixelContentBox
wird in Chrome 84 und höher unterstützt.