Rendering sempurna piksel dengan devicePixelContentBox

Berapa banyak piksel 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, terutama dalam konteks layar kepadatan tinggi.

Browser Support

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: not supported.

Source

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

Meskipun kita sering bekerja dengan satuan 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 yang tidak erat dengan piksel yang ada di layar Anda.

Untuk waktu yang lama, cukup wajar untuk memperkirakan kepadatan piksel layar siapa pun dengan 96 DPI ("dots per inch"), 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 pada area permukaan yang sama. Gabungkan hal itu dengan fakta bahwa banyak konten di web menentukan dimensinya, termasuk ukuran font, dalam px, dan kita akan mendapatkan teks yang tidak terbaca di layar kepadatan tinggi ("HiDPI") ini. Sebagai tindakan balasan, browser menyembunyikan kepadatan piksel sebenarnya dari monitor dan berpura-pura bahwa pengguna memiliki layar 96 DPI. Satuan px di CSS merepresentasikan ukuran satu piksel pada tampilan 96 DPI virtual ini, sehingga dinamakan "Piksel CSS". Unit ini hanya digunakan untuk pengukuran dan penentuan posisi. Sebelum rendering sebenarnya terjadi, konversi ke piksel fisik akan terjadi.

Bagaimana cara beralih dari tampilan virtual ini ke tampilan sebenarnya pengguna? Masukkan devicePixelRatio. Nilai global ini memberi tahu Anda jumlah piksel fisik yang diperlukan untuk membentuk satu piksel CSS. Jika devicePixelRatio (dPR) adalah 1, Anda sedang menggunakan monitor dengan sekitar 96 DPI. Jika Anda memiliki layar retina, dPR Anda mungkin 2. Di ponsel, tidak jarang kita menemukan nilai dPR yang lebih tinggi (dan lebih aneh) seperti 2, 3, atau bahkan 2.65. Penting untuk diperhatikan bahwa nilai ini tepat, tetapi tidak memungkinkan Anda mendapatkan nilai DPI aktual monitor. dPR 2 berarti 1 piksel CSS akan dipetakan ke tepat 2 piksel fisik.

Contoh
Monitor saya memiliki dPR 1 menurut Chrome…

Monitor ini memiliki lebar 3.440 piksel dan area tampilan selebar 79 cm. Hal ini menghasilkan resolusi 110 DPI. Mendekati 96, tetapi tidak tepat. Itulah juga alasan mengapa <div style="width: 1cm; height: 1cm"> tidak akan berukuran tepat 1 cm di sebagian besar layar.

Terakhir, dPR juga dapat dipengaruhi oleh fitur zoom browser Anda. Jika Anda melakukan zoom in, browser akan meningkatkan dPR yang dilaporkan, sehingga semuanya dirender lebih besar. Jika Anda memeriksa devicePixelRatio di Konsol DevTools saat melakukan zoom, Anda dapat melihat nilai pecahan muncul.

DevTools menampilkan berbagai devicePixelRatio pecahan karena melakukan zoom.

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

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

Kesempurnaan Pixel

Dalam beberapa skenario, pemetaan yang tepat dari piksel kanvas ke piksel fisik diinginkan. Jika pemetaan ini tercapai, maka disebut "pixel-perfect". Rendering sempurna piksel sangat penting untuk rendering teks yang mudah dibaca, terutama saat menggunakan rendering subpiksel atau saat menampilkan grafik dengan garis yang selaras rapat dengan kecerahan bergantian.

Untuk mendapatkan sesuatu yang sedekat mungkin dengan kanvas sempurna piksel di web, pendekatan ini kurang lebih telah menjadi pendekatan yang paling umum:

<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 cerdas mungkin bertanya-tanya apa yang terjadi jika dPR bukan nilai bilangan bulat. Itu adalah pertanyaan yang bagus dan di situlah letak inti dari seluruh masalah ini. Selain itu, jika Anda menentukan posisi atau ukuran elemen menggunakan persentase, vh, atau nilai tidak langsung lainnya, elemen tersebut dapat diselesaikan ke nilai piksel CSS pecahan. Elemen dengan margin-left: 33% dapat menghasilkan persegi panjang seperti ini:

DevTools menampilkan nilai piksel fraksional sebagai hasil panggilan getBoundingClientRect().

Piksel CSS murni virtual, jadi memiliki pecahan piksel secara teori tidak masalah, tetapi bagaimana cara browser mengetahui pemetaan ke piksel fisik? Karena piksel fisik fraksional tidak ada.

Penyelarasan piksel

Bagian dari proses konversi unit yang menangani penyelarasan elemen dengan piksel fisik disebut "penyesuaian piksel", dan fungsinya sesuai dengan namanya: Menyesuaikan nilai piksel pecahan ke nilai piksel fisik bilangan bulat. Cara tepatnya hal ini terjadi berbeda-beda 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 792px piksel fisik, sementara browser lain mungkin merendernya pada 791px. Hanya satu piksel yang tidak sesuai, tetapi satu piksel dapat merusak rendering yang harus sempurna untuk piksel. Hal ini dapat menyebabkan keburaman atau bahkan artefak yang lebih terlihat seperti efek Moiré.

Gambar atas adalah raster piksel dengan warna yang berbeda. Gambar bawah sama dengan gambar 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 apa pun.)

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: ini seperti document.onresize untuk elemen, fungsi callback ResizeObserver akan dipanggil sebelum menggambar dan setelah tata letak. Artinya, 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 piksel pada kanvas, sehingga 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 memberikan borderBoxSize, contentBoxSize, dan devicePixelContentBoxSize (asalkan browser mendukungnya), callback hanya akan dipanggil jika ada metrik kotak yang diamati yang berubah.

Dengan properti baru ini, kita bahkan dapat menganimasikan ukuran dan posisi kanvas (secara efektif menjamin nilai piksel fraksional), dan tidak melihat efek Moiré pada rendering. Jika Anda ingin melihat efek Moiré pada pendekatan menggunakan getBoundingClientRect(), dan bagaimana properti ResizeObserver 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 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 Anda tidak dapat mengetahui jumlah pasti piksel fisik yang ditempati suatu elemen di layar pengguna. Properti devicePixelContentBox baru di ResizeObserverEntry memberi Anda informasi tersebut dan memungkinkan Anda melakukan rendering yang sempurna dengan <canvas>. devicePixelContentBox didukung di Chrome 84+.