Idealne renderowanie dzięki urządzeniu devicePixelContentBox

Ile pikseli naprawdę mieści się na płótnie?

Od wersji 84 Chrome ResizeObserver obsługuje nowy pomiar wymiaru o nazwie devicePixelContentBox, który mierzy wymiary elementu w fizycznych pikselach. Umożliwia to renderowanie grafiki z dokładnością do piksela, zwłaszcza na ekranach o wysokiej gęstości pikseli.

Obsługa przeglądarek

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: nieobsługiwane.

Źródło

Często pracujemy z abstrakcyjnymi jednostkami długości, takimi jak em, % czy vh, ale wszystko sprowadza się do pikseli. Gdy określimy rozmiar lub położenie elementu w CSS, mechanizm układu przeglądarki przekształci tę wartość w piksele (px). Są to „piksele CSS”, które mają długą historię i tylko luźno powiązane z pikselami na ekranie.

Przez długi czas dość rozsądnie było szacować gęstość pikseli na ekranie na 96 DPI („punktów na cal”), co oznacza, że każdy monitor miałby około 38 pikseli na cm. Z czasem monitory się powiększały lub zmniejszały albo zaczęły mieć więcej pikseli na tej samej powierzchni. Dodajmy do tego fakt, że wiele treści w internecie ma wymiary, w tym rozmiary czcionek, zdefiniowane w px, a w efekcie otrzymujemy nieczytelny tekst na tych ekranach o wysokiej gęstości („HiDPI”). W ramach przeciwdziałania temu zjawisku przeglądarki ukrywają rzeczywistą gęstość pikseli monitora i udawają, że użytkownik ma wyświetlacz o rozdzielczości 96 DPI. Jednostka px w CSS reprezentuje rozmiar jednego piksela na tym wirtualnym ekranie o rozdzielczości 96 DPI, stąd nazwa „piksel CSS”. Ta jednostka służy tylko do pomiaru i pozycjonowania. Zanim nastąpi rzeczywiste renderowanie, następuje konwersja na fizyczne piksele.

Jak przejść z wirtualnego wyświetlacza na rzeczywisty wyświetlacz użytkownika? Wpisz devicePixelRatio. Ta wartość globalna określa, ile fizycznych pikseli jest potrzebnych do utworzenia pojedynczego piksela CSS. Jeśli devicePixelRatio (dPR) to 1, pracujesz na monitorze o rozdzielczości około 96 DPI. Jeśli masz ekran retina, wartość dPR wynosi 2. Na telefonach często występują wyższe (i dziwniejsze) wartości dPR, np. 2, 3 lub nawet 2.65. Pamiętaj, że ta wartość jest dokładna, ale nie pozwala określić rzeczywistej wartości DPI monitora. Wartość dPR 2 oznacza, że 1 piksel CSS będzie odpowiadał dokładnie 2 pikselom fizycznym.

Ma ona 3440 pikseli szerokości, a jej obszar wyświetlania ma 79 cm szerokości. Daje to rozdzielczość 110 DPI. Blisko 96, ale nie do końca. Z tego powodu na większości wyświetlaczy <div style="width: 1cm; height: 1cm"> nie będzie miał dokładnie 1 cm.

Wreszcie, na dPR może mieć wpływ funkcja powiększania w przeglądarce. Jeśli powiększysz widok, przeglądarka zwiększy wartość dPR, przez co wszystko będzie renderowane w większym rozmiarze. Jeśli podczas powiększania w konsoli DevTools zaznaczysz devicePixelRatio, zobaczysz wartości ułamkowe.

Narzędzia deweloperskie wyświetlają różne wartości ułamkowe devicePixelRatio z powodu powiększenia.

Dodajmy do tego element <canvas>. Za pomocą atrybutów width i height możesz określić, ile pikseli ma mieć kanwa. Wartość <canvas width=40 height=30> oznacza płótno o wymiarach 40 x 30 pikseli. Nie oznacza to jednak, że będzie ona wyświetlana w rozdzielczości 40 x 30 pikseli. Domyślnie płótno używa atrybutów widthheight do określenia swojego rozmiaru, ale możesz dowolnie zmieniać rozmiar płótna, korzystając ze wszystkich znanych i lubionych właściwości CSS. Z dotychczasowych obserwacji wynika, że nie zawsze jest to najlepsze rozwiązanie. Jeden piksel na kanwie może obejmować kilka fizycznych pikseli lub tylko ułamek fizycznego piksela. Może to prowadzić do nieprzyjemnych artefaktów wizualnych.

Podsumowując: elementy płótna mają określony rozmiar, który określa obszar, na którym możesz rysować. Liczba pikseli na płótnie jest całkowicie niezależna od rozmiaru wyświetlania płótna określonego w pikselach CSS. Liczba pikseli CSS nie jest taka sama jak liczba fizycznych pikseli.

Pixel perfection

