在 Blink Renderer 中模拟色觉缺陷

Mathias Bynens
Mathias Bynens

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

背景:色彩对比度不佳

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

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

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

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

Chrome 开发者工具可以帮助开发者和设计人员提高对比度,并为 Web 应用选择更方便易用的配色方案:

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

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

色觉缺陷

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

<ph type="x-smartling-placeholder">
</ph> 融化的蜡笔的彩色图片,没有模拟的色觉缺陷 <ph type="x-smartling-placeholder">
</ph> 已融化的蜡笔的彩色图片,未模拟色觉缺陷。
<ph type="x-smartling-placeholder">
</ph> ALT_TEXT_HERE
模拟全色盲对已融化的蜡笔的彩色图片的影响。
<ph type="x-smartling-placeholder">
</ph> 模拟绿色盲对融化蜡笔的彩色图片的影响。
模拟绿色盲对融化蜡笔的彩色图片的影响。
<ph type="x-smartling-placeholder">
</ph> 模拟红色盲对融化蜡笔的彩色图片的影响。
模拟红色盲对融化蜡笔的彩色图片的影响。
<ph type="x-smartling-placeholder">
</ph> 模拟色盲对已融化蜡笔的彩色图片的影响。
模拟色盲对已融化蜡笔的彩色图片的影响。

作为视力正常的开发者,您可能会发现开发者工具针对看起来没有问题的颜色对显示对比度不佳。这是因为对比度公式考虑到了这些色觉缺陷!在某些情况下,或许仍能阅读低对比度文字,但视障人士则没有这种特权。

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

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

在我们深入了解功能的 Blink Renderer 实现之前,最好先了解一下如何使用网络技术实现等效功能。

您可以将每个色觉缺陷模拟视为覆盖整个页面的叠加层。网络平台提供了一种方法来实现这一点: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 将其应用到网页上的任意元素。我们可以对其他视觉缺陷重复上述模式。其效果如下所示:

如果需要,我们可以按如下方式构建开发者工具功能:当用户在开发者工具界面中模拟视觉缺陷时,我们将 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 命名空间声明。第二行添加的代码是所谓的“solidus”,即代表 <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 并破坏内容。在 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。

总结

回顾一下,我们最初使用网络技术(而不是 C++)构建了一个原型,然后开始将其中的部分组件迁移到 Blink Renderer。

  • 我们最初通过内嵌数据网址使原型更具独立性。
  • 然后,我们通过对加载进行特殊大小写,使这些内部数据网址适合 CSP。
  • 通过将样式移至 Blink-internal viewport,我们的实现与 DOM 无关,并且以编程方式无法观察。

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

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

下载预览渠道

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

与 Chrome 开发者工具团队联系

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

  • 请通过 crbug.com 提交建议或反馈。
  • 使用更多选项报告开发者工具问题 展开 >帮助 >在开发者工具中报告开发者工具问题
  • 请发送电子邮件至 @ChromeDevTools
  • 请对我们的开发者工具新功能 YouTube 视频或开发者工具提示 YouTube 视频发表评论。