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

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

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

תמיכה בדפדפן

  • 84
  • 84
  • 93
  • x

רקע: פיקסלים של 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 פיקסלים פיזיים בדיוק.

דוגמה
לצג שלי יש dPR של 1 לפי Chrome...

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

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

הצמדת Pixel

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

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

devicePixelContentBox

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

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

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

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