Rendering sempurna piksel dengan devicePixelContentBox

Berapa banyak piksel yang sebenarnya dalam sebuah kanvas?

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

Dukungan Browser

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: tidak didukung.x

Sumber

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

Meskipun kita sering menggunakan unit panjang abstrak seperti em, %, atau vh, semuanya bermuara pada piksel. Setiap kali kita menentukan ukuran atau posisi elemen di 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.

Selama waktu yang lama, estimasi kepadatan piksel layar dengan 96DPI ("titik per inci") cukup wajar, yang berarti monitor tertentu akan memiliki sekitar 38 piksel per cm. Seiring waktu, monitor bertambah besar dan/atau mengecil atau mulai memiliki lebih banyak piksel di area permukaan yang sama. Gabungkan dengan fakta bahwa banyak konten di web menentukan dimensinya, termasuk ukuran font, di px, dan kita akan mendapatkan teks yang tidak dapat dibaca di layar berkepadatan tinggi ("HiDPI") ini. Sebagai tindakan pencegahan, browser menyembunyikan kepadatan piksel monitor yang sebenarnya dan berpura-pura bahwa pengguna memiliki layar 96 DPI. Satuan px di CSS mewakili ukuran satu piksel pada layar 96 DPI virtual ini, sehingga namanya "Piksel CSS". Satuan ini hanya digunakan untuk pengukuran dan pemosisian. 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 jumlah piksel fisik yang diperlukan untuk membentuk satu piksel CSS. Jika devicePixelRatio (dPR) adalah 1, berarti Anda sedang mengerjakan monitor dengan sekitar 96DPI. Jika Anda memiliki layar retina, dPR Anda mungkin 2. Di ponsel, tidak jarang Anda menemukan nilai dPR yang lebih tinggi (dan lebih aneh) seperti 2, 3, atau bahkan 2.65. Perhatikan bahwa nilai ini tepat, tetapi tidak memungkinkan Anda memperoleh nilai DPI aktual monitor. dPR 2 berarti bahwa 1 piksel CSS akan dipetakan ke persis 2 piksel fisik.

Contoh
Monitor saya memiliki dPR 1 menurut Chrome…

Lebarnya 3440 piksel dan area tampilannya 79 cm. Itu menghasilkan resolusi 110 DPI. Hampir 96, tapi tidak benar. Ini juga alasan mengapa <div style="width: 1cm; height: 1cm"> tidak akan memiliki 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, menyebabkan segala sesuatunya dirender menjadi lebih besar. Jika Anda memeriksa devicePixelRatio di Konsol DevTools saat melakukan zoom, Anda dapat melihat nilai pecahan muncul.

DevTools menampilkan berbagai devicePixelRatio pecahan karena zoom.

Mari tambahkan elemen <canvas> ke campuran. Anda dapat menentukan jumlah piksel yang akan dimiliki kanvas menggunakan atribut width dan height. Jadi <canvas width=40 height=30> akan menjadi kanvas dengan 40 kali 30 piksel. Namun, hal 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 ketahui dan sukai. Dengan semua yang telah kita pelajari sejauh ini, mungkin terjadi pada Anda bahwa ini tidak akan ideal dalam setiap skenario. Satu piksel di kanvas mungkin akhirnya menutupi beberapa piksel fisik, atau hanya sebagian kecil piksel fisik. Hal ini dapat menyebabkan artefak visual yang tidak menyenangkan.

Untuk meringkas: Elemen kanvas memiliki ukuran tertentu untuk menentukan area yang dapat Anda gambar. Jumlah piksel kanvas sepenuhnya independen dari ukuran tampilan kanvas, yang ditentukan dalam piksel CSS. Jumlah piksel CSS tidak sama dengan jumlah piksel fisik.

Pixel sempurna

