Perfektes Rendering auf Pixel mit devicePixelContentBox

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.

Unterstützte Browser

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

Quelle

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.

Beispiel
Mein Monitor hat laut Chrome eine dPR von 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.

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

Entwicklertools, die als Ergebnis eines getBoundingClientRect()-Aufrufs Pixelwerte für Bruchteile anzeigen.

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.

Das obere Bild ist ein Raster mit Pixeln in verschiedenen Farben. Das untere Bild ist dasselbe wie oben, allerdings wurden Breite und Höhe mithilfe der bilinearen 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

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.