W niektórych przypadkach przydatne jest dokładne mapowanie pikseli na płótnie na fizyczne piksele. Takie mapowanie nazywamy „idealnym”. Renderowanie z dokładnością do piksela jest kluczowe dla czytelnego renderowania tekstu, zwłaszcza przy użyciu renderowania subpikselowego lub wyświetlania grafiki z ściśle wyrównanymi liniami o zmiennej jasności.

Aby uzyskać w internecie jak najbardziej zbliżone do idealnego płótno, stosuje się mniej więcej takie podejście:

<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>

Bystry czytelnik może się zastanawiać, co się dzieje, gdy dPR nie jest liczbą całkowitą. To dobre pytanie i to jest sedno problemu. Jeśli dodatkowo określisz pozycję lub rozmiar elementu za pomocą wartości procentowych, vh lub innych wartości pośrednich, mogą one zostać przekształcone w wartości ułamkowe pikseli CSS. Element z margin-left: 33% może kończyć się prostokątem w taki sposób:

Narzędzia deweloperskie wyświetlają wartości ułamkowe pikseli w wyniku wywołania funkcji getBoundingClientRect().

Piksele CSS są całkowicie wirtualne, więc teoretycznie posiadanie ułamków piksela jest w porządku, ale jak przeglądarka ustala mapowanie na fizyczne piksele? Ponieważ ułamkowe fizyczne piksele nie istnieją.

Dopasowywanie do pikseli

Część procesu konwersji jednostek, która zajmuje się dopasowywaniem elementów do fizycznych pikseli, nazywa się „przyciąganiem do piksela”. Działa zgodnie z nazwą: przyciąga wartości ułamkowe do wartości całkowitych, fizycznych pikseli. Sposób, w jaki to działa, różni się w zależności od przeglądarki. Jeśli mamy element o szerokości 791.984px na wyświetlaczu, na którym dPR = 1, jedna przeglądarka może renderować element z użyciem 792px pikseli fizycznych, a inna z użyciem 791px. To tylko jeden piksel, ale nawet jeden piksel może mieć negatywny wpływ na renderowanie, które musi być idealne. Może to powodować rozmycie lub jeszcze bardziej widoczne artefakty, takie jak efekt moiré.

Górny obraz to raster z różnokolorowych pikseli. Obraz na dole jest taki sam jak powyżej, ale jego szerokość i wysokość zostały zmniejszone o 1 piksel za pomocą skalowania dwuliniowego. Powstały wzór nazywa się efektem Moiré.
(Możesz otworzyć ten obraz w nowej karcie, aby zobaczyć go bez skalowania.)

devicePixelContentBox

devicePixelContentBox podaje rozmiar pola treści elementu w pikselach urządzenia (czyli fizycznych pikselach). Należy do ResizeObserver. Chociaż ResizeObserver jest teraz obsługiwany we wszystkich głównych przeglądarkach od wersji Safari 13.1, właściwość devicePixelContentBox jest obecnie dostępna tylko w Chrome 84 lub nowszej.

Jak wspomniano w ResizeObserver: jest to jak document.onresize dla elementów, funkcja wywołania zwrotnego ResizeObserver zostanie wywołana przed narysowaniem i po ułożeniu. Oznacza to, że parametr entries wywołania zwrotnego będzie zawierać rozmiary wszystkich obserwowanych elementów tuż przed ich narysowaniem. W kontekście opisanego powyżej problemu z płótnem możemy wykorzystać tę okazję do dostosowania liczby pikseli na płótnie, aby zapewnić dokładne mapowanie pikseli na płótnie na piksele fizyczne.

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']});

Właściwość box w obiekcie opcji dla observer.observe() pozwala określić, które rozmiary chcesz obserwować. Oznacza to, że każdy element ResizeObserverEntry będzie zawsze zawierał wartości borderBoxSize, contentBoxSizedevicePixelContentBoxSize (o ile przeglądarka obsługuje te wartości), ale funkcja wywołania zwrotnego zostanie wywołana tylko wtedy, gdy zmieni się jakakolwiek wartość w polu obserwowane.

Dzięki tej nowej właściwości możemy nawet animować rozmiar i położenie kanwy (co skutecznie gwarantuje wartości ułamkowe pikseli) i nie obserwować efektu moiré podczas renderowania. Jeśli chcesz zobaczyć efekt Moiré przy użyciu getBoundingClientRect() i zobaczyć, jak nowa właściwość ResizeObserver pozwala go uniknąć, obejrzyj prezentację w Chrome 84 lub nowszej.

Wykrywanie cech

Aby sprawdzić, czy przeglądarka użytkownika obsługuje devicePixelContentBox, możemy obserwować dowolny element i sprawdzić, czy dana usługa jest obecna w ResizeObserverEntry:

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
}

Podsumowanie

Piksel jest w sieci zaskakująco złożonym tematem. Do tej pory nie było sposobu na dokładne poznanie liczby fizycznych pikseli zajmowanych przez element na ekranie użytkownika. Nowa właściwość devicePixelContentBox w usługach ResizeObserverEntry zawiera te informacje i umożliwia renderowanie z dokładnością do piksela za pomocą <canvas>. devicePixelContentBox jest obsługiwana w Chrome 84 i nowszych wersjach.