עיבוד מושלם בפיקסלים עם devicePixelContentBox

כמה פיקסלים יש באמת בקנבס?

החל מגרסת Chrome 84, ResizeObserver תומך במדידה חדשה של תיבה שנקראת devicePixelContentBox, שמודדת את מידות הרכיב בפיקסלים פיזיים. כך ניתן לעבד גרפיקה מושלמת עם פיקסלים, במיוחד בהקשר של מסכים עם צפיפות גבוהה.

תמיכה בדפדפנים

  • Chrome: 84.
  • Edge:‏ 84.
  • Firefox: 93.
  • Safari: לא נתמך.

מקור

רקע: פיקסלים של CSS, פיקסלים של קנבס ופיקסלים פיזיים

אנחנו עובדים לעיתים קרובות עם יחידות מופשטות של אורך כמו em,‏ % או vh, אבל בסופו של דבר מדובר בפיקסלים. בכל פעם שאנחנו מציינים את הגודל או המיקום של רכיב ב-CSS, מנוע הפריסה של הדפדפן ימיר בסופו של דבר את הערך הזה לפיקסלים (px). אלה הם "פיקסלים של CSS", שיש להם הרבה היסטוריה ויש להם קשר חלש בלבד עם הפיקסלים שמופיעים במסך.

במשך זמן רב, היה הגיוני להעריך את צפיפות הפיקסלים במסך של כל משתמש ב-96DPI (נקודות לאינץ'), כלומר בכל צג יהיו בערך 38 פיקסלים לסנטימטר. עם הזמן, המסכים גדלו או התכווצו, או שהם החלו לכלול יותר פיקסלים באותה שטח פנים. אם תשלבו את העובדה הזו עם העובדה שהרבה תוכן באינטרנט מגדירים את המידות שלהם, כולל גודל הגופנים, ב-px, התוצאה היא טקסט בלתי קריא במסכים עם צפיפות גבוהה (HiDPI). כצעד נגד, הדפדפנים מסתירים את צפיפות הפיקסלים בפועל של המסך, ובמקום זאת מעמידים פנים שלמשתמש יש מסך עם 96 DPI. היחידה px ב-CSS מייצגת את הגודל של פיקסל אחד במסך הוירטואלי הזה של 96 DPI, ולכן השם 'CSS Pixel'. היחידה הזו משמשת למדידה ולמיקום בלבד. לפני שמתבצע עיבוד גרפי בפועל, מתבצעת המרה לפיקסלים פיזיים.

איך עוברים מהמסך הווירטואלי הזה למסך האמיתי של המשתמש? צריך להזין devicePixelRatio. הערך הגלובלי הזה מראה כמה פיקסלים פיזיים נדרשים כדי ליצור פיקסל CSS יחיד. אם הערך של devicePixelRatio (dPR) הוא 1, עובדים על צג עם בערך 96DPI. אם יש לך מסך רטינה, סביר להניח שה-dPR שלך הוא 2. בטלפונים, לפעמים ניתן לראות ערכי dPR גבוהים (ומוזרים יותר) כמו 2, 3 או אפילו 2.65. חשוב לציין שהערך הזה הוא מדויק, אבל לא מאפשר להסיק את ערך ה-DPI בפועל של הצג. המשמעות של dPR של 2 היא שפיקסל CSS אחד ממופה ל-2 פיקסלים פיזיים בדיוק.

דוגמה
לפי הנתונים של Chrome, מספר ה-dPR של המסך שלי הוא 1...

רוחב המסך הוא 3,440 פיקסלים ואזור התצוגה הוא 79 ס"מ. התוצאה היא רזולוציה של 110DPI. קרוב ל-96, אבל לא בדיוק. זו גם הסיבה לכך ש-<div style="width: 1cm; height: 1cm"> לא מודד בדיוק גודל של 1 ס"מ ברוב המסכים.

לבסוף, רזולוציית המסך יכולה להיות מושפעת גם מתכונת הזום של הדפדפן. אם תתקרבו, הדפדפן מגדיל את ה-dPR המדווח וגורם להצגת הכול. אם מסמנים את devicePixelRatio במסוף כלי פיתוח בזמן שינוי מרחק התצוגה, יופיעו ערכים עשרוניים.

כלי הפיתוח מציגים מגוון ערכים חלקיים של devicePixelRatio בגלל הגדלה.

עכשיו נוסיף את הרכיב <canvas> לתמהיל. ניתן לציין כמה פיקסלים רוצים שיהיו באזור העריכה באמצעות המאפיינים width ו-height. לכן, <canvas width=40 height=30> הוא קנבס בגודל 40 על 30 פיקסלים. עם זאת, זה לא אומר שהוא יוצג בגודל של 40 על 30 פיקסלים. כברירת מחדל, הקנבס ישתמש במאפיינים width ו-height כדי להגדיר את הגודל הפנימי שלו, אבל אפשר לשנות את גודל הקנבס באופן שרירותי באמצעות כל מאפייני ה-CSS המוכרים והאהובים. אחרי כל מה שלמדנו עד עכשיו, יכול להיות שתגלו שהפתרון הזה לא אידיאלי בכל תרחיש. פיקסל אחד באזור העריכה עשוי בסופו של דבר לכסות מספר פיקסלים פיזיים, או רק חלק מפיקסל פיזי. זה עלול לגרום ליצירת ארטיפקטים ויזואליים לא נעימים.

לסיכום: לרכיבים של לוח הציור יש גודל נתון להגדרת האזור שאפשר לשרטט. מספר הפיקסלים בקנבס אינו תלוי לחלוטין בגודל התצוגה של אזור העריכה, כפי שמצוין בפיקסלים ב-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% יכול להסתיים במלבן כך:

כלי פיתוח שמוצגים בהם ערכי פיקסלים חלקיים כתוצאה מקריאה ל-getBoundingClientRect().

פיקסלים ב-CSS הם וירטואליים לחלוטין, כך שאפשר להשתמש בחלקי פיקסל באופן תיאורטי, אבל איך הדפדפן מבין איך למפות אותם לפיקסלים פיזיים? כי אין דבר כזה פיקסלים פיזיים חלקיים.

התאמת התמונה לפי פיקסלים

החלק בתהליך המרה של יחידות שמטרתו ליישר אלמנטים עם פיקסלים פיזיים נקרא 'הצמדה לפיקסלים', והוא עושה בדיוק את מה שהוא אומר: הוא מחבר ערכים חלקיים של פיקסלים לערכים שלמים של פיקסלים פיזיים. התהליך הזה שונה בכל דפדפן. אם יש לנו רכיב ברוחב 791.984px בתצוגה שבה ה-dPR הוא 1, דפדפן אחד עשוי לעבד את הרכיב ב-792px פיקסלים פיזיים, ואילו דפדפן אחר עשוי לעבד אותו ב-791px. זהו רק פיקסל אחד, אבל פיקסל אחד יכול להזיק לעיבוד תמונות שצריך להיות מושלם ברמת הפיקסל. כתוצאה מכך, התמונה עשויה להיות מטושטשת או להופיע בהם פגמים גלויים יותר, כמו אפקט מוירé.

התמונה העליונה היא רסטר של פיקסלים בצבעים שונים. התמונה התחתונה זהה לתמונה שלמעלה, אבל הרוחב והגובה שלה צומצמו בפיקסל אחד באמצעות שינוי קנה מידה לינארי דו-כיווני. התבנית שנוצרת נקראת אפקט מוירע (Moiré).
(יכול להיות שתצטרכו לפתוח את התמונה הזו בכרטיסייה חדשה כדי לראות אותה בלי שינוי קנה המידה שלה).

devicePixelContentBox

devicePixelContentBox מאפשרת לכם לראות את תיבת התוכן של רכיב ביחידות של פיקסלים במכשיר (כלומר פיקסלים פיזיים). הוא חלק מ-ResizeObserver. ResizeObserver נתמך עכשיו בכל הדפדפנים העיקריים מאז Safari 13.1, אבל המאפיין 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']});

המאפיין box באובייקט האפשרויות של observer.observe() מאפשר להגדיר את הגדלים שרוצים לצפות בהם. לכן, כל ResizeObserverEntry תמיד יספק את הערכים של borderBoxSize, contentBoxSize ו-devicePixelContentBoxSize (בתנאי שהדפדפן תומך בכך), אבל פונקציית ה-callback תופעל רק אם אחד מהמדדים בתיבה observed ישתנה.

בעזרת המאפיין החדש הזה, אנחנו יכולים אפילו להוסיף אנימציה לגודל ולמיקום של הקנבס שלנו (וכך להבטיח ערכים חלקיים של פיקסלים), ללא השפעות של 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 נתמך ב-Chrome מגרסה 84 ואילך.