הדמיה של ליקויים בראיית צבעים ברינדור Blink

מתיאס ביינס
מתיאס ביינס

במאמר הזה מוסבר למה ואיך הטמענו סימולציית ליקוי בראיית צבעים ב-DevTools וב-Blink Renderer.

רקע: ניגודיות צבעים לא טובה

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

רשימה של בעיות נגישות נפוצות באינטרנט. טקסט עם ניגודיות נמוכה הוא הבעיה הנפוצה ביותר.

לפי ניתוח הנגישות של WebAIM ממיליון האתרים המובילים, נמצא שביותר מ-86% מדפי הבית יש ניגודיות נמוכה. בממוצע, לכל דף בית יש 36 מופעים שונים של טקסט עם ניגודיות נמוכה.

שימוש בכלי הפיתוח לחיפוש, הבנה ותיקון של בעיות ניגודיות

כלי הפיתוח ל-Chrome יכולים לעזור למפתחים ולמעצבים לשפר את הניגודיות ולבחור ערכות צבעים נגישות יותר לאפליקציות אינטרנט:

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

ב-Puppeteer, ממשק ה-API החדש של page.emulateVisionDeficiency(type) מאפשר להפעיל את הסימולציות האלה באופן פרוגרמטי.

ליקויים בראיית צבעים

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

תמונה צבעונית של צבעי פסטל מומסים, ללא ליקויים בראיית צבעים
תמונה צבעונית של צבעים מומסים, ללא הדמיה של ליקויים בראיית צבעים.
ALT_TEXT_HERE
ההשפעה של הדמיית אכרומטופסיה על תמונה צבעונית של צבעי צבע מומסים.
ההשפעה של הדמיית עיוורון לירוק על תמונה צבעונית של צבעי פסטל מומסים.
ההשפעה של הדמיית עיוורון לירוק על תמונה צבעונית של צבעי צבע מומסים.
ההשפעה של הדמיית עיוורון לאדום על תמונה צבעונית של צבעי פסטל מומסים.
ההשפעה של הדמיית עיוורון לאדום על תמונה צבעונית של צבעי פסטל מומסים.
ההשפעה של הדמיית עיוורון לכחול על תמונה צבעונית של צבעי פסטל מומסים.
ההשפעה של הדמיית עיוורון לכחול על תמונה צבעונית של צבעי פסטל מומסים.

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

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

סימולציה של ליקויים בראיית צבעים באמצעות HTML , CSS , SVG ו-C++

לפני שנתעמק בהטמעת התכונה שלנו Blink Renderer, חשוב להבין כיצד תוכל ליישם פונקציונליות מקבילה באמצעות טכנולוגיית אינטרנט.

כל אחת מהסימולציות האלה של חוסר בראיית צבעים היא שכבת-על שמכסה את הדף כולו. פלטפורמת האינטרנט מאפשרת לעשות זאת: מסנני CSS! באמצעות המאפיין filter של CSS, אפשר להשתמש בפונקציות סינון מוגדרות מראש, כגון blur, contrast, grayscale, hue-rotate ועוד הרבה יותר. כדי להשיג שליטה רבה יותר, המאפיין filter מקבל גם כתובת URL שיכולה להפנות להגדרת מסנן מותאמת אישית של SVG:

<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

הדוגמה שלמעלה משתמשת בהגדרת מסנן מותאמת אישית המבוססת על מטריצת צבעים. באופן עקרוני, ערך הצבע [Red, Green, Blue, Alpha] של כל פיקסל מכפיל מטריצה ויוצר צבע חדש [R′, G′, B′, A′].

כל שורה במטריצה מכילה 5 ערכים: מכפיל (מימין לשמאל) R, G, B ו-A, וגם ערך חמישי לערך של שינוי קבוע. יש 4 שורות: השורה הראשונה במטריצה משמשת לחישוב הערך החדש של 'אדום', השורה השנייה 'ירוק', השורה השלישית 'כחול' והשורה האחרונה 'אלפא'.

ודאי מעניין אותך מהיכן מגיעים המספרים המדויקים בדוגמה שלנו. מה הופך את מטריצת הצבעים הזו להערכה טובה לעיוורון לירוק? התשובה היא: מדע! הערכים מבוססים על מודל סימולציה פיזיולוגי של לקות בראיית צבעים על ידי מאצ'אדו, אוליביירה ופרננדס.

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

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

  • ייתכן שהדף כבר כולל מסנן ברכיב הבסיס, והקוד שלנו עשוי לבטל אותו.
  • יכול להיות שהדף כבר מכיל רכיב עם id="deuteranopia", שמתנגש עם הגדרת המסנן שלנו.
  • יכול להיות שהדף מסתמך על מבנה DOM מסוים, ולכן אנחנו עשויים להפר את ההנחות האלה אם מזינים את <svg> ל-DOM.

