devicePixelContentBox के साथ Pixel-परफ़ेक्ट रेंडरिंग

कैनवस में असल में कितने पिक्सल होते हैं?

Chrome 84 से, ResizeObserver, devicePixelContentBox नाम की नई बॉक्स मेज़रमेंट सुविधा के साथ काम करता है. यह सुविधा, एलिमेंट के डाइमेंशन को फ़िज़िकल पिक्सल में मेज़र करती है. इससे पिक्सल-परफ़ेक्ट ग्राफ़िक रेंडर किए जा सकते हैं. खास तौर पर, हाई-डेंसिटी वाली स्क्रीन के लिए.

Browser Support

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

Source

बैकग्राउंड: सीएसएस पिक्सल, कैनवस पिक्सल, और फ़िज़िकल पिक्सल

हम अक्सर लंबाई की ऐब्स्ट्रैक्ट यूनिट, जैसे कि em, % या vh के साथ काम करते हैं. हालांकि, यह सब पिक्सल पर निर्भर करता है. सीएसएस में किसी एलिमेंट का साइज़ या पोज़िशन तय करते समय, ब्राउज़र का लेआउट इंजन उस वैल्यू को पिक्सल (px) में बदल देता है. इन्हें "सीएसएस पिक्सल" कहा जाता है. इनका इतिहास काफ़ी पुराना है और इनका आपकी स्क्रीन पर मौजूद पिक्सल से कोई खास संबंध नहीं है.

काफ़ी समय तक, किसी भी व्यक्ति की स्क्रीन पिक्सल डेंसिटी का अनुमान 96DPI ("डॉट प्रति इंच") से लगाया जाता था. इसका मतलब है कि किसी भी मॉनिटर में करीब 38 पिक्सल प्रति सें॰मी॰ होते थे. समय के साथ, मॉनिटर का साइज़ बढ़ता और/या घटता गया. साथ ही, एक ही सर्फ़ेस एरिया पर ज़्यादा पिक्सल दिखने लगे. इसके अलावा, वेब पर मौजूद ज़्यादातर कॉन्टेंट के डाइमेंशन, जैसे कि फ़ॉन्ट के साइज़, px में तय किए जाते हैं. इस वजह से, हमें ज़्यादा पिक्सल डेंसिटी ("HiDPI") वाली स्क्रीन पर टेक्स्ट पढ़ने में मुश्किल होती है. इस समस्या से बचने के लिए, ब्राउज़र मॉनिटर की असल पिक्सल डेंसिटी को छिपा देते हैं. इसके बजाय, वे यह दिखाते हैं कि उपयोगकर्ता के पास 96 DPI वाला डिसप्ले है. सीएसएस में px यूनिट, इस 96 डीपीआई वाले वर्चुअल डिसप्ले पर एक पिक्सल के साइज़ को दिखाती है. इसलिए, इसे "सीएसएस पिक्सल" कहा जाता है. इस यूनिट का इस्तेमाल सिर्फ़ मेज़रमेंट और पोज़िशनिंग के लिए किया जाता है. असल रेंडरिंग से पहले, फ़िजिकल पिक्सल में कन्वर्ज़न होता है.

हम इस वर्चुअल डिसप्ले से उपयोगकर्ता के असली डिसप्ले पर कैसे जाते हैं? devicePixelRatio डालें. इस ग्लोबल वैल्यू से पता चलता है कि एक सीएसएस पिक्सल बनाने के लिए, आपको कितने फ़िज़िकल पिक्सल की ज़रूरत है. अगर devicePixelRatio (डीपीआर) 1 है, तो इसका मतलब है कि मॉनिटर का डीपीआई करीब 96 है. अगर आपके पास रेटिना स्क्रीन है, तो आपका dPR शायद 2 है. फ़ोन पर, 2, 3 या 2.65 जैसी ज़्यादा (और अजीब) dPR वैल्यू दिखना आम बात है. यह ध्यान रखना ज़रूरी है कि यह वैल्यू सटीक होती है. हालांकि, इससे आपको मॉनिटर की असल डीपीआई वैल्यू का पता नहीं चलता. 2 के डीपीआर का मतलब है कि 1 सीएसएस पिक्सल, ठीक 2 फ़िज़िकल पिक्सल पर मैप होगा.

