Perfektes Rendering auf Pixel mit devicePixelContentBox

Wie viele Pixel hat eine Leinwand wirklich?

Seit Chrome 84 unterstützt ResizeObserver eine neue Feldmessung namens devicePixelContentBox, mit der die Abmessungen des Elements in physischen Pixeln gemessen werden. So können pixelgenaue Grafiken gerendert werden, insbesondere auf Displays mit hoher Pixeldichte.

Browser Support

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: not supported.

Source

Hintergrund: CSS-Pixel, Canvas-Pixel und physische Pixel

Wir arbeiten zwar oft mit abstrakten Längeneinheiten wie em, % oder vh, aber letztendlich geht es immer um Pixel. Wenn wir die Größe oder Position eines Elements in CSS angeben, konvertiert die Layout-Engine des Browsers diesen Wert schließlich in Pixel (px). Das sind „CSS-Pixel“, die eine lange Geschichte haben und nur eine lose Beziehung zu den Pixeln auf Ihrem Bildschirm haben.

Lange Zeit war es relativ einfach, die Pixeldichte eines Bildschirms mit 96 DPI („dots per inch“, deutsch: Punkte pro Zoll) zu schätzen. Das bedeutet, dass ein beliebiger Monitor etwa 38 Pixel pro Zentimeter hat. Im Laufe der Zeit wurden Monitore größer und/oder kleiner oder hatten mehr Pixel auf derselben Fläche. Da viele Inhalte im Web ihre Abmessungen, einschließlich Schriftgrößen, in px definieren, ist der Text auf diesen Bildschirmen mit hoher Pixeldichte („HiDPI“) oft schwer lesbar. Als Gegenmaßnahme blenden Browser die tatsächliche Pixeldichte des Monitors aus und geben stattdessen vor, dass der Nutzer ein Display mit 96 DPI hat. 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 für Messungen und die Positionierung verwendet. Bevor das Rendering erfolgt, werden die Einheiten in physische Pixel umgerechnet.

Wie gelangen wir von dieser virtuellen Anzeige zum tatsächlichen Display des Nutzers? Geben Sie devicePixelRatio ein. Dieser globale Wert gibt an, wie viele physische Pixel für ein einzelnes CSS-Pixel erforderlich sind. Wenn devicePixelRatio (Geräte-Pixelverhältnis) 1 ist, arbeiten Sie auf einem Monitor mit etwa 96 DPI. Wenn Sie ein Retina-Display haben, ist Ihr dPR wahrscheinlich 2. Auf Smartphones sind höhere (und ungewöhnlichere) dPR-Werte wie 2, 3 oder sogar 2.65 nicht ungewöhnlich. Dieser Wert ist exakt, lässt aber keine Rückschlüsse auf den tatsächlichen DPI-Wert des Monitors zu. Ein dPR von 2 bedeutet, dass 1 CSS-Pixel genau 2 physischen Pixeln entspricht.

Beispiel
Mein Monitor hat laut Chrome einen dPR von 1

Es hat eine Breite von 3.440 Pixeln und der Anzeigebereich ist 79 cm breit. Das führt zu einer Auflösung von 110 DPI. Fast 96, aber nicht ganz. Das ist auch der Grund, warum ein <div style="width: 1cm; height: 1cm"> auf den meisten Displays nicht genau 1 cm groß ist.

Schließlich kann sich auch die Zoomfunktion Ihres Browsers auf den dPR auswirken. Wenn Sie heranzoomen, erhöht der Browser den gemeldeten DPR-Wert, sodass alles größer gerendert wird. Wenn Sie devicePixelRatio in der DevTools-Konsole prüfen, während Sie zoomen, werden Bruchwerte angezeigt.

In den Entwicklertools werden aufgrund des Zoomens verschiedene Bruchwerte für devicePixelRatio angezeigt.

Fügen wir das Element <canvas> hinzu. Mit den Attributen width und height können Sie angeben, wie viele Pixel das Canvas haben soll. <canvas width=40 height=30> wäre also ein Canvas mit 40 × 30 Pixeln. Das bedeutet jedoch nicht, dass es mit 40 × 30 Pixeln angezeigt wird. Standardmäßig wird für die intrinsische Größe des Canvas das Attribut width und height verwendet. Sie können die Größe des Canvas aber auch mit allen bekannten CSS-Properties anpassen. Nach allem, was wir bisher gelernt haben, ist Ihnen vielleicht schon klar, dass dies nicht in jedem Szenario ideal ist. Ein Pixel auf dem Canvas kann mehrere physische Pixel oder nur einen Bruchteil eines physischen Pixels abdecken. Dies kann zu unschönen visuellen Artefakten führen.

