Perfektes Rendering auf Pixel mit devicePixelContentBox

Wie viele Pixel enthält ein Canvas wirklich?

Seit Chrome 84 unterstützt ResizeObserver eine neue Feldmessung mit dem Namen devicePixelContentBox. Damit wird die Größe des Elements in physischen Pixeln gemessen. Dies ermöglicht das Rendering pixeliger Grafiken, insbesondere im Kontext von hochauflösenden Bildschirmen.

Unterstützte Browser

  • 84
  • 84
  • 93
  • x

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

Wir arbeiten zwar oft mit abstrakten Längeneinheiten wie em, % oder vh, aber alles läuft auf Pixel hinaus. Immer, 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). Dabei handelt es sich um sogenannte CSS-Pixel, die bereits über einen langen Verlauf verfügen und nur in freier Weise mit den Pixeln auf Ihrem Bildschirm in Beziehung stehen.

Lange Zeit war es recht vernünftig, die Bildschirmpixeldichte mit 96 DPI (Punkten pro Zoll) zu schätzen. Das bedeutet, dass jeder Bildschirm ungefähr 38 Pixel pro Zentimeter haben würde. Im Laufe der Zeit wuchsen bzw. verkleinerte die Monitore und zeigten mehr Pixel auf derselben Oberfläche. Zusammen mit der Tatsache, dass bei vielen Inhalten im Web ihre Abmessungen, einschließlich der Schriftgrößen, in px definiert werden, wird auf diesen Bildschirmen mit hoher Dichte (HiDPI) unleserlicher Text angezeigt. Als Gegenmaßnahme verbergen Browser die tatsächliche Pixeldichte des Monitors und geben stattdessen vor, der Nutzer hätte eine 96-DPI-Anzeige. 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 zur Messung und Positionierung verwendet. Vor dem eigentlichen Rendering erfolgt eine Konvertierung in physische Pixel.

Wie gelangen wir von diesem virtuellen Bildschirm zum echten Display 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. Wenn devicePixelRatio (dPR) 1 ist, arbeiten Sie an einem Monitor mit etwa 96 DPI. Bei einem Netzhautbildschirm ist der dPR wahrscheinlich 2. Auf Smartphones sind höhere (und seltsamere) dPR-Werte wie 2, 3 oder sogar 2.65 nicht ungewöhnlich. Es ist wichtig, dass dieser Wert exakt ist, Sie können jedoch den tatsächlichen DPI-Wert des Monitors nicht ableiten. Ein dPR von 2 bedeutet, dass 1 CSS-Pixel genau 2 physischen Pixeln zugeordnet wird.

Beispiel
Mein Monitor hat laut Chrome einen dPR-Wert von 1...

Es ist 3440 Pixel breit und der Anzeigebereich ist 79 cm breit. Das führt zu einer Auflösung von 110 dpi. Fast bei 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 die dPR auch von der Zoomfunktion Ihres Browsers beeinflusst werden. Beim Heranzoomen erhöht der Browser den gemeldeten dPR-Wert, sodass alles größer dargestellt wird. Wenn du beim Zoomen in der Entwicklertools-Konsole devicePixelRatio auswählst, werden Bruchwerte angezeigt.

In den Entwicklertools wird aufgrund des Zooms eine Vielzahl von devicePixelRatio als Bruch angezeigt.

Fügen wir dem Mix das Element <canvas> hinzu. Mit den Attributen width und height können Sie festlegen, wie viele Pixel auf dem Canvas angezeigt werden sollen. <canvas width=40 height=30> wäre also ein Canvas mit 40 × 30 Pixeln. Das bedeutet jedoch nicht, dass sie bei 40 x 30 Pixeln angezeigt wird. Standardmäßig werden für den Canvas die Attribute width und height verwendet, um die ursprüngliche Größe zu definieren. Sie können die Größe des Canvas jedoch beliebig mit all Ihren vertrauten CSS-Eigenschaften anpassen. Bei allem, was wir bisher gelernt haben, wird Ihnen vielleicht einfallen, dass dies nicht in jedem Szenario ideal ist. Ein Pixel auf dem Canvas könnte schließlich mehrere physische Pixel oder nur einen Bruchteil eines physischen Pixels abdecken. Dies kann zu unangenehmen visuellen Artefakten führen.

Zusammenfassend lässt sich sagen, dass Canvas-Elemente eine bestimmte Größe haben, um den Bereich zu definieren, auf 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 dasselbe wie die Anzahl der physischen Pixel.

Pixel-Perfektion

