在 Blink Renderer 中模擬色彩視覺不足

Mathias Bynens
Mathias Bynens

本文將說明我們在 DevTools 和 Blink 轉譯器中實作色盲模擬功能的原因和方式。

背景:色彩對比不佳

低對比文字是網路上最常見的自動偵測無障礙功能問題。

列出網站上常見的無障礙問題。目前最常見的問題是低對比度的文字。

根據 WebAIM 對百萬大網站的無障礙分析,超過 86% 的首頁對比度偏低。平均來說,每個首頁都有 36 個獨立的低對比文字

使用開發人員工具找出、瞭解和修正對比問題

Chrome 開發人員工具可協助開發人員和設計人員改善對比,並為網頁應用程式挑選更容易使用的色彩配置:

我們在這份清單中最近加入了一項新工具,與其他工具略有不同。上述工具主要用於顯示對比率資訊,並提供修正對比率的選項。我們發現 DevTools 仍缺少一種讓開發人員進一步瞭解這個問題領域的方式。為解決這個問題,我們在開發人員工具的「算繪」分頁中實作了視覺障礙模擬功能。

在 Puppeteer 中,新的 page.emulateVisionDeficiency(type) API 可讓您以程式輔助方式啟用這些模擬功能。

色覺辨認障礙

大約每 20 人中就有 1 人患有色覺辨認障礙 (也稱為「色盲」,但這個詞彙不太準確)。因為這類障礙會讓使用者更難以分辨不同顏色,進而放大對比問題

彩色蠟筆融化後的圖片,沒有模擬色彩視覺缺陷
彩色蠟筆融化圖片,沒有模擬色覺辨認障礙。
ALT_TEXT_HERE
模擬全然不同的色素對著彩色蠟筆的影響。
模擬對綠色失能症的影響,在融化的蠟筆彩色圖片上顯示。
模擬第二色盲對融蠟筆的彩色圖片的影響。
模擬色盲對彩色蠟筆融化圖片的影響。
模擬紅色盲對融蠟筆的彩色圖片的影響。
模擬三色盲對融蠟筆的彩色圖片的影響。
模擬三色盲對融蠟筆彩色圖片的影響。

身為視力正常的開發人員,您可能會看到開發人員工具針對視覺上看起來沒問題的顏色組合顯示不良的對比度。這是因為對比度公式會考量這些色彩視力缺陷!在某些情況下,仍可閱讀低對比文字,但視障人士就沒有這個優勢。

我們希望讓設計人員和開發人員模擬這些視覺缺陷對其網路應用程式的影響,提供缺少的部分:開發人員工具不僅有助於「尋找」及「修正」對比問題,現在您也能瞭解這些問題!

使用 HTML、CSS、SVG 和 C++ 模擬色彩視覺障礙

在深入探討 Blink 轉譯器的功能實作方式前,建議您先瞭解如何使用網路技術實作等同的功能。

您可以將每種色覺障礙模擬視為涵蓋整個頁面的重疊影像。Web 平台有一種方法可以做到這點:CSS 濾鏡!您可以透過 CSS filter 屬性使用一些預先定義的篩選器函式,例如 blurcontrastgrayscalehue-rotate 等。如要進一步控制,filter 資源也接受可指向自訂 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 列:矩陣的第一列用於計算新的紅色值、第二列是綠色、第三列是藍色,最後一列是 Alpha。

您可能想知道我們的範例中數字的來源。為什麼這個色彩矩陣能很好地模擬綠色盲?答案是:科學!這些值是以 Machado、Oliveira 和 Fernandes 的物理視覺弱勢模擬模型為基礎。

總之,我們有了這個 SVG 濾鏡,現在只要使用 CSS,就能套用到網頁上的任意元素。我們可以針對其他視覺缺陷重複相同的模式。以下是示範:

我們可以按照以下方式建構 DevTools 功能:當使用者在 DevTools UI 中模擬視力障礙時,我們會將 SVG 濾鏡插入檢查的文件中,然後在根元素上套用濾鏡樣式。但這種方法有幾個問題:

  • 頁面可能已在其根元素上加入篩選器,而我們的程式碼可能會覆寫該篩選器。
  • 頁面可能已包含 id="deuteranopia" 元素,與篩選器定義衝突。
  • 網頁可能依賴特定的 DOM 結構,而將 <svg> 插入 DOM 中,我們可能違反這些假設。

除了邊緣情況,這種做法的主要問題在於以程式輔助方式對頁面進行可觀測的變更。如果 DevTools 使用者檢查 DOM,可能會突然看到自己從未新增的 <svg> 元素,或是從未編寫的 CSS filter。這樣會造成混淆!如要在開發人員工具中實作這項功能,我們需要一個沒有這些缺點的解決方案。

讓我們來看看如何讓這項功能不那麼令人分心。這個解決方案有兩個部分需要隱藏:1) 使用 filter 屬性的 CSS 樣式,以及 2) 目前屬於 DOM 的 SVG 篩選器定義。

<!-- 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 部分開始:如何避免將可擴充向量圖形加到 DOM?一個方法是將其移至個別的 SVG 檔案。我們可以從上述 HTML 複製 <svg>…</svg>,並儲存為 filter.svg,但我們必須先進行一些變更!HTML 中的內嵌 SVG 會遵循 HTML 剖析規則。也就是說,即使在某些情況下,在屬性值前後省略引號這類的指令也沒關係。不過,分開的檔案中的 SVG 應為有效的 XML,而 XML 剖析比 HTML 嚴格得多。以下是 HTML 內 SVG 程式碼片段:

<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>

