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

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

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

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

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

सोर्स

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

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

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

इसकी चौड़ाई 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 एट्रिब्यूट का इस्तेमाल करेगा. हालांकि, अपनी पसंद की सभी सीएसएस प्रॉपर्टी का इस्तेमाल करके, कैनवस का साइज़ अपने-आप बदला जा सकता है. अब तक हमने जो कुछ भी सीखा है उससे आपको यह लग सकता है कि यह हर स्थिति में सही नहीं होगा. कैनवस पर एक पिक्सल, कई फ़िज़िकल पिक्सल या सिर्फ़ एक फ़िज़िकल पिक्सल का एक हिस्सा कवर कर सकता है. इस वजह से, विज़ुअल में गड़बड़ियां दिख सकती हैं.

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

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

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

वेब पर किसी पिक्सल-परफ़ेक्ट कैनवस के करीब कुछ भी हासिल करने के लिए, यह तरीका कमोबेश कारगर रहा है:

<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 पर रेंडर करे. यह सिर्फ़ एक पिक्सल ऑफ़ है, लेकिन एक पिक्सल, उन रेंडरिंग के लिए नुकसानदेह हो सकता है जो बिलकुल सटीक होने चाहिए. इससे फ़ोटो धुंधली हो सकती है या ऐसे आर्टफ़ैक्ट शामिल हो सकते हैं जो ज़्यादा दिखते हैं. जैसे, Moiré इफ़ेक्ट.

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

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() इस्तेमाल करने के तरीके पर Moiré का असर देखना है और यह भी जानना है कि नई 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 और इसके बाद के वर्शन पर काम करता है.