उदाहरण
Chrome के मुताबिक, मेरे मॉनिटर का dPR 1 है…

इसकी चौड़ाई 3440 पिक्सल है और डिसप्ले एरिया 79 सेंटीमीटर चौड़ा है. इससे 110 डीपीआई का रिज़ॉल्यूशन मिलता है. 96 के आस-पास, लेकिन उससे कम. इस वजह से, ज़्यादातर डिसप्ले पर <div style="width: 1cm; height: 1cm"> का साइज़ ठीक 1 सेमी नहीं होगा.

आखिर में, dPR पर आपके ब्राउज़र की ज़ूम सुविधा का भी असर पड़ सकता है. ज़ूम इन करने पर, ब्राउज़र रिपोर्ट किए गए डीपीआर को बढ़ा देता है. इससे सभी चीज़ें बड़े साइज़ में रेंडर होती हैं. ज़ूम करते समय, DevTools कंसोल में devicePixelRatio को चुनने पर, आपको फ़्रैक्शनल वैल्यू दिख सकती हैं.

ज़ूम करने की वजह से, DevTools में अलग-अलग फ़्रैक्शनल devicePixelRatio दिख रहे हैं.

आइए, अब <canvas> एलिमेंट को जोड़ते हैं. width और height एट्रिब्यूट का इस्तेमाल करके, यह तय किया जा सकता है कि कैनवस में कितने पिक्सल होने चाहिए. इसलिए, <canvas width=40 height=30> 40 x 30 पिक्सल का कैनवस होगा. हालांकि, इसका मतलब यह नहीं है कि इसे 40 x 30 पिक्सल के साइज़ में दिखाया जाएगा. डिफ़ॉल्ट रूप से, कैनवस अपने मूल साइज़ को तय करने के लिए width और height एट्रिब्यूट का इस्तेमाल करेगा. हालांकि, अपनी पसंद के मुताबिक कैनवस का साइज़ बदलने के लिए, सीएसएस की उन सभी प्रॉपर्टी का इस्तेमाल किया जा सकता है जिनके बारे में आपको पता है. अब तक सीखी गई बातों के आधार पर, आपको लग सकता है कि यह तरीका हर स्थिति में सही नहीं होगा. ऐसा हो सकता है कि कैनवस पर मौजूद एक पिक्सल, कई फ़िज़िकल पिक्सल को कवर कर ले या सिर्फ़ एक फ़िज़िकल पिक्सल के कुछ हिस्से को कवर करे. इस वजह से, इमेज में अनचाहे विज़ुअल आर्टफ़ैक्ट दिख सकते हैं.

कैनवस एलिमेंट का साइज़ तय होता है. इससे यह तय होता है कि आपको किस एरिया में ड्रॉइंग बनानी है. कैनवस पिक्सल की संख्या, कैनवस के डिसप्ले साइज़ से पूरी तरह अलग होती है. डिसप्ले साइज़ को सीएसएस पिक्सल में तय किया जाता है. सीएसएस पिक्सल की संख्या, फ़िज़िकल पिक्सल की संख्या से अलग होती है.

Pixel की परफ़ेक्शन

कुछ मामलों में, कैनवस पिक्सल से फ़िज़िकल पिक्सल की सटीक मैपिंग करना ज़रूरी होता है. अगर यह मैपिंग पूरी हो जाती है, तो इसे "पूरी तरह से सटीक" कहा जाता है. टेक्स्ट को साफ़ तौर पर दिखाने के लिए, पिक्सल-परफ़ेक्ट रेंडरिंग ज़रूरी है. खास तौर पर, सबपिक्सल रेंडरिंग का इस्तेमाल करते समय या बारीकी से अलाइन की गई लाइनों वाले ग्राफ़िक दिखाते समय.

