对比度检查——两个颜色、精确的比值,和每一处把你送上线的取整

把前景和背景粘成十六进制、rgb()、hsl()、hwb()、一个 CSS 颜色名或者一个宽色域函数,然后读 WCAG 2.1 和 2.2 的判定,过程一并摊开:三种算法各算一遍的 8 位通道,一个半透明颜色真正画出来的那叠图层,够到线上所需的最小亮度或透明度改动并且把步数数出来,把你粘进去的每一对颜色都测一遍的色板矩阵,还有底下拿你自己的浏览器的颜色解析器、canvas 和计算样式跑的一遍对账。你打的任何东西都不离开这台设备。

比值的两套算术

这个数是什么,以及得到它的那两套算术

对比度比值就是两个亮度之间的一次除法,而每一个亮度是三个字节穿过一条幂曲线得到的结果。全部内容就这么多了。WCAG 把它写成 (L1 + 0.05) / (L2 + 0.05),亮的放分子,其中 L 是在线性化之后的通道上算的 0.2126 R + 0.7152 G + 0.0722 B:一个字节先除以 255,结果如果落在拐点的暗侧就除以 12.92,否则走 ((c + 0.055) / 1.055) ^ 2.4。上下各加的那个 0.05 让黑字白底保持有限,也让一个很淡的背景不至于得出不讲理的高分。这把尺子的天花板是 21 到 1——这就是为什么每一个报出 25 到 1 的工具里都有一个 bug。

一个检查器所有可争论的地方都在更早的一步,在"一个字节"这三个字里。你的颜色不是作为三个字节到来的。它是 hsl(14 50% 50%),或者 rgb(50% 50% 50%),或者 tomato,而在一个字节存在之前,有人得先做除法、乘法、在一个六边形里插值,然后取整。算术把那些分数一直保持精确——用整数之比,不用浮点——并且在最后一步只取一次整,逢半远离零。然后它把答案和另外两个答案并排印出来,因为其中一个就是规范自己那份可打印附录里的算法,而第三个属于你的浏览器。

同一个颜色有三份答案,三份都印出来

字节表对你写的每个颜色给出四栏数。第一栏是算术的,用分数算的。第二栏把同一次转换用普通的 64 位 double 走一遍,用的是 CSS 颜色规范在附录里印出来的那个算法——互联网上大多数代码做的就是这个。第三栏是你的浏览器:把你那串字符画进一个一像素的画布,再从帧缓冲里把字节读回来,所以那一栏不是任何浏览器的模型,它就是你现在正在用的这个浏览器给的答案。

大多数时候三者是一致的。有一个不一致的例子值得从头算一遍,因为它就是全部的那课:hsl(0 80% 50%)。算术的分数说是 230, 26, 26。附录算法用 double 算是 230, 25, 25。起因是一个值正好落在半个单位上。这个颜色的中间通道算出来是 25.5,而 25.5 不是一个 double 存得住的数——于是 double 那条路看到的是它旁边一丝的地方,这一次看到的是 25.49999999999999993,因为路上它算过 1 - 0.9,得到了 0.09999999999999998。精确的那条路看到的就是 25.5,按逢半远离零取整到 26。两个都不是错误:一个是丢掉了一个分数,另一个是故意破的一个平局。

在一百二十乘二十一的网格上、共 52,920 个 hsl() 颜色上——每个隔三度的色相,饱和度和亮度都按五步进——其中 3,960 个有一个通道正压在半个单位上,而两条路对其中 970 个印出了不同的字节:大约 1.8%,也就是每四个平局里有一个。这个数目里有两件事才是重点。第一件事:除非某个通道正好压在半个单位上,否则分歧根本不会发生——这条蕴含关系两个方向都被验过,例外数是零。第二件事:每一次分歧都朝同一个方向取整——double 那条路丢了一个分数,所以它落在精确答案的下方,从不落在上方。如果一个检查器的 hsl() 字节和另一个的差一,发生的就是这件事,它不是它们之中有一个坏了的证据。所以不选赢家:三份都印出来,并且告诉你哪一格不同、为什么不同。你自己浏览器那一栏才是决定访问你的网站的人实际看到什么的那一栏。

一个半透明的颜色不是你的读者看到的那个颜色