Dalam beberapa skenario, sebaiknya Anda memiliki pemetaan yang tepat dari piksel kanvas ke piksel fisik. Jika pemetaan ini tercapai, pemetaan tersebut disebut "sempurna piksel". Rendering sempurna piksel sangat penting untuk rendering teks yang dapat dibaca, terutama saat menggunakan rendering subpiksel atau saat menampilkan grafik dengan garis kecerahan bergantian yang sejajar dengan ketat.

Untuk mendapatkan sesuatu yang sedekat mungkin dengan kanvas yang sempurna piksel di web, ini adalah pendekatan yang paling sering 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 jeli mungkin bertanya-tanya apa yang terjadi jika dPR bukan nilai bilangan bulat. Itu adalah pertanyaan yang bagus dan di mana inti dari seluruh masalah ini berada. Selain itu, jika Anda menentukan posisi atau ukuran elemen menggunakan persentase, vh, atau nilai tidak langsung lainnya, nilai tersebut mungkin akan di-resolve ke nilai piksel CSS pecahan. Elemen dengan margin-left: 33% dapat berakhir dengan persegi panjang seperti ini:

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

Piksel CSS sepenuhnya virtual, jadi teorinya bisa memiliki pecahan piksel, tetapi bagaimana cara browser mengetahui pemetaan ke piksel fisik? Karena piksel fisik pecahan bukanlah sesuatu.

Pengambilan piksel

Bagian dari proses konversi satuan yang menangani penyelarasan elemen dengan piksel fisik disebut "pengepasan piksel", dan melakukan fungsi seperti yang tertera di kaleng: Fungsi ini mengepaskan nilai piksel pecahan ke nilai piksel fisik bilangan bulat. Caranya berbeda dari satu browser ke browser lainnya. Jika kita memiliki elemen dengan lebar 791.984px pada layar dengan dPR 1, satu browser mungkin merender elemen pada piksel fisik 792px, sementara browser lain mungkin merendernya pada 791px. Itu hanya satu piksel saja yang tidak, tetapi satu piksel dapat mengganggu proses rendering yang harus sempurna untuk piksel. Hal ini dapat menyebabkan pemburaman atau bahkan artefak yang lebih terlihat seperti efek Moiré.

Gambar di atas adalah raster dari piksel yang berwarna berbeda. Gambar di bawah sama dengan yang di atas, tetapi lebar dan tingginya 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 penskalaan yang diterapkan.)

devicePixelContentBox

devicePixelContentBox memberi Anda kotak konten elemen dalam unit piksel perangkat (yaitu piksel fisik). Ini adalah 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: seperti document.onresize untuk elemen, fungsi callback ResizeObserver akan dipanggil sebelum proses menggambar dan setelah tata letak. Artinya, parameter entries ke callback akan berisi ukuran semua elemen yang diamati tepat sebelum digambar. Dalam konteks masalah kanvas yang diuraikan di atas, kita dapat menggunakan peluang ini untuk menyesuaikan jumlah piksel pada kanvas, memastikan bahwa kita mendapatkan pemetaan satu-ke-satu 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 dalam objek opsi untuk observer.observe() memungkinkan Anda menentukan ukuran yang ingin diamati. Jadi, meskipun setiap ResizeObserverEntry akan selalu menyediakan borderBoxSize, contentBoxSize, dan devicePixelContentBoxSize (asalkan browser mendukungnya), callback hanya akan dipanggil jika salah satu metrik kotak teramati berubah.

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

Deteksi fitur

Untuk memeriksa apakah browser pengguna memiliki dukungan untuk devicePixelContentBox, kita dapat mengamati elemen apa pun, 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

Piksel adalah topik yang sangat kompleks di web dan hingga saat ini tidak ada cara bagi Anda untuk mengetahui jumlah piksel fisik yang tepat yang digunakan elemen di layar pengguna. Properti devicePixelContentBox baru di ResizeObserverEntry memberi Anda informasi tersebut dan memungkinkan Anda melakukan rendering sempurna piksel dengan <canvas>. devicePixelContentBox didukung di Chrome 84+.