वेब पर पिक्सल-परफ़ेक्ट कैनवस बनाने के लिए, यह तरीका सबसे ज़्यादा इस्तेमाल किया जाता है:

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

पढ़ने वाले व्यक्ति के मन में यह सवाल आ सकता है कि अगर dPR की वैल्यू पूर्णांक नहीं है, तो क्या होगा. यह एक अच्छा सवाल है और इस समस्या की मुख्य वजह यही है. इसके अलावा, अगर किसी एलिमेंट की पोज़िशन या साइज़ को प्रतिशत, vh या अन्य इनडायरेक्ट वैल्यू का इस्तेमाल करके तय किया जाता है, तो हो सकता है कि वे सीएसएस पिक्सल की फ़्रैक्शनल वैल्यू में बदल जाएं. margin-left: 33% वाले एलिमेंट के आखिर में, इस तरह का रेक्टैंगल दिख सकता है:

DevTools में, getBoundingClientRect() कॉल के नतीजे के तौर पर फ़्रैक्शनल पिक्सल वैल्यू दिख रही हैं.

सीएसएस पिक्सल पूरी तरह से वर्चुअल होते हैं. इसलिए, सिद्धांत के तौर पर पिक्सल के कुछ हिस्सों का होना ठीक है. हालांकि, ब्राउज़र को फ़िज़िकल पिक्सल के साथ मैपिंग का पता कैसे चलता है? ऐसा इसलिए है, क्योंकि फ़िज़िकल पिक्सल को फ़्रैक्शन में नहीं बांटा जा सकता.

पिक्सल स्नैपिंग

यूनिट कन्वर्ज़न की प्रोसेस का वह हिस्सा जो एलिमेंट को फ़िज़िकल पिक्सल के साथ अलाइन करने का काम करता है उसे "पिक्सल स्नैपिंग" कहा जाता है. यह नाम के मुताबिक ही काम करता है: यह फ़्रैक्शनल पिक्सल वैल्यू को पूर्णांक, फ़िज़िकल पिक्सल वैल्यू में बदलता है. यह प्रोसेस, ब्राउज़र के हिसाब से अलग-अलग होती है. अगर हमारे पास एक ऐसा एलिमेंट है जिसकी चौड़ाई 791.984px है और उसे ऐसे डिसप्ले पर दिखाया जा रहा है जहां dPR 1 है, तो हो सकता है कि एक ब्राउज़र उस एलिमेंट को 792px फ़िज़िकल पिक्सल पर रेंडर करे, जबकि दूसरा ब्राउज़र उसे 791px पर रेंडर करे. यह सिर्फ़ एक पिक्सल का अंतर है. हालांकि, एक पिक्सल का अंतर भी ऐसी रेंडरिंग के लिए नुकसानदेह हो सकता है जिनमें पिक्सल-परफ़ेक्ट होना ज़रूरी है. इससे इमेज धुंधली हो सकती है. साथ ही, मोइरे इफ़ेक्ट जैसे आर्टफ़ैक्ट ज़्यादा दिख सकते हैं.

सबसे ऊपर दी गई इमेज, अलग-अलग रंग के पिक्सल का रास्टर है. नीचे दी गई इमेज, ऊपर दी गई इमेज जैसी ही है. हालांकि, इसकी चौड़ाई और लंबाई को बाइनरी स्केलिंग का इस्तेमाल करके, एक पिक्सल कम कर दिया गया है. इस पैटर्न को मोइरे इफ़ेक्ट कहते हैं.
(इस इमेज को बिना किसी स्केलिंग के देखने के लिए, आपको इसे नए टैब में खोलना पड़ सकता है.)

