رندر عالی پیکسل با دستگاهPixelContentBox

واقعاً چند پیکسل در یک بوم نقاشی وجود دارد؟

از نسخه ۸۴ کروم، ResizeObserver از یک واحد اندازه‌گیری جدید به نام devicePixelContentBox پشتیبانی می‌کند که ابعاد عنصر را بر حسب پیکسل‌های فیزیکی اندازه‌گیری می‌کند. این امر امکان رندر کردن گرافیک‌های پیکسلی بی‌نقص را، به ویژه در زمینه صفحات نمایش با تراکم بالا، فراهم می‌کند.

Browser Support

  • کروم: ۸۴.
  • لبه: ۸۴.
  • فایرفاکس: ۱۰۸.
  • سافاری: پشتیبانی نمی‌شود.

Source

پس‌زمینه: پیکسل‌های CSS، پیکسل‌های بوم نقاشی و پیکسل‌های فیزیکی

اگرچه ما اغلب با واحدهای انتزاعی طول مانند em ، % یا vh کار می‌کنیم، اما همه چیز به پیکسل خلاصه می‌شود. هر زمان که اندازه یا موقعیت یک عنصر را در CSS مشخص می‌کنیم، موتور طرح‌بندی مرورگر در نهایت آن مقدار را به پیکسل ( px ) تبدیل می‌کند. اینها "پیکسل‌های CSS" هستند که تاریخچه زیادی دارند و فقط رابطه سستی با پیکسل‌های روی صفحه نمایش شما دارند.

برای مدت طولانی، تخمین تراکم پیکسل صفحه نمایش هر کسی با 96DPI ("نقطه در هر اینچ") نسبتاً منطقی بود، به این معنی که هر مانیتور معین تقریباً 38 پیکسل در هر سانتی‌متر مربع داشت. با گذشت زمان، مانیتورها بزرگ و/یا کوچک شدند یا شروع به داشتن پیکسل‌های بیشتر در همان سطح کردند. این را با این واقعیت ترکیب کنید که بسیاری از محتوا در وب ابعاد خود، از جمله اندازه فونت‌ها را بر px تعریف می‌کنند، و در نهایت با متن ناخوانا در این صفحات با چگالی بالا ("HiDPI") مواجه می‌شویم. به عنوان یک اقدام متقابل، مرورگرها تراکم پیکسل واقعی مانیتور را پنهان می‌کنند و در عوض وانمود می‌کنند که کاربر یک صفحه نمایش 96 DPI دارد. واحد px در CSS نشان دهنده اندازه یک پیکسل در این صفحه نمایش مجازی 96 DPI است، از این رو نام آن "CSS Pixel" است. این واحد فقط برای اندازه‌گیری و موقعیت‌یابی استفاده می‌شود. قبل از هرگونه رندر واقعی، تبدیل به پیکسل‌های فیزیکی انجام می‌شود.

چگونه می‌توانیم از این نمایشگر مجازی به نمایشگر واقعی کاربر برویم؟ devicePixelRatio را وارد کنید. این مقدار سراسری به شما می‌گوید که برای تشکیل یک پیکسل CSS به چند پیکسل فیزیکی نیاز دارید. اگر devicePixelRatio (dPR) برابر با 1 باشد، شما روی مانیتوری با تقریباً ۹۶DPI کار می‌کنید. اگر صفحه نمایش شما رتینا است، dPR شما احتمالاً 2 است. در تلفن‌ها، مواجهه با مقادیر بالاتر (و عجیب‌تر) dPR مانند 2 ، 3 یا حتی 2.65 غیرمعمول نیست. لازم به ذکر است که این مقدار دقیق است، اما به شما اجازه نمی‌دهد مقدار DPI واقعی مانیتور را استخراج کنید. dPR برابر با 2 به این معنی است که ۱ پیکسل CSS دقیقاً به ۲ پیکسل فیزیکی نگاشت می‌شود.

مثال
مانیتور من طبق کروم dPR برابر با 1 دارد…

عرض آن ۳۴۴۰ پیکسل است و مساحت نمایشگر ۷۹ سانتی‌متر است. این منجر به وضوح ۱۱۰ DPI می‌شود. نزدیک به ۹۶، اما نه کاملاً. به همین دلیل است که <div style="width: 1cm; height: 1cm"> در اکثر نمایشگرها دقیقاً ۱ سانتی‌متر اندازه نمی‌گیرد.

