在 Blink Renderer 中模拟色觉缺陷

Mathias Bynens
Mathias Bynens

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

背景:色彩对比度不佳

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

网页上常见的无障碍功能问题列表。迄今为止,低对比度文本是最常见的问题。

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

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

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

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

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

色觉缺陷

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

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

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

我们希望通过让设计师和开发者模拟这些视力缺陷对其 Web 应用的影响,提供缺失的一环:开发者工具不仅可以帮助您发现修复对比度问题,现在您还可以了解这些问题!

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

在深入了解该功能的 Blink Renderer 实现之前,了解如何使用 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 行:矩阵的第一行用于计算新的 Red 值,第二行用于计算新的红色值,第三行用于计算 Blue 值,最后一行 Alpha 值。

您可能想知道示例中的确切数字是从何而来。是什么让此颜色矩阵能很好地近似于色盲?答案是:科学!这些值基于 Machado、Oliveira 和 Fernandes 的色觉缺陷生理学准确模拟模型

无论如何,我们已经有了这个 SVG 滤镜,现在可以使用 CSS 将其应用于网页上的任意元素。我们可以针对其他视觉缺陷重复相同的模式。下面的演示展示了该功能的运作方式:

如果需要,我们可以按如下方式构建我们的 DevTools 功能:当用户在 DevTools 界面中模拟视力缺陷时,我们会将 SVG 滤镜注入到被检查的文档中,然后将滤镜样式应用于根元素。不过,这种方法存在几个问题:

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

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

我们来看看如何降低此功能的侵扰性。此解决方案需要隐藏两部分: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-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 和 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 字符串字面量。

到目前为止,我们只讨论了如何使用 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 功能,但是为了利用网络平台来实现。

好的,我们已经了解了如何构建 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。然后,我们可以添加特殊逻辑,以确保此属性永远不会出现在开发者工具或 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。

总结

回顾一下我们在本教程中所做的事,我们首先使用 Web 技术(而非 C++)构建了一个原型,然后开始将其部分内容移至 Blink 渲染程序。

  • 首先,我们通过内嵌数据网址,使原型更加自足。
  • 然后,我们通过对加载进行特殊大小写,使这些内部数据网址适合 CSP。
  • 我们通过将样式移至 Blink 内部的 viewport,使我们的实现不依赖于 DOM 且无法通过程序化方式观察到。

此实现的独特之处在于,我们的 HTML/CSS/SVG 原型最终影响了最终的技术设计。我们找到了一种使用 Web 平台的方法,即使在 Blink 渲染程序中也可以!

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

下载预览渠道

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

与 Chrome DevTools 团队联系

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