devicePixelContentBox

devicePixelContentBox आपको डिवाइस पिक्सल (यानी कि फ़िज़िकल पिक्सल) यूनिट में किसी एलिमेंट का कॉन्टेंट बॉक्स देता है. यह ResizeObserver का हिस्सा है. Safari 13.1 के बाद से, ResizeObserver अब सभी मुख्य ब्राउज़र में काम करता है. हालांकि, फ़िलहाल devicePixelContentBox प्रॉपर्टी सिर्फ़ Chrome 84 और उसके बाद के वर्शन में उपलब्ध है.

ResizeObserver: यह एलिमेंट के लिए document.onresize की तरह होता है में बताया गया है कि ResizeObserver का कॉलबैक फ़ंक्शन, पेंट से पहले और लेआउट के बाद कॉल किया जाएगा. इसका मतलब है कि कॉलबैक के लिए entries पैरामीटर में, पेंट किए जाने से ठीक पहले देखे गए सभी एलिमेंट के साइज़ शामिल होंगे. ऊपर बताई गई कैनवस की समस्या के संदर्भ में, हम इस अवसर का इस्तेमाल अपने कैनवस पर पिक्सल की संख्या को अडजस्ट करने के लिए कर सकते हैं. इससे यह पक्का किया जा सकेगा कि कैनवस पिक्सल और फ़िज़िकल पिक्सल के बीच एक-से-एक मैपिंग हो.

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

observer.observe() के लिए options ऑब्जेक्ट में मौजूद box प्रॉपर्टी की मदद से, यह तय किया जा सकता है कि आपको किन साइज़ को ऑब्ज़र्व करना है. इसलिए, हर ResizeObserverEntry हमेशा borderBoxSize, contentBoxSize, और devicePixelContentBoxSize उपलब्ध कराएगा. हालांकि, ऐसा तब होगा, जब ब्राउज़र इसे सपोर्ट करता हो. इसके अलावा, कॉलबैक सिर्फ़ तब शुरू होगा, जब ऑब्ज़र्व की गई बॉक्स मेट्रिक में कोई बदलाव होता है.

इस नई प्रॉपर्टी की मदद से, हम अपने कैनवस के साइज़ और पोज़िशन को ऐनिमेट भी कर सकते हैं. इससे हमें पक्का करने में मदद मिलती है कि फ़्रैक्शनल पिक्सल वैल्यू का इस्तेमाल किया जा रहा है. साथ ही, रेंडरिंग पर कोई मोइरे इफ़ेक्ट नहीं दिखता. अगर आपको getBoundingClientRect() का इस्तेमाल करके, मोइरे इफ़ेक्ट देखना है और यह देखना है कि नई ResizeObserver प्रॉपर्टी से इसे कैसे रोका जा सकता है, तो Chrome 84 या इसके बाद के वर्शन में डेमो देखें!

सुविधा का पता लगाना

यह देखने के लिए कि किसी उपयोगकर्ता के ब्राउज़र पर devicePixelContentBox काम करता है या नहीं, हम किसी भी एलिमेंट को देख सकते हैं. साथ ही, यह देख सकते हैं कि प्रॉपर्टी devicePixelContentBox पर मौजूद है या नहीं: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
}

नतीजा

वेब पर पिक्सल एक जटिल विषय है. अब तक, आपके पास यह जानने का कोई तरीका नहीं था कि कोई एलिमेंट, उपयोगकर्ता की स्क्रीन पर कितने फ़िज़िकल पिक्सल लेता है. ResizeObserverEntry पर मौजूद नई devicePixelContentBox प्रॉपर्टी से आपको यह जानकारी मिलती है. साथ ही, इसकी मदद से <canvas> का इस्तेमाल करके, पिक्सल-परफ़ेक्ट रेंडरिंग की जा सकती है. devicePixelContentBox, Chrome 84 और इसके बाद के वर्शन पर काम करता है.