rgba(255, 255, 255, 0.5) 压在 #666666 上,不是白字灰底。它是浅灰压灰:白色在半天上刷过 102 落到 179,179,179,而真正到达眼睛的那一对是 179 对 102,也就是 2.74 到 1。改成拿你打进去的那个白去量这个背景,你得到 5.74 到 1——一个"通过",而且错了三个点。很多检查器犯的正是这个错,因为要把这件事算对,意味着你必须对"背景后面是什么"表个态。

所以有第三个盒子:要是它透,它后面是什么。填 #ffffff,背景就摊平成 179,179,179,黑字压上去是 10.02 到 1。留空,就不凭空编一个:这块透明的背景被当成叠在它自己身上,这是唯一一个什么都不改变的假设——同一个颜色合成到同一个颜色之上还是这个颜色——并且它把这句话印在表里而不藏起来。如果你的背景完全不透明,那么你在它后面打的任何东西都会被丢掉,并附一行解释为什么,因为那里没有洞可以让它透出来。

半透明还会把修法挪走。rgba(128, 128, 128, 0.5) 在白底上过不了 4.5 到 1,而且只要它还半透明,就没有任何一个亮度这个灰能过关:在一半的地方,合成结果永远只能在白色和 128 之间,那条路的顶点也在线下方。能过关的是完全不透明时的 0.464 亮度——两个滑块一起走 538 步,落在 #767676 上。会单独搜亮度、单独搜不透明度、也把两个一起搜,每个都按步数计价,给出最便宜的那一个,并且把亚军列在旁边。这就是为什么修法表可以坦然地写"更不透明",而不只是写"更暗"。

画布里面还多一轮量化。一个真实的画布把像素按已经乘过自己 alpha 的形式存着,被问到时再除回去。在一个空白的一像素画布上画 rgb(200 120 40 / 0.31) 然后把字节要回来:这台机器上的 Chrome 答 200, 119, 39——两个通道各低一,而底下没有任何半个单位可争,因为 alpha 自己先变成了 255 分之 79,然后才去乘。这点漂移不是噪声,是一个模型,而合成那一行会说出你的浏览器用的是哪个 8 位模型——画布本该有的那个预乘模型,还是大多数代码顺手就用的、只取一次整的朴素模型——或者干脆明说两个都不是。在上面的 #123456 上,这台机器上的 Chrome 画出来的是 74, 72, 71,而算术的分数给的是 74, 73, 72,预乘模型给的是 74, 73, 71:它在绿色上比画布本该遵循的那个模型还低一步。所以那一行只把缓冲区的答复印出来,不在三份答案里封一个赢家。

字号这件事是由你的样式引擎定夺的,不是由一个系数

大字算 3 到 1 而不是 4.5 到 1,而"大字"意味着 24px,或者在字重 700 及以上时的 56/3px(18.67px)。这两个数一个不是整数,另一个通常根本是拿另一个单位写的:18pt。pt 按 96 除以 72 换成 px,这正是 CSS 定义英寸的方式,rem 则按你的根字号换——但那次换算被当成一句待核对的说法,于是去问你的浏览器 18pt 算出来是多少,把两个答案放进底下那张对账表。如果它们不同意——你改过的根字号、一个没有换算规则的单位——那一行就照实说,并且改用浏览器那个数,因为将来给你的页面排版的是浏览器。

字重只在那个 18.67px 的边界上要紧,而且只关系到 700 及以上:19px 配字重 600 是正文,24px 配字重 400 是大字。第一节里那个灰正好能把整条字号规则讲完:#777777 压在白底上是 4.48 到 1,按正文差 0.02 过关不了,按大字富余一整点通过,所以同一对颜色既是缺陷又是合格,差别只在样式表里的一行。

修法从哪里来,以及它在什么地方诚实地没有

“白底上能通过的最浅的灰是哪个"那个著名答案是 #767676,而它是一步一步走出来的:从 #777777 起,亮度刻度上走三步、从 119 到 118,而 118 给出 4.542 到 1。那张表的最后一栏是步数,也正是读者可以自己动手核对的那部分。它下面那一行——“这个结论撑得住的亮度范围”——是反方向的走法:从 0.465 到 1.000,每一个保持同一个过或不过的亮度,于是你知道在结论翻面之前能动多远。

有些对子怎么动前景都修不好。黑字压在 #7f7f7f 上是 5.24 到 1:它跨过了 4.5,而 7 到 1 够不着,因为这块背景对黑字太亮、对白字又太暗,文字的任何亮度或任何不透明度都改变不了这件事。上限会被印出来——两个滑块各自能做到的最好,在你打的 alpha 下和在完全不透明下——然后说出剩下唯一一句真话:该动的是背景。上限不会被打扮成一个修法。(4.5 到 1 本身以这种方式是够不到的,其原因值得知道:在任何一个 8 位背景上,纯白或纯黑总有一个能跨过它,所以那里的一个"不过"永远有一条出路。)

