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

馬蒂亞斯.拜恩斯 (Mathias Bynens)
Mathias Bynens

本文說明我們在開發人員工具和 Blink Renderer 中實作色彩視覺障礙的原因和方式。

背景:色彩對比度不佳

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

網路上常見的無障礙問題清單。低對比文字是目前最常見的問題。

根據 WebAIM 針對前 100 萬個熱門網站進行的無障礙分析,超過 86% 的首頁對比度較低。每個首頁平均有 36 種不同的低對比文字

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

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

我們最近在這份清單中新增了一項新工具,而且工具與其他工具不同。上述工具主要著重於「顯示對比度資訊」,並提供「修正」選項。我們發現,開發人員工具仍缺少方法,讓開發人員進一步瞭解這個問題空間的understanding。為解決此問題,我們在開發人員工具「轉譯」分頁中實作了視覺障礙模擬體驗

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

色彩視覺障礙

有大約 1/20 的人患有色覺障礙,也就是較不準確的「色盲」一詞。這類障礙會使系統更難以區分不同顏色,因此可能會提高對比問題

彩色金屬蠟筆的圖片,未模擬任何色彩視覺障礙
彩色粉彩融化相片,未模擬任何色彩視覺障礙。
ALT_TEXT_HERE
模擬全色盲人士對色彩繽紛的融化蠟像造成的影響。
模擬糖尿病對色彩繽紛融化蠟像的影響。
模擬糖尿病對色彩繽紛融化蠟像的影響。
模擬紅色盲對色彩融化蠟像的影響。
模擬紅色盲對彩色金屬蠟筆造成的影響。
模擬藍色盲對彩繪蠟像的繽紛圖片的影響。
模擬藍色盲對色彩融化蠟像的影響。

如果您是擁有一般視覺能力的開發人員,您可能會發現開發人員工具在視覺上看起來正常,但色彩配對的對比度不佳。這是因為對比度公式會將這些色彩視覺障礙納入考量!在某些情況下,您或許還是能讀取低對比度的文字,但視障人士沒有該權限。

我們允許設計人員和開發人員模擬這些視覺障礙對自家網頁應用程式的影響,藉此提供缺少的功能:開發人員工具不僅可協助您「找出」及「修正」對比問題,現在您也可以瞭解這些問題!

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

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

您可以將每個色彩視覺障礙模擬結果視為覆蓋整個頁面的重疊元素。Web Platform 即可達成這個目標: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 將圖片套用至網頁上的任意元素。我們針對其他視覺障礙者重複執行相同的模式。外觀如下:

我們可按照以下方式建構開發人員工具功能:當使用者在開發人員工具 UI 中模擬視覺障礙時,在檢查過的文件中插入 SVG 濾鏡,然後在根元素上套用篩選器樣式。不過,該方法有幾個問題:

  • 網頁的根元素可能已經設有篩選器,程式碼可能會覆寫該篩選器。
  • 網頁可能已經有包含 id="deuteranopia" 的元素,但不符合篩選器定義。
  • 頁面可能依賴特定的 DOM 結構,而將 <svg> 插入 DOM 中,可能會違反這些假設。

除了邊緣案例之外,這種方法的主要問題在於,我們會透過程式輔助方式,對網頁進行變更。如果開發人員工具的使用者檢查 DOM,他們可能會突然看到從未新增的 <svg> 元素,或從未編寫的 CSS filter。我聽起來可能你困惑!如要在開發人員工具中實作這項功能,我們需要具有這類缺點的解決方案。

來看看如何降低幹擾程度。此解決方案必須隱藏兩個部分:1) 使用 filter 屬性的 CSS 樣式,以及 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 檔案。我們可以從上述 HTML 複製 <svg>…</svg>,並將其儲存為 filter.svg,但我們需要先進行一些變更!在 HTML 中內嵌 SVG 會遵循 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 命名空間宣告。第二個加上的字元就是所謂的「solidus」,意指 <feColorMatrix> 標記會開啟並關閉元素。此次變更實際上並非必要 (我們可改為使用明確的 </feColorMatrix> 結束標記),但因為 XML 和 SVG-in-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 並損毀內容。在開發人員工具中檢查計算樣式時也會顯示這個 ID,造成混淆。我們該如何避免這些問題?我們必須設法在文件中加入篩選器,而不需要讓開發人員觀察到篩選器。

我們的想法是建立新的 Chrome 內部 CSS 屬性,其運作方式與 filter 類似,但名稱不同 (例如 --internal-devtools-filter)。接著,我們可以新增特殊邏輯,確保這個屬性永遠不會出現在開發人員工具或 DOM 中的計算樣式中。我們甚至可以確保這項技術能在我們所需的單一元素 (根元素) 上執行。不過,這項解決方案並非理想:我們會複製 filter 既有的功能,即使我們難以隱藏這項非標準資源,網頁開發人員仍可找出並開始使用,這樣對 Web Platform 來說並非準確。我們需要其他套用 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-indexdisplaypositionoverflow)。這些都是您可能熟悉的 CSS 概念!堆疊結構定義仍有其他神奇效果,無法「直接」轉譯為 CSS 屬性,但整體來說,您可以將這個 viewport 物件想成可以在 Blink 中使用 CSS 設定樣式,就像在 DOM 元素中使用 DOM 元素一樣。

這正是我們的目標!我們可以將 filter 樣式套用至 viewport 物件,這能影響算繪結果,而不會以任何方式乾擾可觀察的頁面樣式或 DOM。

結語

為了回顧這邊的這個過程,我們先從網頁技術 (而非 C++) 建構原型,接著著手將部分內容遷移到 Blink Renderer。

  • 首先,我們透過內嵌資料網址讓原型設計更加完善。
  • 然後,我們以特別的方式處理這些內部資料網址的載入,讓 CSP 支援 CSP。
  • 我們將樣式移至 Blink-internal viewport,將樣式從 DOM 通用和程式輔助無法觀測。

這項導入作業的獨特之處在於,我們的 HTML/CSS/SVG 原型實際影響了最終的技術設計。我們找到了使用「網路平台」的方法,甚至可以透過 Blink 轉譯器使用!

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

下載預覽頻道

建議您使用 Chrome Canary開發人員版Beta 版做為預設開發瀏覽器。這些預覽管道可讓您使用最新的開發人員工具、測試最先進的網路平台 API,以及在使用者操作之前在網站上找出問題!

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

使用下列選項,在文章中討論新功能和異動,或與開發人員工具相關的任何其他內容。

  • 透過 crbug.com 提供建議或意見。
  • 如要回報開發人員工具問題,請在開發人員工具中依序點選「更多選項」更多   >「說明」 >「回報開發人員工具的問題」
  • @ChromeDevTools 張貼推文。
  • 歡迎前往開發人員工具的 YouTube 影片或開發人員工具的 YouTube 影片提供新功能留言。