Zusammenfassend lässt sich sagen, dass Canvas-Elemente eine bestimmte Größe haben, um den Bereich zu definieren, in dem Sie zeichnen können. Die Anzahl der Canvas-Pixel ist völlig unabhängig von der Anzeigegröße des Canvas, die in CSS-Pixeln angegeben wird. Die Anzahl der CSS-Pixel ist nicht mit der Anzahl der physischen Pixel identisch.

Pixelgenauigkeit

In einigen Fällen ist es wünschenswert, eine genaue Zuordnung von Canvas-Pixeln zu physischen Pixeln zu haben. Wenn diese Zuordnung erreicht wird, spricht man von „pixelgenau“. Die pixelgenaue Darstellung ist entscheidend für die lesbare Darstellung von Text, insbesondere bei Verwendung von Subpixel-Rendering oder bei der Darstellung von Grafiken mit eng ausgerichteten Linien mit abwechselnder Helligkeit.

Um im Web ein möglichst pixelgenaues Canvas zu erhalten, war dies mehr oder weniger der Standardansatz:

<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 ganzzahliger Wert ist. Das ist eine gute Frage und genau hier liegt der Knackpunkt des Problems. Wenn Sie die Position oder Größe eines Elements mit Prozentangaben, vh oder anderen indirekten Werten angeben, kann es außerdem sein, dass sie in CSS-Pixelbruchteile aufgelöst werden. Ein Element mit margin-left: 33% kann so aussehen:

DevTools mit fraktionierten Pixelwerten als Ergebnis eines getBoundingClientRect()-Aufrufs.

CSS-Pixel sind rein virtuell. Daher sind Bruchteile eines Pixels theoretisch in Ordnung. Aber wie ordnet der Browser sie physischen Pixeln zu? Da es keine physischen Pixel mit Bruchteilen gibt.

Pixel-Snapping

Der Teil des Einheitenkonvertierungsprozesses, der für die Ausrichtung von Elementen an physischen Pixeln zuständig ist, wird als „Pixel-Snapping“ bezeichnet. Dabei werden gebrochene Pixelwerte in ganzzahlige, physische Pixelwerte umgewandelt. Wie genau das geschieht, ist von Browser zu Browser unterschiedlich. Wenn wir ein Element mit einer Breite von 791.984px auf einem Display mit einem Gerätepixelverhältnis von 1 haben, rendert ein Browser das Element möglicherweise mit 792px physischen Pixeln, ein anderer Browser mit 791px. Das ist nur ein Pixel, aber ein einzelnes Pixel kann sich negativ auf Renderings auswirken, die pixelgenau sein müssen. Das kann zu Unschärfe oder noch sichtbaren Artefakten wie dem Moiré-Effekt führen.

Das obere Bild ist ein Raster aus verschiedenfarbigen Pixeln. Das untere Bild ist dasselbe wie oben, 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 auf einem neuen Tab öffnen, um es ohne Skalierung zu sehen.)

devicePixelContentBox

devicePixelContentBox gibt die Contentbox eines Elements in Gerätepixeln (d.h. physischen Pixeln) an. Sie ist Teil von ResizeObserver. ResizeObserver wird seit Safari 13.1 in allen wichtigen Browsern unterstützt, die devicePixelContentBox-Eigenschaft ist derzeit jedoch nur in Chrome 84 und höher verfügbar.

Wie in ResizeObserver: it's like document.onresize for elements erwähnt, wird die Callback-Funktion von ResizeObserver vor dem Rendern und nach dem Layout aufgerufen. Das bedeutet, dass der Parameter entries für den Callback die Größen aller beobachteten Elemente kurz vor dem Rendern enthält. Im Zusammenhang mit dem oben beschriebenen Problem mit dem Canvas können wir die Gelegenheit nutzen, die Anzahl der Pixel auf dem Canvas anzupassen, um eine genaue 1:1-Zuordnung zwischen Canvas-Pixeln und physischen Pixeln zu erreichen.

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 dem Attribut box im Optionenobjekt für observer.observe() können Sie festlegen, welche Größen beobachtet werden sollen. Jede ResizeObserverEntry liefert also immer borderBoxSize, contentBoxSize und devicePixelContentBoxSize (sofern der Browser dies unterstützt). Der Callback wird jedoch nur aufgerufen, wenn sich einer der beobachteten Messwerte für das Feld ändert.

Mit dieser neuen Eigenschaft können wir sogar die Größe und Position unseres Canvas animieren (was effektiv Bruchpixelwerte garantiert), ohne dass Moiré-Effekte beim Rendern auftreten. Wenn Sie den Moiré-Effekt bei der Verwendung von getBoundingClientRect() sehen möchten und erfahren 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 Eigenschaft 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. Das neue Attribut devicePixelContentBox für ein ResizeObserverEntry liefert diese Information und ermöglicht pixelgenaue Renderings mit <canvas>. devicePixelContentBox wird in Chrome 84 und höher unterstützt.