根据背景颜色确定字体颜色

302

假设有一个系统(例如网站),它允许用户自定义某些部分的背景颜色,但不允许自定义字体颜色(为了将选项数量最小化),是否有一种编程方式可以确定是否需要“浅色”或“深色”字体颜色?

我相信存在某些算法,但我不了解足够的颜色、亮度等知识来独立解决此问题。

23个回答

523

我遇到了类似的问题。我需要找到一种选择对比字体颜色以显示颜色标度/热力图上的文本标签的好方法。它必须是通用的方法,并且生成的颜色必须是“好看的”,这意味着简单地生成补色不是一个好的解决方案 - 有时会生成奇怪、非常强烈的颜色,很难观看和阅读。

经过长时间的测试和尝试解决这个问题,我发现最好的解决方案是为“深色”颜色选择白色字体,为“浅色”颜色选择黑色字体。

这里是我在C#中使用的函数示例:

Color ContrastColor(Color color)
{
    int d = 0;
    
    // Counting the perceptive luminance - human eye favors green color...      
    double luminance = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;
    
    if (luminance > 0.5)
       d = 0; // bright colors - black font
    else
       d = 255; // dark colors - white font
                
    return  Color.FromArgb(d, d, d);
}

这经过了对多种不同的配色方案进行测试(彩虹、灰度、热、冰等),是我发现的唯一“通用”的方法。

编辑
把计算a的公式改为“感知亮度” - 看起来更好!已在我的软件中实现,效果很棒。

编辑2 @WebSeed提供了一个这个算法的工作示例:http://codepen.io/WebSeed/full/pvgqEq/


18
也许这并不重要,但你可能需要一个更好的函数来计算亮度。https://dev59.com/QHRB5IYBdhLWcg3wiHpl - Josh Lee
你的感知亮度权重来自哪里? - Mat
从这个答案开始:https://dev59.com/QHRB5IYBdhLWcg3wiHpl - Gacek
@Gacek我的屏幕已经校准,背景色是(135, 135, 135),字体颜色在这里肯定更好,就像这个例子所示:https://www.htmlcsscolor.com/hex/878787是否有一种方法可以调整算法,使较暗的色调更倾向于白色的亮度感知,就像(135, 135, 135)的情况? - Pavan
1
这太棒了。我也遇到了求逆的问题,这解决了我的问题。 - tjans
显示剩余6条评论

33

基于Gacek的答案,但直接返回颜色常量(以下是其他修改):

public Color ContrastColor(Color iColor)
{
  // Calculate the perceptive luminance (aka luma) - human eye favors green color... 
  double luma = ((0.299 * iColor.R) + (0.587 * iColor.G) + (0.114 * iColor.B)) / 255;

  // Return black for bright colors, white for dark colors
  return luma > 0.5 ? Color.Black : Color.White;
}

注: 我删除了亮度值的倒置,使得明亮的颜色具有更高的值,这对我来说更自然,并且也是“默认”的计算方法。(参见此链接)
编辑:原回答中已采用此方法。

我使用了与此处的Gacek相同的常量,因为它们对我非常有效。


你还可以使用以下签名将其实现为扩展方法

public static Color ContrastColor(this Color iColor)

然后,您可以通过以下方式轻松调用它
foregroundColor = backgroundColor.ContrastColor()


14

谢谢@Gacek。这是适用于Android的版本:

@ColorInt
public static int getContrastColor(@ColorInt int color) {
    // Counting the perceptive luminance - human eye favors green color...
    double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;

    int d;
    if (a < 0.5) {
        d = 0; // bright colors - black font
    } else {
        d = 255; // dark colors - white font
    }

    return Color.rgb(d, d, d);
}

并且有一个改进后(更短)的版本:

@ColorInt
public static int getContrastColor(@ColorInt int color) {
    // Counting the perceptive luminance - human eye favors green color...
    double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
    return a < 0.5 ? Color.BLACK : Color.WHITE;
}

如果您应用@Marcus Mangelsdorf的更改(摆脱1-...部分并将a重命名为luminance),那么它将变得更短,更易于阅读。 - Ridcully
使用第一个,你可以捕获alpha。 - Brill Pappin

