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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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