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

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

רקע: ניגודיות צבעים נמוכה

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

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

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

איך משתמשים בכלי הפיתוח כדי למצוא בעיות של ניגודיות, להבין אותן ולפתור אותן

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

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

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

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

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

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

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

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

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

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

אפשר לחשוב על כל אחת מהסימולציות האלה של לקות בראיית צבעים כשכבת-על שמכסה את כל הדף. בפלטפורמת האינטרנט יש דרך לעשות את זה: מסנני CSS! מאפיין ה-CSS filter מאפשר להשתמש בכמה פונקציות סינון מוגדרות מראש, כמו 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 באופן הבא: כשהמשתמש מדמה לקות ראייה בממשק המשתמש של 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 בחלק העליון. התוספת השנייה היא מה שנקרא 'קו נטוי אנכי' – הקו הנטוי שמציין שתג <feColorMatrix> פותח וסוגר את הרכיב. השינוי האחרון הזה לא הכרחי (אפשר פשוט להשתמש בתג הסגירה הברור </feColorMatrix> במקום זאת), אבל מכיוון שגם XML וגם SVG ב-HTML תומכים בקיצור הדרך </feColorMatrix>, כדאי להשתמש בו./>

בכל מקרה, בעזרת השינויים האלה אפשר סוף סוף לשמור את הקובץ כקובץ SVG תקין, ולהפנות אליו מערך הערך של מאפיין filter ב-CSS במסמך ה-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. כך, במקום להפנות לשם הקובץ כמו שעשינו קודם, אנחנו יכולים להפנות לכתובת ה-URL של הנתונים:

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

עד עכשיו דיברנו רק על סימולציה של ליקויים בראייה באמצעות טכנולוגיית אינטרנט. באופן מעניין, ההטמעה הסופית שלנו ב-Blink Renderer דומה למדי. הנה כלי עזר ב-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 של נתונים שאפשר להשתמש בהן בערך של מאפיין ה-CSS filter. האם יש בעיה כלשהי בשיטה הזו? מתברר שאנחנו לא יכולים להסתמך על כתובת ה-URL של הנתונים שנטענה בכל המקרים, כי יכול להיות שדף היעד מכיל Content-Security-Policy שחוסם כתובות URL של נתונים. ההטמעה הסופית ברמת Blink מקפידה במיוחד לעקוף את CSP בכתובות ה-URL ה"פנימיות" האלה של הנתונים במהלך הטעינה.

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

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

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

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

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

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

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

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

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.

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

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

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

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

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

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

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