為了讓這個可擴充向量圖形 (以及 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 和 HTML 中的 SVG 都支援這個 /> 速記法,因此我們不妨使用這個方法。

無論如何,在完成這些變更後,我們終於可以將這個圖片儲存為有效的 SVG 檔案,並在 HTML 文件的 CSS filter 屬性值中指向該檔案:

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

太好了,我們現在不必再將 SVG 插入文件!這樣好多了。不過,我們現在需要使用其他檔案。這仍是依附元件。我們可以以某種方式移除嗎?

結果後,我們根本不需要檔案。我們可以使用資料網址,在網址中編碼整個檔案。為此,我們只會擷取先前使用 SVG 檔案的內容、新增 data: 前置字串、設定適當的 MIME 類型,接著我們便得到一個代表同一個 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>

在網址結尾,我們仍會像先前一樣指定要使用的篩選器 ID。請注意,您不需要在網址中對 SVG 文件進行 Base64 編碼,這麼做只會降低可讀性並增加檔案大小。我們在每一行的結尾加上反斜線,確保資料網址中的換行字元不會終止 CSS 字串常值。

目前為止,我們只談瞭如何使用網路技術來模擬視覺缺陷。有趣的是,我們在 Blink 轉譯器中最終實作的內容其實非常相似。以下是我們新增的C++ 輔助公用程式,可根據相同技術,使用指定的篩選器定義建立資料網址:

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 轉譯器功能,但我們會利用網路平台來實作。

我們已經瞭解如何建構 SVG 濾鏡,並將其轉換為可在 CSS filter 屬性值中使用的資料網址。您是否能想到這項技術的問題?事實上,我們無法在所有情況下依賴載入的資料網址,因為目標網頁可能含有會封鎖資料網址的 Content-Security-Policy。我們最終的 Blink 層級實作會特別注意,在載入期間為這些「內部」資料網址略過 CSP。

除了極端的案例,我們還取得了進展。由於我們不再依賴同一個文件中的內嵌 <svg>,因此解決方案實際上只剩下單一自給自足的 CSS filter 屬性定義。太好了!我們現在也來移除這個元素。

避免文件內 CSS 相依性

重點回顧:

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

我們仍依附這項 CSS filter 屬性,這個屬性可能會覆寫實際文件中的 filter 並中斷內容。在開發人員工具中檢查已計算的樣式時,也會顯示這項資訊,這會造成混淆。如何避免這些問題?我們需要設法在文件中加入篩選器,但開發人員還是可以透過程式查看篩選器。

我們所想到的一個想法,是建立新的 Chrome 內部 CSS 屬性,運作方式雖然與 filter 相似,但卻具有不同的名稱,例如 --internal-devtools-filter。接著,我們可以新增特殊邏輯,確保這個屬性不會顯示在開發人員工具或 DOM 中所計算的樣式中。我們甚至可以確保它只會套用於所需的一個元素:根元素。不過,這項解決方案並不理想:我們會複製 filter 現有的功能,即使我們努力隱藏這類非標準資源,網頁開發人員依然可以發現並開始使用這個非標準資源,這對 Web Platform 來說是壞事。我們需要另一種方式來套用 CSS 樣式,但是無法在 DOM 中觀察。請問您知道這方面該找誰嗎?

CSS 規格有一節介紹所使用的視覺格式模型,其中一個重要概念就是可視區域。使用者透過這種視覺化檢視畫面來查詢網頁。所謂的「初始包含區塊」就是密切相關的概念,這是一種僅存在於規格層級的可設定可視區域 <div>。規格所指的其實是整個地點的「可視區域」概念。舉例來說,您知道瀏覽器如何在內容不符合螢幕尺寸時顯示捲軸嗎?這些項目皆在 CSS 規格中定義,並以這個「檢視區」為依據。

這個 viewport 也存在於 Blink 轉譯器中,做為實作詳細資料。以下是程式碼,可根據規格套用預設的 viewport 樣式:

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 樣式引擎的複雜性,即可瞭解這個程式碼會處理 viewport (或更準確地說,初始包含區塊) 的 z-indexdisplaypositionoverflow。這些都是您可能熟悉的 CSS 概念!還有一些與堆疊內容相關的魔法,這些魔法不會直接轉譯為 CSS 屬性,但整體來說,您可以將這個 viewport 物件視為可在 Blink 中使用 CSS 設定樣式的物件,就像 DOM 元素一樣,只是它不是 DOM 的一部分。

這正是我們需要的!我們可以將 filter 樣式套用至 viewport 物件,這會在視覺上影響轉譯,但不會以任何方式干擾可觀察的網頁樣式或 DOM。

結論

在此回顧一下這堂課的重點,我們先使用網頁技術 (而不是 C++) 來建構原型,然後開始將部分原型移至 Blink Renderer。

  • 我們首先透過內嵌資料網址,讓原型設計更為自給自足。
  • 接著,我們透過特殊載入方式,讓這些內部資料網址符合 CSP 規範。
  • 我們將實作項目移至 Blink 內部 viewport,讓實作項目不受 DOM 影響,並且在程式設計上無法觀察到。

這項實作方式的獨特之處在於,HTML/CSS/SVG 原型設計最終影響了技術設計。我們找到了一種方法,即使在 Blink 轉譯器中也能使用網路平台!

如需更多背景資訊,請參閱設計提案Chromium 追蹤錯誤,其中列出所有相關修補程式。

下載預覽管道

建議您將 Chrome Canary開發人員版Beta 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!

與 Chrome 開發人員工具團隊聯絡

請使用下列選項來討論最新功能、更新,或任何與開發人員工具相關的內容。