Rendering sempurna piksel dengan devicePixelContentBox

Berapa banyak {i>pixel<i} yang sebenarnya ada dalam kanvas?

Sejak Chrome 84, ResizeObserver mendukung pengukuran kotak baru yang disebut devicePixelContentBox, yang mengukur dimensi elemen dalam piksel fisik. Hal ini memungkinkan rendering grafis yang sempurna untuk piksel, terutama dalam konteks layar berkepadatan tinggi.

Dukungan Browser

  • 84
  • 84
  • 93
  • x

Latar belakang: Piksel CSS, piksel kanvas, dan piksel fisik

Meskipun kita sering menggunakan unit abstrak panjang seperti em, %, atau vh, semuanya akan terpadu pada piksel. Setiap kali kita menentukan ukuran atau posisi elemen dalam CSS, mesin tata letak browser pada akhirnya akan mengonversi nilai tersebut menjadi piksel (px). Ini adalah "Piksel CSS", yang memiliki banyak histori dan hanya memiliki hubungan longgar dengan piksel yang Anda miliki di layar.

Untuk waktu yang lama, cukup masuk akal untuk memperkirakan kepadatan piksel layar siapa pun dengan 96DPI ("titik per inci"), yang berarti setiap monitor yang diberikan akan memiliki sekitar 38 piksel per cm. Seiring waktu, monitor bertambah dan/atau menyusut atau mulai memiliki lebih banyak piksel pada area permukaan yang sama. Kombinasikan hal tersebut dengan fakta bahwa banyak konten di web menentukan dimensinya, termasuk ukuran font, dalam px, dan kita akan mendapatkan teks yang tidak terbaca di layar berkepadatan tinggi ("HiDPI") ini. Sebagai langkah penanggulangan, browser menyembunyikan kerapatan piksel monitor yang sebenarnya dan berpura-pura bahwa pengguna memiliki tampilan 96 DPI. Unit px dalam CSS mewakili ukuran satu piksel pada tampilan 96 DPI virtual ini, sehingga disebut "CSS Pixel". Unit ini hanya digunakan untuk pengukuran dan penentuan posisi. Sebelum rendering yang sebenarnya terjadi, konversi ke piksel fisik akan terjadi.

Bagaimana kita beralih dari tampilan virtual ini ke tampilan nyata pengguna? Masukkan devicePixelRatio. Nilai global ini memberi tahu Anda berapa banyak piksel fisik yang diperlukan untuk membentuk satu piksel CSS. Jika devicePixelRatio (dPR) adalah 1, Anda sedang mengerjakan monitor dengan sekitar 96DPI. Jika Anda memiliki layar retina, dPR Anda mungkin 2. Di ponsel, terkadang nilai dPR yang lebih tinggi (dan lebih aneh) seperti 2, 3, atau bahkan 2.65. Perlu diperhatikan bahwa nilai ini persis, tetapi tidak memungkinkan Anda mendapatkan nilai DPI sebenarnya pada monitor. DPR 2 berarti bahwa 1 piksel CSS akan dipetakan ke persis 2 piksel fisik.

Contoh
Monitor saya memiliki dPR 1 menurut Chrome...

Layar ini memiliki lebar 3440 {i>pixel<i} dan area tampilan adalah 79cm. Itu mengarah ke resolusi 110 DPI. Mendekati 96, tetapi belum benar. Itu juga alasan mengapa <div style="width: 1cm; height: 1cm"> tidak akan berukuran tepat dengan ukuran 1 cm di sebagian besar layar.

Terakhir, dPR juga dapat dipengaruhi oleh fitur zoom browser. Jika Anda memperbesar tampilan, browser akan meningkatkan dPR yang dilaporkan, sehingga semua objek dirender lebih besar. Jika Anda memeriksa devicePixelRatio di DevTools Console saat memperbesar, Anda dapat melihat nilai pecahan muncul.

DevTools menampilkan berbagai pecahan devicePixelRatio karena zoom dilakukan.

Mari tambahkan elemen <canvas> ke campuran. Anda dapat menentukan jumlah piksel yang ingin dimiliki kanvas menggunakan atribut width dan height. Jadi, <canvas width=40 height=30> akan menjadi kanvas dengan ukuran 40 kali 30 piksel. Namun, ini tidak berarti bahwa gambar akan ditampilkan dengan ukuran 40x30 piksel. Secara default, kanvas akan menggunakan atribut width dan height untuk menentukan ukuran intrinsiknya, tetapi Anda dapat mengubah ukuran kanvas secara arbitrer menggunakan semua properti CSS yang Anda kenal dan sukai. Dengan semua yang telah kita pelajari sejauh ini, mungkin Anda terpikir bahwa ini tidak ideal dalam setiap skenario. Satu piksel di kanvas mungkin akhirnya menutupi beberapa piksel fisik, atau hanya sebagian kecil dari piksel fisik. Hal ini dapat menyebabkan artefak visual yang tidak menyenangkan.

Ringkasnya: Elemen kanvas memiliki ukuran yang ditentukan untuk menentukan area yang dapat Anda gambar. Jumlah piksel kanvas benar-benar independen dari ukuran tampilan kanvas, yang ditentukan dalam piksel CSS. Jumlah piksel CSS tidak sama dengan jumlah piksel fisik.

