UI: Improve brand color adjustment (#6351)

Uses a more finegrained method of deciding whether or not to adjust the brand color: Instead of simply using the brightness of the color, we calculate the contrast ratio and adjust the color by increasing the contrast until it is sufficient.

The thresholds also try to preserve the brand color in its original form, so that the color only gets adjusted if the contrast is very low.
This commit is contained in:
d11n 2024-11-08 09:01:28 +01:00 committed by GitHub
parent 12681eda36
commit 540fb6c9f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 18 deletions

View File

@ -110,5 +110,10 @@ namespace BTCPayServer
{
return ColorTranslator.FromHtml(html);
}
public string ToHtml(Color color)
{
return ColorTranslator.ToHtml(color);
}
}
}

View File

@ -0,0 +1,121 @@
using System;
using System.Drawing;
namespace BTCPayServer;
public static class ColorExtensions
{
public static double GetLuminance(this Color color)
{
var r = color.R / 255.0;
var g = color.G / 255.0;
var b = color.B / 255.0;
r = (r <= 0.03928) ? r / 12.92 : Math.Pow((r + 0.055) / 1.055, 2.4);
g = (g <= 0.03928) ? g / 12.92 : Math.Pow((g + 0.055) / 1.055, 2.4);
b = (b <= 0.03928) ? b / 12.92 : Math.Pow((b + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
public static double GetContrastRatio(this Color color1, Color color2)
{
var luminance1 = color1.GetLuminance();
var luminance2 = color2.GetLuminance();
var lighter = Math.Max(luminance1, luminance2);
var darker = Math.Min(luminance1, luminance2);
return (lighter + 0.05) / (darker + 0.05);
}
public static Color GetAdjustedForegroundForBackground(this Color foregroundColor, Color backgroundColor, double lightnessIncrement = 0.01, double threshold = 4.5)
{
var contrastRatio = foregroundColor.GetContrastRatio(backgroundColor);
if (contrastRatio >= threshold) return foregroundColor;
RgbToHsl(foregroundColor, out double hue, out double saturation, out double lightness);
for (int i = 0; i < 100; i++)
{
lightness += lightnessIncrement;
lightness = Math.Clamp(lightness, 0, 1);
Color adjustedColor = HslToRgb(hue, saturation, lightness);
contrastRatio = adjustedColor.GetContrastRatio(backgroundColor);
if (contrastRatio >= threshold) return adjustedColor;
}
return foregroundColor;
}
private static void RgbToHsl(Color color, out double hue, out double saturation, out double lightness)
{
double r = color.R / 255.0;
double g = color.G / 255.0;
double b = color.B / 255.0;
double max = Math.Max(r, Math.Max(g, b));
double min = Math.Min(r, Math.Min(g, b));
lightness = (max + min) / 2.0;
if (max == min)
{
hue = saturation = 0;
}
else
{
double delta = max - min;
saturation = (lightness > 0.5) ? delta / (2.0 - max - min) : delta / (max + min);
if (max == r)
{
hue = (g - b) / delta + (g < b ? 6 : 0);
}
else if (max == g)
{
hue = (b - r) / delta + 2;
}
else
{
hue = (r - g) / delta + 4;
}
hue /= 6;
}
}
private static Color HslToRgb(double hue, double saturation, double lightness)
{
double r, g, b;
if (saturation == 0)
{
r = g = b = lightness;
}
else
{
Func<double, double, double, double> hueToRgb = (p, q, t) =>
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6.0) return p + (q - p) * 6 * t;
if (t < 1 / 2.0) return q;
if (t < 2 / 3.0) return p + (q - p) * (2 / 3.0 - t) * 6;
return p;
};
double q = (lightness < 0.5) ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
double p = 2 * lightness - q;
r = hueToRgb(p, q, hue + 1 / 3.0);
g = hueToRgb(p, q, hue);
b = hueToRgb(p, q, hue - 1 / 3.0);
}
return Color.FromArgb(
(int)Math.Round(r * 255),
(int)Math.Round(g * 255),
(int)Math.Round(b * 255));
}
}

View File

@ -1,14 +1,17 @@
@using BTCPayServer.Services
@using System.Drawing
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model StoreBrandingViewModel
@inject ThemeSettings Theme
@if (!string.IsNullOrEmpty(Model.BrandColor))
{
var hasCustomeTheme = Theme.CustomTheme && Theme.CustomThemeCssUrl is not null;
const double thresholdLight = 1.5;
const double thresholdDark = 2.5;
var brand = Model.BrandColor;
var brandColor = ColorPalette.Default.FromHtml(brand);
var brandRgbValues = $"{brandColor.R}, {brandColor.G}, {brandColor.B}";
var brightness = brandColor.GetBrightness();
var bgLight = ColorPalette.Default.FromHtml("#F8F9FA");
var bgDark = ColorPalette.Default.FromHtml("#0d1117");
var brandColorAdjustedForLight = brandColor.GetAdjustedForegroundForBackground(bgLight, -.02, thresholdLight);
var brandColorAdjustedForDark = brandColor.GetAdjustedForegroundForBackground(bgDark, .02, thresholdDark);
var accent = ColorPalette.Default.AdjustBrightness(brand, (float)-.15);
var accentColor = ColorPalette.Default.FromHtml(accent);
var accentRgbValues = $"{accentColor.R}, {accentColor.G}, {accentColor.B}";
@ -28,13 +31,12 @@
--btcpay-primary-text-active: @complementVar;
}
</style>
@if (brightness > .5 || (Theme.CustomThemeExtension == ThemeExtension.Dark && brightness < .5))
@if (brandColorAdjustedForLight != brandColor)
{
var brandAdjusted = ColorPalette.Default.AdjustBrightness(brand, (float)(.35-brightness));
var brandColorAdjusted = ColorPalette.Default.FromHtml(brandAdjusted);
var brandRgbValuesAdjusted = $"{brandColorAdjusted.R}, {brandColorAdjusted.G}, {brandColorAdjusted.B}";
var accentAdjusted = ColorPalette.Default.AdjustBrightness(brandAdjusted, (float)-.15);
var accentColorAdjusted = ColorPalette.Default.FromHtml(accentAdjusted);
var brandAdjusted = ColorPalette.Default.ToHtml(brandColorAdjustedForLight);
var brandRgbValuesAdjusted = $"{brandColorAdjustedForLight.R}, {brandColorAdjustedForLight.G}, {brandColorAdjustedForLight.B}";
var accentColorAdjusted = ColorPalette.Default.AdjustBrightness(brandColorAdjustedForLight, (float)-.15);
var accentAdjusted = ColorPalette.Default.ToHtml(accentColorAdjusted);
var accentRgbValuesAdjusted = $"{accentColorAdjusted.R}, {accentColorAdjusted.G}, {accentColorAdjusted.B}";
var complementAdjusted = ColorPalette.Default.TextColor(brandAdjusted);
var complementVarAdjusted = $"var(--btcpay-{(complementAdjusted == "black" ? "black" : "white")})";
@ -68,16 +70,15 @@
}
</style>
}
@if (brightness < .5 && (!hasCustomeTheme || Theme.CustomThemeExtension == ThemeExtension.Dark))
@if (brandColorAdjustedForDark != brandColor)
{
var brandAdjusted = ColorPalette.Default.AdjustBrightness(brand, (float)(.5-brightness));
var brandColorAdjusted = ColorPalette.Default.FromHtml(brandAdjusted);
var brandRgbValuesAdjusted = $"{brandColorAdjusted.R}, {brandColorAdjusted.G}, {brandColorAdjusted.B}";
var accentAdjusted = ColorPalette.Default.AdjustBrightness(brandAdjusted, (float).15);
var accentColorAdjusted = ColorPalette.Default.FromHtml(accentAdjusted);
var brandAdjusted = ColorPalette.Default.ToHtml(brandColorAdjustedForDark);
var brandRgbValuesAdjusted = $"{brandColorAdjustedForDark.R}, {brandColorAdjustedForDark.G}, {brandColorAdjustedForDark.B}";
var accentColorAdjusted = ColorPalette.Default.AdjustBrightness(brandColorAdjustedForDark, (float).15);
var accentAdjusted = ColorPalette.Default.ToHtml(accentColorAdjusted);
var accentRgbValuesAdjusted = $"{accentColorAdjusted.R}, {accentColorAdjusted.G}, {accentColorAdjusted.B}";
var complementAdjusted = ColorPalette.Default.TextColor(brandAdjusted);
var complementVarAdjusted = $"var(--btcpay-{(complementAdjusted == "black" ? "black" : "white")})";
var complementAdjusted = ColorPalette.Default.TextColor(brandColorAdjustedForDark);
var complementVarAdjusted = $"var(--btcpay-{(complementAdjusted == Color.Black ? "black" : "white")})";
<style>
:root[data-theme='dark'],
:root[data-btcpay-theme='dark'] {