给定一个系统(例如一个网站),允许用户自定义某些部分的背景色,但不允许自定义字体颜色(以保持选项的数量最小化),是否有一种方法可以通过编程来确定“浅色”或“深色”字体颜色是必要的?

我相信有一些算法,但我对颜色、光度等了解不够,无法自己找出答案。


当前回答

简短的回答:

计算给定颜色的亮度(Y),并根据预先确定的中间对比度图将文本翻转为黑色或白色。对于典型的sRGB显示,当Y < 0.4(即40%)时切换为白色

再回答

毫不奇怪,这里几乎每个答案都有一些误解,和/或引用了不正确的系数。唯一接近的答案是Seirios,尽管它依赖于WCAG 2对比,而WCAG 2对比本身是不正确的。

如果我说“不足为奇”,部分原因是互联网上关于这个特定主题的大量错误信息。事实上,这一领域仍然是一个活跃的研究和悬而未决的科学的主题,增加了乐趣。我得出这个结论是由于过去几年对一种新的可读性对比预测方法的研究。

视知觉领域是密集而抽象的,也是不断发展的,因此误解的存在是很常见的。例如,HSV和HSL甚至在感知上都不接近准确。为此,您需要一个感知上统一的模型,如CIELAB或CIELUV或CIECAM02等。

一些误解甚至已经进入了标准,例如WCAG 2(1.4.3)的对比部分,在其大部分范围内都被证明是不正确的。

第一个解决办法:

这里许多答案中显示的系数为(。299, .587, .114)是错误的,因为它们属于一个被称为NTSC YIQ的早已过时的系统,这是几十年前北美的模拟广播系统。虽然在一些YCC编码规范中仍可用于向后兼容,但不应在sRGB上下文中使用它们。

sRGB和Rec.709 (HDTV)的系数为:

红色:0.2126 绿色:0.7152 蓝色:0.0722

其他颜色空间如Rec2020或AdobeRGB使用不同的系数,对于给定的颜色空间使用正确的系数是很重要的。

这些系数不能直接应用于8位sRGB编码的图像或颜色数据。编码的数据必须首先线性化,然后应用系数来找到给定像素或颜色的亮度(光值)。

对于sRGB,有一个分段变换,但由于我们只对感知的亮度对比感兴趣,以找到将文本从黑色“翻转”到白色的点,我们可以通过简单的gamma方法走捷径。

安迪的亮度和亮度捷径

将每个sRGB颜色除以255.0,然后提高到2.2的幂,然后乘以系数并将它们相加,得到估计的亮度。

 let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
          Math.pow(sG/255.0,2.2) * 0.7152 +
          Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4

这里,Y是sRGB显示器的相对亮度,在0.0到1.0范围内。但这与感知无关,我们需要进一步的转换来适应我们人类对相对亮度的视觉感知,以及感知到的对比。

40%的翻转

但在此之前,如果你只是寻找一个基本点来将文本从黑色翻转到白色,反之亦然,那么作弊是使用我们刚刚推导出的Y,并使翻转点约为Y = 0.40;。对于大于0.4 Y的颜色,将文本设置为黑色#000,对于大于0.4 Y的颜色,将文本设置为白色#fff。

  let textColor = (Ys < 0.4) ? "#fff" : "#000"; // Low budget down and dirty text flipper.

为什么是40%而不是50%?人类对明暗和对比的感知不是线性的。对于自动照明显示器来说,在大多数典型条件下,0.4 Y恰好是中等对比度。

是的,它是不同的,是的,这是一个过度简化。但如果你要将文本翻转成黑色或白色,简单的答案是有用的。

知觉奖励轮

预测对特定颜色和亮度的感知仍然是一个活跃的研究课题,而不是完全确定的科学。CIELAB或LUV的L* (Lstar)已被用于预测感知亮度,甚至预测感知对比度。然而,L*在非常明确/受控的环境中很好地用于表面颜色,而不适用于自发光显示器。