14

我对 Gacek 回答的 Swift 实现:

func contrastColor(color: UIColor) -> UIColor {
    var d = CGFloat(0)

    var r = CGFloat(0)
    var g = CGFloat(0)
    var b = CGFloat(0)
    var a = CGFloat(0)

    color.getRed(&r, green: &g, blue: &b, alpha: &a)

    // Counting the perceptive luminance - human eye favors green color...
    let luminance = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b))

    if luminance < 0.5 {
        d = CGFloat(0) // bright colors - black font
    } else {
        d = CGFloat(1) // dark colors - white font
    }

    return UIColor( red: d, green: d, blue: d, alpha: a)
}

在 Swift 中,由于 r/g/b 是 CGFloat 类型,因此您不需要使用 "/255" 来计算亮度: let luminance = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b)) - SuperDuperTango

12
JavaScript [ES2015]
const hexToLuma = (colour) => {
    const hex   = colour.replace(/#/, '');
    const r     = parseInt(hex.substr(0, 2), 16);
    const g     = parseInt(hex.substr(2, 2), 16);
    const b     = parseInt(hex.substr(4, 2), 16);

    return [
        0.299 * r,
        0.587 * g,
        0.114 * b
    ].reduce((a, b) => a + b) / 255;
};

11

简短回答:

计算给定颜色的亮度(Y),并根据预先确定的中等对比度数值将文本翻转为黑色或白色。对于典型的sRGB显示器,当Y < 0.36(即36%)时翻转为白色。

编辑添加:这个问题经常被问到,我在https://github.com/Myndex/max-contrast上设置了一个准备好的JS库。

就是这样:

    // Simple: send it sRGB values 0-255.
   // If Ys is > 0.342 it returns 'black' otherwise returns 'white'
  // See  https://github.com/Myndex/max-contrast 


function maxContrast (Rs = 164, Gs = 164, Bs = 164) {
  
  const flipYs = 0.342; // based on APCA™ 0.98G middle contrast BG

  const trc = 2.4, Rco = 0.2126729, Gco = 0.7151522, Bco = 0.0721750; // 0.98G

  let Ys = (Rs/255.0)**trc*Rco + (Gs/255.0)**trc*Gco + (Bs/255.0)**trc*Bco; 

  return Ys < flipYs ? 'white' : 'black'
}

更长的回答

毫不奇怪,几乎每个回答都存在一些误解,和/或引用了错误的系数。唯一一个接近正确的答案是Seirios的回答,尽管它依赖于已知不正确的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,有一个分段变换,但由于我们只关心感知亮度对比度,以找到将文本从黑色翻转为白色的点,我们可以通过简单的伽马方法来简化操作。

Andy的亮度和明亮度的捷径

将每个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的范围内。然而,这与感知无关,我们需要进一步的转换来适应我们人类对相对亮度和感知对比度的视觉感知。

36%翻转

但在我们继续之前,如果你只想要一个基本的将文本从黑色翻转为白色或反之的方法,那么诀窍就是使用我们刚刚得出的Y,并将翻转点设置为Y = 0.36;。所以对于大于0.36 Y的颜色,将文本设置为黑色#000,对于比0.36 Y更暗的颜色,将文本设置为白色#fff

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

为什么是36%,而不是50%?我们对亮度/暗度和对比度的人类感知并不是线性的。对于自发光显示器来说,恰好在大多数典型条件下,0.36 Y是中等对比度。
是的,这个数值会有所变化,而且这只是一个过于简化的答案。但是,如果你要将文本翻转成黑色或白色,这个简单的答案是有用的。(最佳数值可能在34%到42%之间变化,取决于环境条件和刺激空间特征)。
感知奖励环节
预测给定颜色和亮度的感知仍然是一个活跃研究的课题,尚未完全解决。CIELAB或LUV的L*(Lstar)已被用来预测感知亮度,甚至预测感知对比度。然而,L*在非常明确/受控的环境中适用于表面颜色,但对于自发光显示器效果不佳。
虽然这取决于显示器类型和校准,以及您的环境和整个页面内容,但如果您将上述的Y值提高约^0.66到^0.7,您会发现0.5是将文本从白色翻转为黑色的中间点。
  let textColor = (Math.pow(Ys,0.678) < 0.5) ? "#fff" : "#000"; // perceptual text flipper.

使用指数0.6会使文本颜色变为较暗的颜色,而使用0.8会使文本变为较亮的颜色。
空间频率双重奖励回合
值得注意的是,对比度不仅仅是两种颜色之间的距离。空间频率,也就是字体的粗细和大小,也是不可忽视的关键因素。
也就是说,当颜色处于中间范围时,您可能会发现您希望增加字体的大小和/或粗细。
  let textSize = "16px";
  let textWeight = "normal"; 
  let Ls = Math.pow(Ys,0.678);

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

你好吗

在这篇文章中,我们不会深入探讨色调和色度,因为这超出了本文的范围。然而,色调和色度确实会产生影响,比如Helmholtz Kohlrausch,而上面简单的亮度计算并不能总是准确预测饱和色调的强度。

要预测这些更微妙的感知方面,需要一个完整的外观模型。R. Hunt, M. Fairshild, E. Burns是一些值得研究的作者,如果你想深入了解人类视觉感知的话...

对于这个狭窄的目的,我们可以稍微重新调整系数的权重,知道绿色占据了大部分亮度,纯蓝色和纯红色应该始终是两种颜色中最暗的。使用标准系数时,中间带有大量蓝色或红色的颜色可能会在较低的亮度下变成黑色,而具有高绿色成分的颜色可能会相反。

话虽如此,我发现最好的解决方法是增加中间颜色的字体大小和粗细。

将所有内容整合在一起

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

看看这个受Seirios启发的CODEPEN。Codepen的代码之一是为低对比度中间范围增加文本大小。

更新:全新的花式字体翻转GitHub仓库

一个更完整的演示。

样本

这是一个样本,翻转点是Ys 42,太高了: 翻转点过高的花式字体翻转样本

这是一个样本,翻转点是Ys 36: 翻转点为Ys 36的花式字体翻转样本

如果你想玩弄一些基本概念,可以查看SAPC development网站,点击“研究模式”可以进行交互式实验,以演示这些概念。

启蒙术语

  • 亮度: Y(相对)或 L(绝对cd/m2)是光的光谱加权但线性度量。不要与“亮度”混淆。

  • 亮度:随时间变化的光,对天文学有用。

  • 明度: L*(Lstar)是由CIE定义的感知明度。一些模型有一个相关的明度J*


6

如果你不想写Python,可以使用丑陋的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'

6

感谢这篇文章。

对于可能感兴趣的读者,以下是Delphi中该函数的示例:

function GetContrastColor(ABGColor: TColor): TColor;
var
  ADouble: Double;
  R, G, B: Byte;
begin
  if ABGColor <= 0 then
  begin
    Result := clWhite;
    Exit; // *** EXIT RIGHT HERE ***
  end;

  if ABGColor = clWhite then
  begin
    Result := clBlack;
    Exit; // *** EXIT RIGHT HERE ***
  end;

  // Get RGB from Color
  R := GetRValue(ABGColor);
  G := GetGValue(ABGColor);
  B := GetBValue(ABGColor);

  // Counting the perceptive luminance - human eye favors green color...
  ADouble := 1 - (0.299 * R + 0.587 * G + 0.114 * B) / 255;

  if (ADouble < 0.5) then
    Result := clBlack  // bright colors - black font
  else
    Result := clWhite;  // dark colors - white font
end;

倒数第四行有一个小错误。Result:= clBlack 后面不应该有分号; - Andy k

5

这是非常有帮助的答案。谢谢!

我想分享一个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 );

}

现在需要找出如何使用算法自动生成菜单链接的悬停颜色。浅色标题会得到更深的悬停效果,反之亦然。

4

Flutter 实现

Color contrastColor(Color color) {
  if (color == Colors.transparent || color.alpha < 50) {
    return Colors.black;
  }
  double luminance = (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue) / 255;
  return luminance > 0.5 ? Colors.black : Colors.white;
}

我所做的只是在前面加上了“static”。谢谢! - Pete Alvin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接