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

במאמר הזה נסביר למה ואיך יישמנו סימולציה של ליקוי בראיית צבעים בכלי הפיתוח ובכלי הרינדור בלי קישור (Blink Renderer).

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

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

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

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

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

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

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

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

חוסר ראיית צבעים

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

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

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

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

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

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

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

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

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

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

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

אחד מהרעיונות שלנו היה ליצור נכס 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 Renderer.

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

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

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

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

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

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

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

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