מלבד זאת, הבעיה העיקרית בגישה הזו היא שנבצע בדף שינויים גלויים פרוגרמטיים. אם משתמש של כלי פיתוח בוחן את ה-DOM, הוא עשוי לפתע לראות רכיב <svg> שהוא מעולם לא הוסיף, או CSS filter שהוא מעולם לא כתב. זה יהיה מבלבל! כדי להטמיע את הפונקציונליות הזו בכלי הפיתוח, אנחנו זקוקים לפתרון שלא כולל את החסרונות האלה.

בואו נראה איך אפשר להפוך את הקטע הזה לפחות פולשני. בפתרון הזה יש שני חלקים שעלינו להסתיר: 1) סגנון ה-CSS עם המאפיין filter, ו-2) הגדרת מסנן SVG, שכרגע היא חלק מה-DOM.

<!-- Part 1: the CSS style with the filter property -->
<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

הימנעות מהתלות של SVG במסמך

נתחיל בחלק 2: איך אפשר להימנע מהוספת ה-SVG ל-DOM? אחד מהעקרונות הוא להעביר אותו לקובץ SVG נפרד. אנחנו יכולים להעתיק את <svg>…</svg> מה-HTML שלמעלה ולשמור אותו כקובץ filter.svg, אבל קודם אנחנו צריכים לבצע כמה שינויים! פורמט SVG מוטבע ב-HTML תואם לכללי ניתוח ה-HTML. זאת אומרת שבמקרים מסוימים תוכלו להשמיט מירכאות מערכי מאפיינים. עם זאת, פורמט SVG בקבצים נפרדים אמור להיות XML חוקי, וניתוח ה-XML מחמיר הרבה יותר מאשר HTML. הנה שוב קטע הקוד של SVG-in-HTML:

<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

כדי ליצור את קובץ ה-SVG הנפרד והתקף הזה (ומכאן גם את ה-XML), אנחנו צריכים לבצע כמה שינויים. רוצה לדעת איזה?

<svg xmlns="http://www.w3.org/2000/svg">
 
<filter id="deuteranopia">
   
<feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000"
/>
 
</filter>
</svg>

השינוי הראשון הוא הצהרת מרחב השמות של ה-XML שמופיעה בחלק העליון. התוספת השנייה היא "solidus" – הקו הנטוי שמציין שהתג <feColorMatrix> פותח וגם סוגר את הרכיב. השינוי האחרון הזה לא נחוץ בפועל (אפשר פשוט לשמור על תג הסגירה </feColorMatrix> המפורש במקום זאת), אבל מכיוון שגם XML וגם SVG-in-HTML תומכים בקיצור /> הזה, יכול להיות שנשתמש בו.

בכל מקרה, בעקבות השינויים האלה, נוכל סוף סוף לשמור את הקובץ הזה כקובץ SVG חוקי, ולהצביע עליו מהערך של מאפיין ה-CSS filter במסמך ה-HTML:

