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

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

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

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

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

מקור

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

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

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

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

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

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

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

לסיכום: לאלמנטים ב-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% יכול להסתיים בריבועים כאלה:

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

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

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

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

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

devicePixelContentBox

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