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

כמה פיקסלים יש באמת באזור הציור?

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

Browser Support

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

Source

רקע: פיקסלים של CSS, פיקסלים של בד הציור ופיקסלים פיזיים

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

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

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

דוגמה
לפי Chrome, למוניטור שלי יש dPR של 1

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

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

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

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

הצמדה של חלונות ב-Pixel

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

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

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

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