Kesempurnaan piksel

Dalam beberapa skenario, disarankan untuk memiliki pemetaan yang tepat dari piksel kanvas ke piksel fisik. Jika pemetaan ini dapat dilakukan, pemetaan ini disebut "pixel-perfect". Perenderan sempurna piksel sangat penting untuk rendering teks yang dapat dibaca, terutama saat menggunakan rendering subpiksel atau saat menampilkan grafis dengan garis kecerahan bergantian yang disejajarkan dengan erat.

Untuk mencapai sesuatu sedekat mungkin dengan kanvas di web, berikut ini kurang lebih adalah pendekatan yang perlu digunakan:

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

Pembaca yang cerdik mungkin bertanya-tanya apa yang terjadi ketika dPR bukan merupakan nilai bilangan bulat. Itu pertanyaan yang bagus dan di mana letak inti dari seluruh masalah ini. Selain itu, jika Anda menentukan posisi atau ukuran elemen menggunakan persentase, vh, atau nilai tidak langsung lainnya, nilai piksel CSS mungkin akan di-resolve menjadi nilai piksel CSS pecahan. Elemen dengan margin-left: 33% dapat memiliki bentuk persegi panjang seperti ini:

DevTools menampilkan nilai piksel pecahan sebagai hasil dari panggilan getBoundingClientRect().

Piksel CSS sebenarnya bersifat virtual, sehingga secara teoritis memiliki pecahan piksel, tetapi bagaimana browser mengetahui pemetaan ke piksel fisik? Karena piksel fisik pecahan bukanlah hal.

Pengepasan piksel

Bagian dari proses konversi satuan yang menangani penyelarasan elemen dengan piksel fisik disebut "pengikatan piksel", dan melakukan apa yang tertulis di timah: Proses ini mengikat nilai piksel pecahan menjadi bilangan bulat, nilai piksel fisik. Perbedaan ini terjadi antara browser dan browser. Jika kita memiliki elemen dengan lebar 791.984px pada tampilan dengan dPR 1, satu browser mungkin merender elemen pada 792px piksel fisik, sementara browser lain mungkin merendernya pada 791px. Itu hanya pengurangan satu piksel, tetapi satu piksel dapat merugikan rendering yang harus sempurna untuk piksel. Hal ini dapat menyebabkan artefak buram atau artefak lebih terlihat seperti efek Moiré.

Gambar atas adalah raster piksel dengan warna berbeda. Gambar di bawah sama dengan di atas, tetapi lebar dan tinggi telah dikurangi satu piksel menggunakan penskalaan bilinear. Pola yang muncul disebut efek Moiré.
(Anda mungkin harus membuka gambar ini di tab baru untuk melihatnya tanpa menerapkan penskalaan.)

devicePixelContentBox

devicePixelContentBox memberi Anda kotak konten elemen dalam unit piksel perangkat (yaitu piksel fisik). Bagian dari ResizeObserver. Meskipun ResizeObserver kini didukung di semua browser utama sejak Safari 13.1, properti devicePixelContentBox hanya ada di Chrome 84+ untuk saat ini.

Seperti yang disebutkan dalam ResizeObserver: ini seperti document.onresize untuk elemen, fungsi callback ResizeObserver akan dipanggil sebelum paint dan setelah tata letak. Ini berarti parameter entries ke callback akan berisi ukuran semua elemen yang diamati tepat sebelum elemen tersebut digambar. Dalam konteks masalah kanvas yang diuraikan di atas, kita dapat menggunakan kesempatan ini untuk menyesuaikan jumlah {i>pixel<i} di kanvas, memastikan bahwa kita akhirnya akan mendapatkan pemetaan one-to-one yang tepat antara piksel kanvas dan piksel fisik.

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

Properti box di objek opsi untuk observer.observe() memungkinkan Anda menentukan ukuran mana yang ingin diamati. Jadi, meskipun setiap ResizeObserverEntry akan selalu menyediakan borderBoxSize, contentBoxSize, dan devicePixelContentBoxSize (asalkan browser mendukungnya), callback hanya akan dipanggil jika ada perubahan metrik kotak yang diamati.

Dengan properti baru ini, kita bahkan dapat menganimasikan ukuran dan posisi kanvas (secara efektif menjamin nilai piksel pecahan), dan tidak melihat efek Moiré apa pun pada rendering. Jika Anda ingin melihat efek Moiré pada pendekatan menggunakan getBoundingClientRect(), dan cara properti ResizeObserver baru memungkinkan Anda menghindarinya, lihat demo di Chrome 84 atau yang lebih baru.

Deteksi fitur

Guna memeriksa apakah browser pengguna memiliki dukungan untuk devicePixelContentBox, kita dapat mengamati setiap elemen, dan memeriksa apakah properti tersebut ada di 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
}

Kesimpulan

{i>Pixels<i} adalah topik yang sangat rumit di web, dan hingga saat ini, tidak ada cara bagi Anda untuk mengetahui jumlah persis piksel fisik yang ditempati oleh sebuah elemen di layar pengguna. Properti devicePixelContentBox baru di ResizeObserverEntry memberi Anda informasi tersebut dan memungkinkan Anda melakukan rendering yang sempurna untuk piksel dengan <canvas>. devicePixelContentBox didukung di Chrome 84 dan yang lebih baru.