<style>
  :root {
    filter: url(filters.svg#deuteranopia);
  }
</style>

אופס, כבר אין צורך להזריק SVG למסמך! זה כבר הרבה יותר טוב. אבל... עכשיו אנחנו תלויים בקובץ נפרד. זאת עדיין תלות, האם נוכל להיפטר ממנו בדרך כלשהי?

בסופו של דבר מתברר שאנחנו לא צריכים קובץ. אנחנו יכולים לקודד את הקובץ כולו בכתובת URL באמצעות כתובת URL של נתונים. כדי לעשות זאת, אנחנו משתמשים בתוכן של קובץ ה-SVG שהיה לנו בעבר, מוסיפים את הקידומת data:, מגדירים את סוג ה-MIME המתאים. יש לנו כתובת URL חוקית של נתונים שמייצגת את אותו קובץ SVG:

data:image/svg+xml,
  <svg xmlns="http://www.w3.org/2000/svg">
    <filter id="deuteranopia">
      <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                             0.280  0.673  0.047  0.000  0.000
                            -0.012  0.043  0.969  0.000  0.000
                             0.000  0.000  0.000  1.000  0.000" />
    </filter>
  </svg>

היתרון הוא שמעכשיו אין יותר צורך לאחסן את הקובץ בכל מקום, או לטעון אותו מהדיסק או דרך הרשת רק כדי להשתמש בו במסמך ה-HTML שלנו. כך שבמקום להתייחס לשם הקובץ כפי שעשינו קודם, אנחנו יכולים עכשיו להצביע על כתובת האתר של הנתונים:

<style>
  :root {
    filter: url('data:image/svg+xml,\
      <svg xmlns="http://www.w3.org/2000/svg">\
        <filter id="deuteranopia">\
          <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000\
                                 0.280  0.673  0.047  0.000  0.000\
                                -0.012  0.043  0.969  0.000  0.000\
                                 0.000  0.000  0.000  1.000  0.000" />\
        </filter>\
      </svg>#deuteranopia');
  }
</style>

בסוף כתובת ה-URL אנחנו עדיין מציינים את המזהה של המסנן שבו אנחנו רוצים להשתמש, בדיוק כמו קודם. שימו לב שאין צורך לקודד את מסמך ה-SVG בפורמט Base64 בכתובת ה-URL. הפעולה הזו רק תפגע בנוחות הקריאה ותגדיל את הקובץ. הוספנו לוכסנים הפוכים בסוף כל שורה כדי להבטיח שתווי השורה החדשה בכתובת ה-URL של הנתונים לא מסיימים את ליטרל מחרוזת ה-CSS.

עד כה, דיברנו רק על איך לדמות לקויות ראייה באמצעות טכנולוגיית אינטרנט. מעניין, היישום הסופי שלנו בכלי לעיבוד של Blink הוא למעשה די דומה. הנה כלי עזר ל-C++ שהוספנו כדי ליצור כתובת URL של נתונים עם הגדרת מסנן נתונה, על סמך אותה שיטה:

AtomicString CreateFilterDataUrl(const char* piece) {
  AtomicString url =
      "data:image/svg+xml,"
        "<svg xmlns=\"http://www.w3.org/2000/svg\">"
          "<filter id=\"f\">" +
            StringView(piece) +
          "</filter>"
        "</svg>"
      "#f";
  return url;
}

כך אנחנו משתמשים בו כדי ליצור את כל המסננים הדרושים לנו:

AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
  switch (vision_deficiency) {
    case VisionDeficiency::kAchromatopsia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kBlurredVision:
      return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
    case VisionDeficiency::kDeuteranopia:
      return CreateFilterDataUrl(
          "<feColorMatrix values=\""
          " 0.367  0.861 -0.228  0.000  0.000 "
          " 0.280  0.673  0.047  0.000  0.000 "
          "-0.012  0.043  0.969  0.000  0.000 "
          " 0.000  0.000  0.000  1.000  0.000 "
          "\"/>");
    case VisionDeficiency::kProtanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kTritanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kNoVisionDeficiency:
      NOTREACHED();
      return "";
  }
}

השיטה הזו מאפשרת לנו לנצל את מלוא העוצמה של מסנני SVG בלי שתצטרכו להטמיע מחדש שום דבר או להמציא מחדש גלגלים. אנחנו מטמיעים תכונה של Blink Renderer, אבל אנחנו עושים זאת על ידי שימוש בפלטפורמת האינטרנט.

טוב, אז למדנו איך ליצור מסנני SVG ולהפוך אותם לכתובות URL של נתונים שבהן נוכל להשתמש במסגרת הערך של נכס filter ב-CSS. האם אתם יכולים לחשוב על בעיה עם הטכניקה הזו? מתברר שאנחנו לא יכולים להסתמך על כתובת ה-URL של הנתונים שנטענת בכל המקרים, כי יכול להיות שדף היעד מכיל Content-Security-Policy שחוסם את כתובות ה-URL של הנתונים. בהטמעה הסופית ברמת ההבהוב יש שיקול דעת מיוחד כדי לעקוף את ה-CSP בכתובות ה-URL ה"פנימיות" של הנתונים במהלך הטעינה.

מלבד מקרי הקצה, התקדמנו בצורה טובה. מכיוון שאנחנו כבר לא תלויים בכך ש-<svg> מוטבע באותו מסמך, צמצמנו את הפתרון שלנו להגדרה אחת בלבד של נכס CSS filter עצמאי. נהדר! בואו ניפטר גם מזה.

הימנעות מתלות של CSS בתוך המסמך

לסיכום, זה מה שעשינו עד כה:

<style>
  :root {
    filter: url('data:…');
  }
</style>

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

אחד מהרעיונות שהצענו היה ליצור נכס CSS פנימי חדש ל-Chrome, שפועל כמו filter, אבל בשם אחר, כמו --internal-devtools-filter. לאחר מכן נוכל להוסיף לוגיקה מיוחדת כדי להבטיח שהנכס הזה אף פעם לא יופיע בכלי הפיתוח או בסגנונות המחושבים ב-DOM. נוכל אפילו לוודא שהתכונה פועלת רק ברכיב היחיד שעבורו אנחנו זקוקים: רכיב הבסיס. אבל הפתרון הזה לא יהיה אידיאלי: אנחנו נשכפל פונקציונליות שכבר קיימת ב-filter, וגם אם נשתדל להסתיר את הנכס הלא סטנדרטי הזה, מפתחי אתרים עדיין יוכלו לגלות עליו פרטים ולהתחיל להשתמש בו, מה שיביא נזק לפלטפורמת האינטרנט. אנחנו צריכים דרך אחרת להחיל סגנון CSS בלי שהוא יהיה גלוי ב-DOM. יש לך רעיונות?

