在 Blink Renderer 中模拟色觉缺陷

马蒂亚斯·拜恩斯
Mathias Bynens

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

背景:色彩对比度不佳

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

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

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

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

Chrome 开发者工具可以帮助开发者和设计师提高对比度,并为 Web 应用选择更符合无障碍标准的配色方案:

我们最近在此列表中添加了一款新工具,它与其他工具略有不同。上述工具主要侧重于显示对比度信息以及为您提供用于修正的选项。我们意识到,开发者工具仍然无法让开发者更深入地了解此问题。understanding为了解决这个问题,我们在开发者工具中的“渲染”标签页中实现视觉缺陷模拟

在 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 值,第二行为绿色,第三行为蓝色,最后一行为 Alpha。

您可能想知道示例中的确切数字来自哪里。是什么让该颜色矩阵成为了绿色盲的良好近似值?答案是:科学!这些值基于由 Machado、Oliveira 和 Fernandes 开发的生理学准确的色觉缺陷模拟模型

总之,我们已经有了这个 SVG 滤镜,现在我们可以使用 CSS 将其应用于页面上的任意元素。我们可以针对其他视觉缺陷重复上述模式。其效果演示如下:

如果需要,我们可以按如下方式构建开发者工具功能:当用户在开发者工具界面中模拟视觉缺陷时,我们将 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-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 字符串字面量。

到目前为止,我们只讨论了如何使用网络技术来模拟视觉缺陷。有趣的是,我们在 Blink Renderer 中的最终实现实际上非常相似。以下是我们添加的 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 并破坏内容。在开发者工具中检查计算的样式时,也会显示该颜色,这会使用户感到困惑。如何避免这些问题?我们需要设法向文档添加过滤器,但又不会导致开发者能够通过编程方式进行观察。

我想到了一个想法,那就是创建一个新的 Chrome 内部 CSS 属性,它的行为方式类似于 filter,但名称不同,比如 --internal-devtools-filter。然后,我们可以添加特殊逻辑来确保此属性永远不会显示在开发者工具中或 DOM 中计算的样式中。我们甚至可以确保它只适用于需要它的一个元素,即根元素。但是,这种解决方案并不理想:我们会通过 filter 复制已有的功能,即使我们尽量隐藏这个非标准属性,网站开发者仍然能够找到它并开始使用,这对 Web 平台来说可不利。我们需要通过其他方式来应用 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 的 Style 引擎的复杂之处,便可看到此代码会处理视口的 z-indexdisplaypositionoverflow(更准确地说是初始包含块)。这些都是您在 CSS 中可能比较熟悉的概念!还有一些与堆叠上下文有关的其他魔法,并不直接转换为 CSS 属性,但总体而言,您可以将此 viewport 对象视为可在 Blink 中使用 CSS 设置样式的对象,就像 DOM 元素一样,只不过它不是 DOM 的一部分。

这正是我们想要的功能!我们可以将 filter 样式应用于 viewport 对象,这会对渲染产生视觉影响,而不会以任何方式干扰可观察的页面样式或 DOM。

总结

回顾一下这次小小的旅程,我们首先使用网络技术而非 C++ 构建了一个原型,然后开始将它的部分内容转移到 Blink 渲染程序中。

  • 首先,我们通过内嵌数据网址使原型更加独立。
  • 然后,通过特殊格式加载这些内部数据网址,使这些内部数据网址适合内容安全政策。
  • 通过将样式移至 Blink-internal viewport,我们的实现与 DOM 无关,并且无法通过编程实现可观测性。

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

如需了解更多背景信息,请查看我们的设计方案Chromium 跟踪 bug,其中引用了所有相关补丁。

下载预览渠道

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

与 Chrome 开发者工具团队联系

使用以下选项讨论博文中的新功能和变化,或讨论与开发者工具有关的任何其他内容。

  • 请通过 crbug.com 向我们提交建议或反馈。
  • 使用开发者工具中的更多选项   了解详情   > Help > Report a DevTools issues,报告开发者工具问题。
  • 您可以前往 @ChromeDevTools 发 Twitter 微博。
  • 请在 YouTube 视频或“开发者工具提示”YouTube 视频中留言说明“开发者工具的新变化”。