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

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

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

ब्राउज़र के इस्तेमाल से जुड़ी सहायता

  • Chrome: 84.
  • किनारा: 84.
  • Firefox: 93.
  • Safari: यह सुविधा काम नहीं करती.

सोर्स

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

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

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

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

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

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

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

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

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

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

पिक्सल की परफ़ॉर्मेंस

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

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

<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% वाले एलिमेंट का आखिरी हिस्सा इस तरह का रेक्टैंगल हो सकता है:

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

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

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

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

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

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() के विकल्प ऑब्जेक्ट में मौजूद box प्रॉपर्टी की मदद से, यह तय किया जा सकता है कि आपको किन साइज़ को निगरानी करनी है. इसलिए, हर ResizeObserverEntry हमेशा borderBoxSize, contentBoxSize, और devicePixelContentBoxSize (अगर ब्राउज़र इसकी अनुमति देता है) उपलब्ध कराएगा. हालांकि, कॉलबैक सिर्फ़ तब ट्रिगर होगा, जब निगरानी में रखी गई बॉक्स मेट्रिक में कोई बदलाव होगा.

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

फ़ीचर का पता लगाना

यह देखने के लिए कि उपयोगकर्ता के ब्राउज़र में 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 और इसके बाद के वर्शन पर काम करता है.