במפרט ה-CSS יש קטע שמציג את מודל העיצוב החזותי שבו הוא משתמש, ואחד מהמושגים המרכזיים הוא אזור התצוגה. זוהי התצוגה החזותית שדרכה המשתמשים מעיינים בדף האינטרנט. קונספט קשור בקשר הדוק הוא הבלוק המכיל הראשון, שדומה קצת לאזור תצוגה ניתנת להתאמה אישית <div> שקיים רק ברמת המפרט. המפרט מתייחס לקונספט הזה של "אזור תצוגה" בכל מקום. למשל, ידוע לך איך סרגלי גלילה מוצגים בדפדפן כשהתוכן לא מתאים? כל ההגדרות האלה מוגדרות במפרט ה-CSS, על סמך 'אזור התצוגה' הזה.

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

scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
  scoped_refptr<ComputedStyle> viewport_style =
      InitialStyleForElement(GetDocument());
  viewport_style->SetZIndex(0);
  viewport_style->SetIsStackingContextWithoutContainment(true);
  viewport_style->SetDisplay(EDisplay::kBlock);
  viewport_style->SetPosition(EPosition::kAbsolute);
  viewport_style->SetOverflowX(EOverflow::kAuto);
  viewport_style->SetOverflowY(EOverflow::kAuto);
  // …
  return viewport_style;
}

לא צריך להתעמק ב-C++ או במורכבות של מנוע הסגנון Blink, כדי לראות שהקוד הזה מתייחס לאזורי התצוגה z-index, display, position ו-overflow של אזור התצוגה (או ליתר דיוק: הקוד הראשוני שכולל את הבלוק). אלה הם כל המושגים שאולי אתם מכירים מ-CSS! יש קסם נוסף שקשור להקשרים של סידור בערימה, שלא מתורגם ישירות לנכס CSS, אבל באופן כללי אפשר לחשוב על האובייקט viewport כעל משהו שניתן לעצב באמצעות CSS מתוך Blink, בדיוק כמו רכיב DOM - אלא שהוא לא חלק מה-DOM.

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

סיכום

לסיכום המסע הקטן שלנו, התחלנו בבניית אב-טיפוס באמצעות טכנולוגיית אינטרנט במקום C++, ולאחר מכן התחלנו להעביר חלקים ממנו אל Blink Renderer.

  • בשלב הראשון, אב הטיפוס שלנו פועל על ידי הטמעת כתובות URL של נתונים.
  • לאחר מכן, שינינו את כתובות ה-URL של הנתונים הפנימיים האלה כידידותיות ל-CSP, על ידי הוספה מיוחדת של הטעינה שלהן.
  • כדי שלא תהיה אפשרות לזהות את ההטמעה שלנו ב-DOM, באופן פרוגרמטי, העברנו את הסגנונות אל viewport של Blink-internal.

מה שמייחד את היישום הזה הוא שאב-טיפוס ה-HTML/CSS/SVG השפיע בסופו של דבר על התכנון הטכני הסופי. מצאנו דרך להשתמש בפלטפורמת האינטרנט, גם ב-Blink Renderer!

אם רוצים לקבל רקע נוסף, אפשר לעיין בהצעת העיצוב שלנו או בבאג המעקב ב-Chromium, שכולל קישורים לכל התיקונים הקשורים.

מורידים את הערוצים של התצוגה המקדימה.

כדאי להשתמש ב-Chrome Canary, Dev או בטא כדפדפן הפיתוח שמוגדר כברירת מחדל. ערוצי התצוגה המקדימה האלה מעניקים לך גישה לתכונות החדשות של כלי הפיתוח, בודקים ממשקי API מתקדמים של פלטפורמת האינטרנט ומוצאים בעיות באתר לפני שהמשתמשים נתקלים בבעיות!

יצירת קשר עם צוות כלי הפיתוח ל-Chrome

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

  • שלחו לנו הצעה או משוב בכתובת crbug.com.
  • כדי לדווח על בעיה בכלי הפיתוח, לוחצים על אפשרויות נוספות   עוד   > עזרה > דיווח על בעיות בכלי הפיתוח בכלי הפיתוח.
  • אפשר לשלוח ציוץ אל @ChromeDevTools.
  • אפשר לכתוב תגובות לגבי 'מה חדש' בסרטוני YouTube או בקטע 'טיפים לשימוש בכלי הפיתוח' בסרטוני YouTube.