diff --git a/BTCPayServer/ColorPalette.cs b/BTCPayServer/ColorPalette.cs index 462a4350b..631c54987 100644 --- a/BTCPayServer/ColorPalette.cs +++ b/BTCPayServer/ColorPalette.cs @@ -110,5 +110,10 @@ namespace BTCPayServer { return ColorTranslator.FromHtml(html); } + + public string ToHtml(Color color) + { + return ColorTranslator.ToHtml(color); + } } } diff --git a/BTCPayServer/Extensions/ColorExtensions.cs b/BTCPayServer/Extensions/ColorExtensions.cs new file mode 100644 index 000000000..71b43b275 --- /dev/null +++ b/BTCPayServer/Extensions/ColorExtensions.cs @@ -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 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)); + } +} diff --git a/BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml b/BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml index 2c4f5c570..fbd860e35 100644 --- a/BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml +++ b/BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml @@ -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; } - @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 @@ } } - @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")})";