对于我正在开发的 mod,我想合并玩家的主题颜色并使用它们来生成 UI 元素。但是,我遇到了一个问题,即并非所有颜色主题都具有提供 良好对比度 的颜色,如 Web 内容可访问性指南 (WCAG) 2.1 的 1.4.3 对比度(最小值) 中所述。 .
我目前可以通过以下方式检查对比度:
float RelativeLuminance(Color color)
{
float ColorPartValue(float part)
{
return part <= 0.03928f ? part / 12.92f : Mathf.Pow((part + 0.055f) / 1.055f, 2.4f);
}
var r = ColorPartValue(color.r);
var g = ColorPartValue(color.g);
var b = ColorPartValue(color.b);
var l = 0.2126f * r + 0.7152f * g + 0.0722f * b;
return l;
}
private float ColorContrast(Color a, Color b)
{
float result = 0f;
var La = RelativeLuminance(a) + 0.05f;
var Lb = RelativeLuminance(b) + 0.05f;
result = Mathf.Max(La, Lb) / Mathf.Min(La, Lb);
return result;
}
我使用找到的颜色对比度来确定初始文本颜色是否足够好。
public Color GetContrastingColors(Color backgroundColor, Color textColor)
{
Color contrastColor;
// See if we have good enough contrast already
if (!(ColorContrast(backgroundColor, textColor) < 4.5f))
{
return textColor;
}
Color.RGBToHSV(textColor, out var textH, out var textS, out var textV);
Color.RGBToHSV(backgroundColor, out var bgH, out var bgS, out var bgV);
// Modify textV by some value to provide enough contrast.
contrastColor = Color.HSVToRGB(textH, textS, textV);
return contrastColor;
}
但是,我不确定如何调整颜色,使文本颜色变亮(或变暗)足以达到 4.5:1 的对比度。最初,我正在考虑对亮度和对比度方程进行代数运算,直到
sRGB
值乘以某个值 X。不过我记得 HSV,调整颜色的亮度对我来说似乎要简单得多。问题是,我不确定如何比较 2 个 HSV 颜色的对比度,更不用说使用它们的值来操纵颜色的亮度以达到所需的对比度。
我目前的想法是做一些像这样的愚蠢的事情:
float targetL;
bool brighter = false;
var backL = RelativeLuminance(backgroundColor);
var textL = RelativeLuminance(textColor);
var ratio = 4.5f;
// Try to go in the direction of brightness originally.
if (textL > backL)
{
targetL = ((backL + 0.05f) * ratio) - 0.05f;
brighter = true;
if (targetL > 1f)
{
targetL = ((backL + 0.05f) / ratio) - 0.05f;
brighter = false;
}
}
else
{
targetL = ((backL + 0.05f) / ratio) - 0.05f;
if (targetL > 0f)
{
targetL = ((backL + 0.05f) * ratio) - 0.05f;
brighter = true;
}
}
Color adjustedColor = textColor;
while ((!brighter && textL > targetL) || (brighter && textL < targetL))
{
Color.RGBToHSV(adjustedColor, out var textH, out var textS, out var textV);
textV += brighter ? 0.01f : -0.01f;
adjustedColor = Color.HSVToRGB(textH, textS, textV);
textL = RelativeLuminance(adjustedColor);
}
contrastColor = adjustedColor;
但这并不是真正有效,那么我如何操纵文本颜色,使其“保持不变”但提供足够的对比度?
编辑:
为了给我想要做的事情提供更多背景信息,想象我有以下 4 种颜色作为玩家的主题。
用HTML代码来说,那就是:
#32263d | #3d1c70 |
#7347b6 | #320d68 |
在为他们创建 UI 时,我想合并他们主题中的 2 种颜色。然而,并非所有这些都很容易区分,您可以在这里看到本例中的各种对比:
现在每个主题都包含较深和较浅的颜色,就像本示例中的中心两行一样,但也像本示例一样,它们的对比度可能并不总是可供最终用户阅读。按照示例进行操作,在本例中,我们将使用
#32263d
和 #7347b6
来构建我们的 UI。
虽然我可以尝试随机创建类似的紫色阴影,但我想使其尽可能接近原始颜色并使其变亮。我们可以在这里看到它在不同光线水平下的外观:
如果我们将
#7347b6
设置为 #a163ff
处的最大亮度,我们现在得到以下对:
虽然比以前好,但对比度仍然只有 3.88 : 1。所以现在我想缩小
#32263d
的亮度。如果我们将其减少到 #251B2D
,我们最终会得到这样的结果:
两种新颜色的颜色对比度为 4.51 : 1。
现在,我可以手动浏览每个主题,但考虑到主题的数量,我更愿意编写一个算法来动态生成更新的颜色。
查看我的答案 调整给定的颜色对以遵守 ePub 的 W3C 可访问性标准
您可以跳过我谈论对比度公式的部分,因为您已经有了,但我谈论如何调整颜色以获得更好的对比度。
如果我要根据之前的答案实际编写我的建议,我会更有效率,而不是从每个 RGB 分量中添加或减去 1 并重新计算亮度,我可能会添加/减去 10 并重新计算。 如果对比度不够,请再做 10 次。 一旦获得足够的对比度,我就可以向相反的方向重新调整这些值,也许每次调整 2,直到接近 4.5 而不会下降。
我最终在我的代码中使用了循环。虽然 slugolicious 的答案很接近我想要的,但我发现将所有 RGB 分量调整相同的量并不是我想要的,因为这实际上会影响色调,所以我最终使用 HSV 代替。
public Color[] GetContrastingColors(Color backgroundColor, Color textColor, float ratio)
{
Color[] colors = new Color[2];
var backL = RelativeLuminance(backgroundColor);
var textL = RelativeLuminance(textColor);
if (textL > backL)
{
colors[0] = textColor;
colors[1] = backgroundColor;
}
else
{
colors[1] = textColor;
colors[0] = backgroundColor;
}
// See if we have good enough contrast already
if (!(ColorContrast(backgroundColor, textColor) < ratio))
{
return colors;
}
Color.RGBToHSV(colors[0], out var lightH, out var lightS, out var lightV);
Color.RGBToHSV(colors[1], out var darkH, out var darkS, out var darkV);
// If the darkest color can be darkened enough to have enough contrast after brightening the color.
if (ColorContrast(Color.HSVToRGB(darkH, darkS, 0f), Color.HSVToRGB(lightH, lightS, 1f)) >= ratio)
{
var lightDiff = 1f - lightV;
var darkDiff = darkV;
var steps = new float[] { 0.12f, 0.1f, 0.08f, 0.05f, 0.04f, 0.03f, 0.02f, 0.01f, 0.005f };
var step = 0;
var lightRatio = (lightDiff / (lightDiff + darkDiff));
var darkRatio = (darkDiff / (lightDiff + darkDiff));
while (ColorContrast(Color.HSVToRGB(lightH, lightS, lightV), Color.HSVToRGB(darkH, darkS, darkV)) < ratio)
{
while (ColorContrast(Color.HSVToRGB(lightH, lightS, lightV + lightRatio * steps[step]), Color.HSVToRGB(darkH, darkS, darkV - darkRatio * steps[step])) > ratio && step < steps.Length - 1)
{
step++;
}
lightV += lightRatio * steps[step];
darkV -= darkRatio * steps[step];
}
colors[0] = Color.HSVToRGB(lightH, lightS, lightV);
colors[1] = Color.HSVToRGB(darkH, darkS, darkV);
}
// Fall back to using white.
else
{
colors[0] = Color.white;
while (ColorContrast(Color.white, Color.HSVToRGB(darkH, darkS, darkV)) < ratio)
{
darkV -= 0.01f;
}
colors[1] = Color.HSVToRGB(darkH, darkS, darkV);
}
return colors;
}
我也不喜欢使用HSV,所以我将willuwontu的算法变成了通用算法,并添加了保留backgroundColor和textColor的功能。我还固定了步长:
static double stepsize = 0.01;
public static Color[] GetContrastingColors<ColorModel>(Color backgroundColor, Color textColor, double ratio, ChangingMode changingMode) where ColorModel : IContrastAdjustable<ColorModel>
{
Color[] colors = new Color[2];
colors[0] = backgroundColor;
colors[1] = textColor;
Color[] lightAndDarkAsColors = new Color[2];
var backL = RelativeLuminance(backgroundColor);
var textL = RelativeLuminance(textColor);
bool preferChangingLight = false;
bool darkFixed, lightFixed;
bool textIsLight = textL > backL;
// See if we have good enough contrast already
if (!(ColorContrast(backgroundColor, textColor) < ratio))
{
return colors;
}
ColorModel light, dark;
ColorModel verydark, verylight;
//decide which colors we change
{
double rw, rb;
if (changingMode == ChangingMode.ForegroundFixed || changingMode == ChangingMode.BackgroundFixed)
{
//if one color is fixed, high contrast can be achieve by increasing or by decreasing the Luminosity of the other Color Jochla
//so we test if we can achieve a good result while keeping the dark color dark and the light color light
lightAndDarkAsColors[0] = textIsLight ? textColor : backgroundColor;
lightAndDarkAsColors[1] = textIsLight ? backgroundColor : textColor;
Color colorFixed = changingMode == ChangingMode.ForegroundFixed ? textColor : backgroundColor;
Color colorChangeable = changingMode == ChangingMode.ForegroundFixed ? backgroundColor : textColor;
rw = ColorContrast(Color.White, colorFixed);
rb = ColorContrast(colorFixed, Color.Black);
preferChangingLight = rw > rb;
if ((!(preferChangingLight ^ (changingMode == ChangingMode.ForegroundFixed))) == textIsLight)
{
lightFixed = !(changingMode == ChangingMode.ForegroundFixed ^ textIsLight); //^Xor
darkFixed = !(changingMode == ChangingMode.BackgroundFixed ^ textIsLight); //^Xor
light = ColorModel.FromColor(lightAndDarkAsColors[0]);
dark = ColorModel.FromColor(lightAndDarkAsColors[1]);
verydark = dark.Clone();
verylight = light.Clone();
if (!lightFixed)
{
verylight.Luminosity = ColorModel.MaxLuminosity;
}
if (!darkFixed)
{
verydark.Luminosity = ColorModel.MinLuminosity;
}
if (ColorContrast(ColorModel.ToColor(verydark), ColorModel.ToColor(verylight)) >= ratio)
{
var lightDiff = 1 - light.Luminosity;
var darkDiff = dark.Luminosity;
var lightRatio = (lightDiff / (lightDiff + darkDiff));
var darkRatio = (darkDiff / (lightDiff + darkDiff));
while (ColorContrast(ColorModel.ToColor(light), ColorModel.ToColor(dark)) < ratio)
{
if (!lightFixed)
{
light.Luminosity += lightRatio * stepsize;
}
if (!darkFixed)
{
dark.Luminosity -= darkRatio * stepsize;
}
}
lightAndDarkAsColors[0] = ColorModel.ToColor(light);
lightAndDarkAsColors[1] = ColorModel.ToColor(dark);
colors[0] = textIsLight ? lightAndDarkAsColors[1] : lightAndDarkAsColors[0];
colors[1] = textIsLight ? lightAndDarkAsColors[0] : lightAndDarkAsColors[1];
return colors;
}
}
textIsLight = changingMode == ChangingMode.BackgroundFixed ? preferChangingLight : !preferChangingLight;
lightAndDarkAsColors[0] = preferChangingLight ? colorChangeable : colorFixed;
lightAndDarkAsColors[1] = preferChangingLight ? colorFixed : colorChangeable;
darkFixed = preferChangingLight;
lightFixed = !preferChangingLight;
}else
{
lightAndDarkAsColors[0] = textIsLight ? textColor : backgroundColor;
lightAndDarkAsColors[1] = textIsLight ? backgroundColor : textColor;
darkFixed = false;
lightFixed = false;
}
}
light = ColorModel.FromColor(lightAndDarkAsColors[0]);
dark = ColorModel.FromColor(lightAndDarkAsColors[1]);
verydark = dark.Clone();
verylight = light.Clone();
if (!lightFixed)
{
verylight.Luminosity = ColorModel.MaxLuminosity;
}
if (!darkFixed)
{
verydark.Luminosity = ColorModel.MinLuminosity;
}
// If the darkest and the lightest we can achieve is enough
if (ColorContrast(ColorModel.ToColor(verydark), ColorModel.ToColor(verylight)) >= ratio)
{
var lightDiff = 1 - light.Luminosity;
var darkDiff = dark.Luminosity;
var lightRatio = (lightDiff / (lightDiff + darkDiff));
var darkRatio = (darkDiff / (lightDiff + darkDiff));
while (ColorContrast(ColorModel.ToColor(light), ColorModel.ToColor(dark)) < ratio)
{
if (!lightFixed)
{
light.Luminosity += lightRatio * stepsize;
}
if (!darkFixed)
{
dark.Luminosity -= darkRatio * stepsize;
}
}
lightAndDarkAsColors[0] = ColorModel.ToColor(light);
lightAndDarkAsColors[1] = ColorModel.ToColor(dark);
}
else
{
double rw, rb;
switch (changingMode)
{
//if we prefer white we use white l
case ChangingMode.PreferWhite:
lightAndDarkAsColors[0] = Color.White;
//Can we achieve any better than black and white?
if (ColorContrast(Color.White, ColorModel.ToColor(verydark)) < ratio)
{
lightAndDarkAsColors[1] = Color.Black;
}else
{
while (ColorContrast(Color.White, ColorModel.ToColor(dark)) < ratio)
{
dark.Luminosity -= 0.01;
}
lightAndDarkAsColors[1] = ColorModel.ToColor(dark);
}
break;
//if we prefer black we use black
case ChangingMode.PreferBlack:
lightAndDarkAsColors[1] = Color.Black;
//Can we achieve any better than black and white?
if (ColorContrast(ColorModel.ToColor(verylight), Color.Black) < ratio)
{
lightAndDarkAsColors[0] = Color.White;
}else
{
while (ColorContrast(ColorModel.ToColor(light), Color.Black) < ratio)
{
light.Luminosity += 0.01;
}
lightAndDarkAsColors[0] = ColorModel.ToColor(light);
}
break;
case ChangingMode.ForegroundFixed:
colors[0] = preferChangingLight ? Color.White : Color.Black;
colors[1] = textColor;
return colors;
case ChangingMode.BackgroundFixed:
colors[0] = backgroundColor;
colors[1] = preferChangingLight ? Color.White : Color.Black;
return colors;
}
}
colors[0] = textIsLight ? lightAndDarkAsColors[1] : lightAndDarkAsColors[0];
colors[1] = textIsLight ? lightAndDarkAsColors[0] : lightAndDarkAsColors[1];
return colors;
}
public enum ChangingMode
{
PreferWhite,
PreferBlack,
BackgroundFixed,
ForegroundFixed
}