在 Blink Renderer 中模拟色觉缺陷

Mathias Bynens
Mathias Bynens

本文介绍了我们在开发者工具和 Blink 渲染程序中实现色觉缺陷模拟的原因和方式。

背景:色彩对比度不佳

低对比度文本是网络上最常见的自动检测到的无障碍功能问题。

网页上常见的无障碍功能问题列表。低对比度文本是目前最常见的问题。

根据 WebAIM 对前 100 万个网站的无障碍功能分析,超过 86% 的首页对比度较低。平均而言,每个首页上有 36 个不同的低对比度文字实例

使用开发者工具查找、了解和修复对比度问题

Chrome DevTools 可以帮助开发者和设计师提高对比度,并为 Web 应用选择更易于访问的配色方案:

我们最近向此列表中添加了一款新工具,这款工具与其他工具略有不同。上述工具主要侧重于显示对比度信息以及提供修正选项的选项。我们意识到,DevTools 仍然缺少一种方法来帮助开发者更深入地了解这一问题领域。为此,我们在开发者工具的“Rendering”(呈现)标签页中实现了视觉缺陷模拟

在 Puppeteer 中,您可以使用新的 page.emulateVisionDeficiency(type) API 以编程方式启用这些模拟。

色觉缺陷

大约每 20 个人中就有 1 人患有色觉缺陷(也称为“色盲”,这个术语不太准确)。此类缺陷导致更难以区分不同的颜色,而这可能会放大对比度问题

融化的蜡笔的彩色图片,没有模拟的色觉缺陷
一张色彩鲜艳的融化的蜡笔照片,未模拟色觉缺陷。
ALT_TEXT_HERE
模拟全色盲对彩色融蜡笔图片的影响。
模拟绿色盲对融化蜡笔的彩色图片的影响。
模拟色盲对彩色融化蜡笔图片的影响。
模拟色盲对彩色融化蜡笔图片的影响。
模拟红色盲对融化的蜡笔的彩色图片的影响。
模拟色盲对彩色融化蜡笔图片的影响。
模拟色盲对彩色融化蜡笔图片的影响。

作为视力正常的开发者,您可能会发现开发者工具针对看起来没有问题的颜色对显示对比度不佳。之所以会出现这种情况,是因为对比度公式会考虑到这些色觉缺陷!在某些情况下,可能仍能看清低对比度文本,但患有视力障碍的用户却没有这样得天独厚的条件。

通过让设计人员和开发者模拟这些视觉缺陷在自己的 Web 应用中的效果,我们致力于提供这方面的缺失:开发者工具不仅可以帮助您查找修复对比度问题,现在您还可以理解这些问题!

使用 HTML、CSS、SVG 和 C++ 模拟色觉缺陷

在深入了解该功能的 Blink Renderer 实现之前,了解如何使用 Web 技术实现等效功能会很有帮助。

您可以将这些色觉缺陷模拟功能视为覆盖整个网页的叠加层。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 界面中模拟视力缺陷时,我们会将 SVG 滤镜注入到被检查的文档中,然后将滤镜样式应用于根元素。不过,这种方法存在几个问题:

  • 网页的根元素可能已经有过滤器,我们的代码可能会覆盖该过滤器。
  • 网页可能已经包含 id="deuteranopia" 元素,与我们的过滤条件定义冲突。
  • 网页可能依赖于特定的 DOM 结构,而将 <svg> 插入 DOM 可能会违反这些假设。

除了极端情况之外,这种方法的主要问题在于,我们将以程序化方式对网页进行可观察的更改。如果 DevTools 用户检查 DOM,可能会突然看到自己从未添加的 <svg> 元素或从未编写的 CSS filter。这很令人困惑!如需在 DevTools 中实现此功能,我们需要一个不存在这些缺点的解决方案。

让我们来看看如何减少干扰。此解决方案中有两个部分需要隐藏: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 部分开始:如何避免将 SVG 添加到 DOM?一种想法是将其移至单独的 SVG 文件中。我们可以从上面的 HTML 中复制 <svg>…</svg> 并将其另存为 filter.svg,但首先需要进行一些更改!HTML 中的内嵌 SVG 遵循 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-in-HTML 都支持此 /> 简写形式,因此我们也可以使用它。

无论如何,完成这些更改后,我们最终就可以将此文件另存为有效的 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 字符串字面量。

到目前为止,我们只讨论了如何使用 Web 技术模拟视觉缺陷。有趣的是,我们在 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 Renderer 功能,但会利用 Web 平台来实现。

好的,我们已经了解了如何构建 SVG 滤镜并将其转换为可在 CSS filter 属性值中使用的的数据网址。您能想到这种方法存在的问题吗?事实证明,我们实际上无法在所有情况下依赖加载的数据网址,因为目标网页可能包含用于屏蔽数据网址的 Content-Security-Policy。我们在最终的 Blink 级实现中特别注意了在加载期间为这些“内部”数据网址绕过 CSP。

除了极端情况之外,我们取得了一些不错的进展。由于我们不再依赖于内嵌 <svg> 位于同一文档中,因此我们有效地将解决方案缩减为仅包含一个自包含的 CSS filter 属性定义。太棒了!现在,我们也来移除它。

避免出现文档内 CSS 依赖项

总结一下,我们目前的进展如下:

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

我们仍然依赖于此 CSS filter 属性,该属性可能会替换实际文档中的 filter 并破坏内容。在 DevTools 中检查计算的样式时,它也会显示,这会造成混淆。如何才能避免这些问题?我们需要找到一种向文档添加过滤器的方法,而不会让开发者能够以编程方式观察到过滤器。

我们想到的一个想法是,创建一个 Chrome 内部 CSS 属性,其行为与 filter 类似,但名称不同,例如 --internal-devtools-filter。然后,我们可以添加特殊逻辑,以确保此属性永远不会显示在 DevTools 或 DOM 中的计算样式中。我们甚至可以确保它只对需要它的那一个元素起作用,那就是根元素。不过,这种解决方案并不理想:我们将重复 filter 中已有的功能,即使我们努力隐藏此非标准属性,Web 开发者仍可能会发现并开始使用它,这对 Web 平台不利。我们需要通过其他方式来应用 CSS 样式,但该样式在 DOM 中不可观察。请问您知道应该找谁吗?

CSS 规范中有一个部分介绍了它使用的可视化格式模型视口是一个重要概念。这是用户查看网页的视觉视图。一个密切相关的概念是初始容器块,它有点像仅在规范级别存在的可样式视口 <div>。规范中随处都提及了“视口”这一概念。例如,您知道浏览器如何在内容不合适时显示滚动条吗?CSS 规范中基于此“视口”定义了所有这些元素。

viewport 也作为实现细节存在于 Blink Renderer 中。按照规范应用默认视口样式的代码如下:

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 要求。
  • 我们通过将样式移至 Blink 内部的 viewport,使我们的实现不依赖于 DOM 且无法通过程序化方式观察到。

此实现的独特之处在于,我们的 HTML/CSS/SVG 原型最终影响了最终的技术设计。我们找到了一种使用该网络平台的方法,即使在 Blink Renderer 中也能使用!

如需了解更多背景信息,请参阅我们的设计提案Chromium 跟踪 bug,其中列出了所有相关补丁。

下载预览渠道

不妨考虑将 Chrome Canary 版开发者版Beta 版用作默认开发浏览器。通过这些预览渠道,您可以使用最新的开发者工具功能,测试先进的网络平台 API,并帮助您在用户之前发现网站上的问题!

与 Chrome DevTools 团队联系

您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。