色板那个盒子把同一套合成跑在你粘进来的每一对有序对上——最多十二个颜色、132 对,最差的排前面——而每一对里上面那个颜色都先合成到下面那个之上再去量,所以一块半透明的样品是按它显示出来的样子量的,不是按它被打进来的样子量的。

拐点之争,在八位上根本不可能有影响

有的文档把线性化的拐点写成 0.03928,有的写成 0.04045;这个差别是给 sRGB 标准打的一个后补补丁,WCAG 没有跟。它是一场真实的分歧,而在 8 位上它是一场观测不到的分歧。两个拐点按字节算落在 10.0164 到 10.31475 之间——一个 0.3 个字节宽的窗口。256 个通道值被全枚举了一遍找落在里面的那一个,一个也没有。所以字节 10 和字节 11 各走哪个公式分支,跟你信哪一个公布数字无关,而你在这里读到的比值对这个争论不敏感。两个拐点之间那个带子会被直接印出来,而不是请你相信查过。

永久不在范围内的事

最后那张表不是一份失败清单,而是一句范围声明,你打不打东西它都在那里。它拒绝 APCA,那个 Lc 是拿调过的表查出来的,不是一个 WCAG 判定。它拒绝替你的显示器说话,因为这些字节和你的屏幕之间还隔着操作系统、显示配置文件和任何夜间模式滤镜。它拒绝谈字形:笔画粗细、hinting 和字体挑选会在同一个比值下改变可读性,而那个比值说的是两个颜色的事,别的一概不说。它拒绝字后面的渐变和照片,因为一个比值需要两个平色。它也拒绝 contrast-color(),哪怕你的浏览器解析得动它——这台机器上的 Chrome 被问到支不支持这个关键字时答的是「支持」,而对账表把它这个答复单独印在一行里——理由是一个替你挑前景色的检查器会把你最终用的是哪个前景色藏起来,而你问的那个比值正是会被印出来的那一个。它也拒绝抄一份颜色名表:它去问你的浏览器 tomato 是什么,再把答复印出来——这同时意味着一个宽色域的 color(display-p3 1 0 0) 是被量出来的,不是被猜出来的:在将要显示它的那台机器上,在它要画进的那个色域里。在这台机器上那个答案是 255, 0, 0:一个普通的 8 位画布在这里是把分量夹进画布自己的色域,而不是做转换,所以 p3 的红落在了 sRGB 的红上,印在它旁边的那个比值是缓冲区真实持有的那几个字节之比,不是某个色彩管理模型许诺出来的那一个。

这一切都在你的浏览器里、在一个文件里、就着你打的那两串字符跑完。你打的任何东西都不离开这台设备:没有东西会被打发出去算分;那份报告是从屏幕上已有的数拼出来的,所以你可以直接把它粘进工单,不用先查有没有自留一份。

怎么读底下那张对账表

那张表是同一个问题问两遍——一遍用精确的分数问,一遍问你的浏览器——再加一栏判定。那一栏里只会出现三个词。一样表示两个实现印出了一模一样的字节。不一样表示没有,而那一行最后一栏会说为什么:某个通道正压在半个单位上、画布的缓冲按预乘存着这个颜色、或者一个不会被自己转换的色域。没对成表示你的浏览器没有提供任何可对照的东西——没有画布、没有计算样式、一个把这串字符拒了的解析器——就印一个空着的对照,也不印一个让人安心的。

那里有两个行为值得知道,因为两个都是量出来的而不是想出来的。给样式引擎拒收的颜色赋值并不会把那个属性清空;它把上一个值留在原地,看起来像一个答复,所以这里每一个探针都先装上一个哨兵值,而读回来等于哨兵的那次才被当作拒答。还有一个游离的元素算不出任何东西,所以那个字号探针是先挂上、读、然后才摘掉的。关于 CSS.supports 的那几行也不给你的浏览器打分:它们记下这个浏览器解析得动五种颜色写法里的哪几种,而每一种自己给出的态度就印在旁边——你就是这么看出:什么都不插值、拒绝替你挑颜色、并且 hwb() 那段算术是直接精确做的。

微信
微信
支付宝
支付宝