In einigen Szenarien ist es wünschenswert, eine genaue Zuordnung von Canvas-Pixeln zu physischen Pixeln zu haben. Diese Zuordnung wird als „pixelperfect“ bezeichnet. Für ein gut lesbares Rendering von Text ist ein genaues Rendering von entscheidender Bedeutung, insbesondere bei Subpixel-Rendering oder bei der Darstellung von Grafiken mit eng aufeinander ausgerichteten Linien abwechselnder Helligkeit.

Um im Web eine möglichst Pixel-perfekte Leinwand zu erreichen, ist dies mehr oder weniger der bevorzugte 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 genaue Leser fragt sich vielleicht, was passiert, wenn dPR kein Ganzzahlwert ist. Das ist eine gute Frage und der Kern des gesamten Problems liegt. Wenn Sie außerdem die Position oder Größe eines Elements mithilfe von Prozentsätzen, vh oder anderen indirekten Werten angeben, kann es vorkommen, dass diese in CSS-Pixelwerte als Bruchwerte aufgelöst werden. Ein Element mit margin-left: 33% kann am Ende ein Rechteck wie dieses haben:

Die Entwicklertools zeigen Pixelwerte als Bruchwerte als Ergebnis eines getBoundingClientRect()-Aufrufs an.

CSS-Pixel sind rein virtuell, sodass es theoretisch in Ordnung ist, Bruchteile eines Pixels zu haben. Aber wie findet der Browser die Zuordnung zu physischen Pixeln? Bruchteile physische Pixel sind keine Sache.

Andocken an Pixel

Der Teil der Einheitenumrechnung, der das Ausrichten von Elementen an physischen Pixeln durchführt, wird als „Pixel-Andocken“ bezeichnet und tut, was auf der Zinn steht: Sie ordnet gebrochene Pixelwerte zu ganzzahligen, physischen Pixelwerten zu. Wie genau das funktioniert, ist von Browser zu Browser unterschiedlich. Wenn wir ein Element mit der Breite 791.984px auf einem Display haben, bei dem der dPR-Wert 1 ist, könnte ein Browser das Element mit 792px physischen Pixeln und ein anderer Browser mit 791px rendern. Das ist zwar nur ein Pixel entfernt, aber ein einzelnes Pixel kann sich nachteilig auf Renderings auswirken, die eine Pixelperfekte erzielen müssen. Dies kann zu Unschärfe oder noch stärker sichtbaren Artefakten wie dem Moiré-Effekt führen.

Das obere Bild ist ein Raster aus Pixeln in unterschiedlichen Farben. Das untere Bild ist dasselbe wie oben, aber die Breite und Höhe wurden 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 das Inhaltsfeld eines Elements in Gerätepixeleinheiten (physische Pixeleinheiten) an. Es gehört zu ResizeObserver. Seit Safari 13.1 wird ResizeObserver jetzt in allen gängigen Browsern unterstützt, die Eigenschaft devicePixelContentBox ist derzeit aber nur in Chrome 84 und höher verfügbar.

Wie bereits unter ResizeObserver erwähnt: Ähnelt document.onresize für Elemente, wird die Callback-Funktion einer ResizeObserver vor dem Paint und nach dem Layout aufgerufen. Das bedeutet, dass der entries-Parameter für den Callback die Größen aller beobachteten Elemente enthält, bevor diese dargestellt werden. Im Kontext unseres oben beschriebenen Canvas-Problems können wir diese Gelegenheit nutzen, um die Anzahl der Pixel auf dem Canvas anzupassen, um sicherzustellen, dass wir am Ende eine genaue 1:1-Zuordnung zwischen Canvas-Pixeln und physischen Pixeln erhalten.

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 dann aufgerufen, wenn sich einer der beobachteten Feldmesswerte ändert.

Mit dieser neuen Eigenschaft können wir sogar die Größe und Position unseres Canvas animieren, was praktisch Bruchwerte für Pixelwerte darstellt. Moiré-Effekte auf das Rendering werden dadurch nicht mehr deutlich. Wenn du wissen möchtest, wie sich die Verwendung von getBoundingClientRect() auf Moiré auswirkt und wie du es mit der neuen ResizeObserver-Eigenschaft vermeiden kannst, sieh dir 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 ist ein erstaunlich komplexes Thema im Web. Bisher gab es keine Möglichkeit, die genaue Anzahl physischer Pixel zu ermitteln, die ein Element auf dem Bildschirm des Nutzers einnimmt. Die neue devicePixelContentBox-Eigenschaft einer ResizeObserverEntry liefert dir diese Information und ermöglicht dir Pixel-perfekte Renderings mit <canvas>. devicePixelContentBox wird in Chrome 84 und höher unterstützt.