虽然这不仅取决于显示类型和校准,还取决于你的环境和整个页面内容,如果你从上面取Y,并将它提高到^0.685到^0.75左右,你会发现0.5通常是将文本从白色翻转到黑色的中间点。

  let textColor = (Math.pow(Ys,0.75) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.

使用指数0.685将使文本颜色交换在较深的颜色上,而使用0.8将使文本颜色交换在较浅的颜色上。

空间频率双倍奖励回合

值得注意的是,对比度不仅仅是两种颜色之间的距离。空间频率,换句话说,字体的重量和大小,也是不可忽视的关键因素。

也就是说,你可能会发现当颜色处于中间位置时,你会想要增加字体的大小和重量。

  let textSize = "16px";
  let textWeight = "normal"; 
  let Ls = Math.pow(Ys,0.7);

  if (Ls > 0.33 && Ls < 0.66) {
      textSize = "18px";
      textWeight = "bold";
      }  // scale up fonts for the lower contrast mid luminances.

Hue R U

这超出了本文的范围,但上面我们忽略了色相和色度。色相和色度确实有影响,如Helmholtz Kohlrausch,上面简单的亮度计算并不总是预测饱和色相的强度。

为了预测感知的这些更微妙的方面,需要一个完整的外观模型。亨特,费尔希尔德,伯恩斯是值得一看的几位作家,如果你想坠入人类视觉感知的兔子洞……

出于这个狭窄的目的,我们可以稍微重新加权系数,知道绿色占亮度的大部分,纯蓝色和纯红色应该总是两种颜色中最暗的。使用标准系数往往会发生的情况是,含有大量蓝色或红色的中间颜色可能会在低于理想亮度的情况下变成黑色,而含有大量绿色成分的颜色可能会相反。

也就是说,我发现通过增加中间颜色的字体大小和粗细来解决这个问题是最好的。

把它们放在一起

因此,我们假设您将向这个函数发送一个十六进制字符串,它将返回一个样式字符串,可以发送到特定的HTML元素。

看看CODEPEN,灵感来自Seirios的作品:

CodePen:花式字体翻转

Codepen代码所做的其中一件事是为低对比度的中范围增加文本大小。下面是一个例子:

如果您想尝试其中一些概念,请访问SAPC开发网站https://www.myndex.com/SAPC/,点击“研究模式”提供交互式实验来演示这些概念。

启蒙的术语

亮度:Y(相对)或L(绝对cd/m2),一种光谱加权的光的线性度量。不要和“光度”混淆。 光度:随时间变化的光,在天文学上很有用。 亮度:由CIE定义的L* (Lstar)感知亮度。一些型号有相关的明度J*。

其他回答

请注意,在谷歌闭包库中有一个算法,该算法引用了w3c推荐:http://www.w3.org/TR/AERT#color-contrast。但是,在这个API中,您提供了一个建议颜色列表作为起点。

/**
 * Find the "best" (highest-contrast) of the suggested colors for the prime
 * color. Uses W3C formula for judging readability and visual accessibility:
 * http://www.w3.org/TR/AERT#color-contrast
 * @param {goog.color.Rgb} prime Color represented as a rgb array.
 * @param {Array<goog.color.Rgb>} suggestions Array of colors,
 *     each representing a rgb array.
 * @return {!goog.color.Rgb} Highest-contrast color represented by an array.
 */
goog.color.highContrast = function(prime, suggestions) {
  var suggestionsWithDiff = [];
  for (var i = 0; i < suggestions.length; i++) {
    suggestionsWithDiff.push({
      color: suggestions[i],
      diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
          goog.color.colorDiff_(suggestions[i], prime)
    });
  }
  suggestionsWithDiff.sort(function(a, b) { return b.diff - a.diff; });
  return suggestionsWithDiff[0].color;
};


/**
 * Calculate brightness of a color according to YIQ formula (brightness is Y).
 * More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb Color represented by a rgb array.
 * @return {number} brightness (Y).
 * @private
 */
goog.color.yiqBrightness_ = function(rgb) {
  return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
};


/**
 * Calculate difference in brightness of two colors. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
 * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
 * @return {number} Brightness difference.
 * @private
 */
goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
  return Math.abs(
      goog.color.yiqBrightness_(rgb1) - goog.color.yiqBrightness_(rgb2));
};


/**
 * Calculate color difference between two colors. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
 * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
 * @return {number} Color difference.
 * @private
 */
goog.color.colorDiff_ = function(rgb1, rgb2) {
  return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
      Math.abs(rgb1[2] - rgb2[2]);
};

作为Kotlin / Android扩展:

fun Int.getContrastColor(): Int {
    // Counting the perceptive luminance - human eye favors green color...
    val a = 1 - (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255
    return if (a < 0.5) Color.BLACK else Color.WHITE
}

这是一个非常有用的答案。谢谢!

我想分享一个SCSS版本:

@function is-color-light( $color ) {

  // Get the components of the specified color
  $red: red( $color );
  $green: green( $color );
  $blue: blue( $color );

  // Compute the perceptive luminance, keeping
  // in mind that the human eye favors green.
  $l: 1 - ( 0.299 * $red + 0.587 * $green + 0.114 * $blue ) / 255;
  @return ( $l < 0.5 );

}

现在弄清楚如何使用算法来自动创建菜单链接的悬停颜色。浅标题的悬停颜色较深,反之亦然。

丑陋的Python,如果你不想写它:)

'''
Input a string without hash sign of RGB hex digits to compute
complementary contrasting color such as for fonts
'''
def contrasting_text_color(hex_str):
    (r, g, b) = (hex_str[:2], hex_str[2:4], hex_str[4:])
    return '000' if 1 - (int(r, 16) * 0.299 + int(g, 16) * 0.587 + int(b, 16) * 0.114) / 255 < 0.5 else 'fff'

基于Gacek的回答,在用WAVE浏览器扩展分析了@WebSeed的例子后,我提出了以下版本,它根据对比度(在W3C的Web内容可访问性指南(WCAG) 2.1中定义)而不是亮度来选择黑色或白色文本。

这是代码(javascript):

// As defined in WCAG 2.1
var relativeLuminance = function (R8bit, G8bit, B8bit) {
  var RsRGB = R8bit / 255.0;
  var GsRGB = G8bit / 255.0;
  var BsRGB = B8bit / 255.0;

  var R = (RsRGB <= 0.03928) ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
  var G = (GsRGB <= 0.03928) ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
  var B = (BsRGB <= 0.03928) ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);

  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};

var blackContrast = function(r, g, b) {
  var L = relativeLuminance(r, g, b);
  return (L + 0.05) / 0.05;
};

var whiteContrast = function(r, g, b) {
  var L = relativeLuminance(r, g, b);
  return 1.05 / (L + 0.05);
};

// If both options satisfy AAA criterion (at least 7:1 contrast), use preference
// else, use higher contrast (white breaks tie)
var chooseFGcolor = function(r, g, b, prefer = 'white') {
  var Cb = blackContrast(r, g, b);
  var Cw = whiteContrast(r, g, b);
  if(Cb >= 7.0 && Cw >= 7.0) return prefer;
  else return (Cb > Cw) ? 'black' : 'white';
};

在我的@WebSeed的代码依赖的分支中可以找到一个工作示例,它在WAVE中产生零低对比度错误。