本文介绍了我们在开发者工具和 Blink 渲染程序中实现色觉缺陷模拟的原因和方式。
背景:色彩对比度不佳
低对比度文本是网络上最常见的自动检测到的无障碍功能问题。
根据 WebAIM 对前 100 万个网站的无障碍功能分析,超过 86% 的首页对比度较低。平均而言,每个首页都有 36 个不同的低对比度文本实例。
使用开发者工具查找、了解和修复对比度问题
Chrome DevTools 可以帮助开发者和设计师提高对比度,并为 Web 应用选择更易于访问的配色方案:
- 网页顶部显示的“检查模式”提示会显示文本元素的对比度。
- 开发者工具的颜色选择器会指出文本元素的对比度不佳,显示建议的对比度线,以帮助手动选择更好的颜色,甚至可以建议易辨认的颜色。
- CSS Overview 面板和 Lighthouse 无障碍功能审核报告都会列出网页上发现的低对比度文本元素。
我们最近在此列表中添加了一款新工具,它与其他工具略有不同。上述工具主要用于显示对比度信息,并为您提供修正对比度的方法。我们意识到,开发者仍缺少一种方法来更深入地了解此问题领域。为此,我们在开发者工具的“Rendering”(呈现)标签页中实现了视觉缺陷模拟。
在 Puppeteer 中,您可以使用新的 page.emulateVisionDeficiency(type)
API 以编程方式启用这些模拟。
色觉缺陷
大约每 20 个人中就有 1 人患有色觉缺陷(也称为“色盲”,这个术语不太准确)。此类缺陷导致更难以区分不同的颜色,而这可能会放大对比度问题。
作为视力正常的开发者,您可能会发现,开发者工具会针对看起来没有问题的颜色对显示对比度较低的结果。这是因为对比度公式考虑到了这些色觉缺陷!在某些情况下,您可能仍能看清低对比度文本,但患有视力障碍的用户却没有这样得天独厚的条件。
我们希望通过让设计师和开发者模拟这些视力缺陷对其 Web 应用的影响,提供缺失的一环:开发者工具不仅可以帮助您发现和修复对比度问题,现在您还可以了解这些问题!
使用 HTML、CSS、SVG 和 C++ 模拟色觉缺陷
在深入了解该功能的 Blink Renderer 实现之前,了解如何使用 Web 技术实现等效功能会很有帮助。
您可以将这些色觉缺陷模拟功能视为覆盖整个网页的叠加层。网络平台提供了一种方法来实现这一点:CSS 过滤器!借助 CSS filter
属性,您可以使用一些预定义的过滤函数,例如 blur
、contrast
、grayscale
、hue-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-index
、display
、position
和 overflow
。这些都是您可能熟悉的 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 相关的新功能、更新或任何其他内容。
- 请访问 crbug.com 向我们提交反馈和功能请求。
- 在 DevTools 中,依次选择 More options > Help > Report a DevTools issue 以报告 DevTools 问题。
- 请发送电子邮件至 @ChromeDevTools。
- 在 “开发者工具的新变化”YouTube 视频或 “开发者工具提示”YouTube 视频中留言。