Perfektes Rendering auf Pixel mit devicePixelContentBox

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.

Unterstützte Browser

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: Nicht unterstützt.

Quelle

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.

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.

In den Entwicklertools werden aufgrund des Zoomens verschiedene Bruchteile von devicePixelRatio 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:

In den DevTools werden nach einem getBoundingClientRect()-Aufruf Bruchteilpixelwerte angezeigt.

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.

Das obere Bild ist ein Raster aus verschiedenfarbigen Pixeln. Das untere Bild ist mit dem obigen identisch, aber Breite und Höhe wurden durch bilineare Skalierung um ein Pixel reduziert. Das entstehende Muster wird als Moiré-Effekt bezeichnet.
Möglicherweise müssen Sie dieses Bild in einem neuen Tab öffnen, um es ohne Skalierung zu sehen.

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.