در نهایت، dPR می‌تواند تحت تأثیر قابلیت بزرگنمایی مرورگر شما نیز قرار گیرد. اگر بزرگنمایی کنید، مرورگر dPR گزارش شده را افزایش می‌دهد و باعث می‌شود همه چیز بزرگتر رندر شود. اگر هنگام بزرگنمایی، devicePixelRatio در کنسول DevTools بررسی کنید، می‌توانید مقادیر کسری را مشاهده کنید.

DevTools انواع مختلفی از نسبت پیکسل به پیکسل devicePixelRatio به دلیل بزرگنمایی نشان می‌دهد.

بیایید عنصر <canvas> را به این مجموعه اضافه کنیم. می‌توانید با استفاده از ویژگی‌های width و height ، تعداد پیکسل‌های مورد نظر برای بوم نقاشی را مشخص کنید. بنابراین <canvas width=40 height=30> یک بوم نقاشی با ابعاد ۴۰ در ۳۰ پیکسل خواهد بود. با این حال، این بدان معنا نیست که با ابعاد ۴۰ در ۳۰ پیکسل نمایش داده می‌شود. به طور پیش‌فرض، بوم نقاشی از ویژگی‌های width و height برای تعریف اندازه ذاتی خود استفاده می‌کند، اما می‌توانید با استفاده از تمام ویژگی‌های CSS که می‌شناسید و دوست دارید، به طور دلخواه اندازه بوم نقاشی را تغییر دهید. با توجه به هر آنچه تاکنون آموخته‌ایم، ممکن است به ذهنتان خطور کند که این روش در هر سناریویی ایده‌آل نخواهد بود. یک پیکسل روی بوم نقاشی ممکن است چندین پیکسل فیزیکی یا فقط کسری از یک پیکسل فیزیکی را بپوشاند. این می‌تواند منجر به مصنوعات بصری ناخوشایند شود.

به طور خلاصه: عناصر Canvas اندازه مشخصی برای تعریف ناحیه‌ای که می‌توانید روی آن ترسیم کنید، دارند. تعداد پیکسل‌های Canvas کاملاً مستقل از اندازه نمایش Canvas است که با پیکسل‌های CSS مشخص می‌شود. تعداد پیکسل‌های CSS با تعداد پیکسل‌های فیزیکی یکسان نیست.

کمال پیکسل

در برخی سناریوها، مطلوب است که نگاشت دقیقی از پیکسل‌های بوم به پیکسل‌های فیزیکی داشته باشیم. اگر این نگاشت حاصل شود، به آن "پیکسل بی‌نقص" می‌گویند. رندر پیکسل بی‌نقص برای رندر خوانای متن بسیار مهم است، به خصوص هنگام استفاده از رندر زیرپیکسلی یا هنگام نمایش گرافیک‌هایی با خطوط کاملاً هم‌راستا با روشنایی متناوب.

برای رسیدن به چیزی تا حد امکان نزدیک به یک بوم پیکسلی بی‌نقص در وب، این رویکرد کم و بیش رایج بوده است:

<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 یا سایر مقادیر غیرمستقیم مشخص کنید، ممکن است که آنها به مقادیر پیکسلی کسری CSS تبدیل شوند. عنصری با margin-left: 33% می‌تواند در نهایت مستطیلی مانند این داشته باشد:

ابزارهای توسعه (DevTools) مقادیر پیکسل‌های کسری را در نتیجه فراخوانی تابع getBoundingClientRect() نشان می‌دهند.

پیکسل‌های CSS کاملاً مجازی هستند، بنابراین داشتن کسری از یک پیکسل از نظر تئوری اشکالی ندارد، اما مرورگر چگونه نگاشت به پیکسل‌های فیزیکی را تشخیص می‌دهد؟ زیرا پیکسل‌های فیزیکی کسری وجود ندارند.

ثبت پیکسل

