כמה פיקסלים יש באמת בקנבס?
החל מגרסה 84 של Chrome, ResizeObserver תומך במדידת קופסה חדשה שנקראת devicePixelContentBox
, שממדידה את המאפיינים של הרכיב בפיקסלים פיזיים. כך אפשר ליצור גרפיקה ברזולוציה של פיקסלים מושלמים, במיוחד בהקשר של מסכים עם צפיפות גבוהה.
רקע: פיקסלים ב-CSS, פיקסלים בקנבס ופיקסלים פיזיים
אנחנו עובדים לעיתים קרובות עם יחידות מופשטות של אורך כמו 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 אחד ממופה ל-שני פיקסלים פיזיים בדיוק.
1
לפי Chrome…רוחב המסך הוא 3,440 פיקסלים ואזור התצוגה הוא 79 ס"מ.
התוצאה היא רזולוציה של 110 DPI. קרוב ל-96, אבל לא בדיוק.
זו גם הסיבה לכך שרוב המסכים לא מציגים תמונה בגודל 1 ס"מ בדיוק.<div style="width: 1cm; height: 1cm">
לבסוף, רזולוציית המסך יכולה להיות מושפעת גם מתכונת הזום של הדפדפן. אם מתקרבים, הדפדפן מגדיל את הערך של dPR שמדווח, וכתוצאה מכך כל התצוגה נראית גדולה יותר. אם מסמנים את האפשרות devicePixelRatio
במסוף DevTools בזמן שמגדילים את התצוגה, אפשר לראות ערכים עשרוניים.
נוסיף את האלמנט <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%
יכול להסתיים בריבועים כאלה:
פיקסלים ב-CSS הם וירטואליים לחלוטין, כך ששימוש בחלקי פיקסל תקין מבחינה תיאורטית, אבל איך הדפדפן מחשב את המיפוי לפיקסלים פיזיים? כי אין דבר כזה פיקסלים פיזיים חלקיים.
התאמה לפי פיקסלים
החלק בתהליך המרה של יחידות שמטרתו ליישר אלמנטים עם פיקסלים פיזיים נקרא 'הצמדה לפיקסלים', והוא עושה בדיוק את מה שהוא אומר: הוא מחבר ערכים חלקיים של פיקסלים לערכים שלמים של פיקסלים פיזיים. האופן שבו זה קורה משתנה מדפדפן לדפדפן. אם יש לנו אלמנט ברוחב 791.984px
במסך שבו dPR הוא 1, יכול להיות שדפדפן אחד ירנדר את האלמנט ב-792px
פיקסלים פיזיים, בעוד שדפדפן אחר ירנדר אותו ב-791px
. זהו רק פיקסל אחד, אבל פיקסל אחד יכול להזיק לעיבוד תמונות שמחויב להיות מושלם ברמת הפיקסל. כתוצאה מכך, התמונה עשויה להיות מטושטשת או להופיע בהם פגמים גלויים יותר, כמו אפקט מוירé.
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 ואילך.