بخشی از فرآیند تبدیل واحد که وظیفه تراز کردن عناصر با پیکسل‌های فیزیکی را بر عهده دارد، "pixel snapping" نام دارد و همانطور که از نامش پیداست، عمل می‌کند: مقادیر کسری پیکسل را به مقادیر صحیح پیکسل‌های فیزیکی تبدیل می‌کند. نحوه دقیق انجام این کار از مرورگری به مرورگر دیگر متفاوت است. اگر عنصری با عرض 791.984px روی صفحه نمایشی داشته باشیم که dPR آن ۱ است، یک مرورگر ممکن است عنصر را با 792px پیکسل پیکسل فیزیکی رندر کند، در حالی که مرورگر دیگری ممکن است آن را با 791px رندر کند. این فقط یک پیکسل اختلاف است، اما یک پیکسل می‌تواند برای رندرهایی که باید پیکسل‌ها بی‌نقص باشند، مضر باشد. این می‌تواند منجر به تاری یا حتی مصنوعات قابل مشاهده‌تری مانند اثر Moiré شود.

تصویر بالایی یک تصویر شطرنجی از پیکسل‌های با رنگ‌های متفاوت است. تصویر پایینی مشابه تصویر بالاست، اما عرض و ارتفاع آن با استفاده از مقیاس‌بندی دوخطی به اندازه یک پیکسل کاهش یافته است. الگوی پدیدار شده، اثر مویر (Moiré effect) نام دارد.
(برای مشاهده تصویر بدون اعمال هیچ گونه مقیاس‌بندی، ممکن است لازم باشد آن را در یک برگه جدید باز کنید.)

devicePixelContentBox

devicePixelContentBox به شما یک کادر محتوای عنصر را بر اساس واحد پیکسل دستگاه (یعنی پیکسل فیزیکی) می‌دهد. این بخشی از ResizeObserver است. در حالی که ResizeObserver اکنون از Safari 13.1 در همه مرورگرهای اصلی پشتیبانی می‌شود ، ویژگی devicePixelContentBox فعلاً فقط در Chrome 84+ است.

همانطور که در ResizeObserver ذکر شد: این تابع مانند document.onresize برای عناصر است ، تابع callback یک ResizeObserver قبل از رنگ‌آمیزی و بعد از طرح‌بندی فراخوانی می‌شود. این بدان معناست که پارامتر entries در تابع callback شامل اندازه تمام عناصر مشاهده شده درست قبل از رنگ‌آمیزی آنها خواهد بود. در زمینه مشکل canvas که در بالا ذکر شد، می‌توانیم از این فرصت برای تنظیم تعداد پیکسل‌ها روی canvas خود استفاده کنیم و اطمینان حاصل کنیم که در نهایت یک نگاشت دقیق یک به یک بین پیکسل‌های canvas و پیکسل‌های فیزیکی خواهیم داشت.

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

ویژگی box در شیء options برای observer.observe() به شما امکان می‌دهد اندازه‌هایی را که می‌خواهید مشاهده کنید، تعریف کنید. بنابراین، اگرچه هر ResizeObserverEntry همیشه borderBoxSize ، contentBoxSize و devicePixelContentBoxSize را ارائه می‌دهد (به شرطی که مرورگر از آن پشتیبانی کند)، تابع callback فقط در صورتی فراخوانی می‌شود که هر یک از معیارهای box مشاهده شده تغییر کند.

با این ویژگی جدید، ما حتی می‌توانیم اندازه و موقعیت بوم خود را متحرک کنیم (که عملاً مقادیر پیکسلی کسری را تضمین می‌کند) و هیچ اثر Moiré را در رندر مشاهده نکنیم. اگر می‌خواهید اثر Moiré را در رویکرد با استفاده از 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
}

نتیجه‌گیری

پیکسل‌ها موضوع پیچیده‌ای در وب هستند و تاکنون هیچ راهی برای دانستن تعداد دقیق پیکسل‌های فیزیکی اشغال شده توسط یک عنصر در صفحه نمایش کاربر وجود نداشت. ویژگی جدید devicePixelContentBox در ResizeObserverEntry این اطلاعات را در اختیار شما قرار می‌دهد و به شما امکان می‌دهد رندرهای پیکسلی بی‌نقصی را با <canvas> انجام دهید. devicePixelContentBox در کروم ۸۴+ پشتیبانی می‌شود.