diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c9799a38ec..3558dcf94c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap { "JsonUtils": NewJsonUtils, // ----------------------------------------------------------------- - // svg / avatar / icon + // svg / avatar / icon / color "svg": svg.RenderHTML, "EntryIcon": base.EntryIcon, "MigrationIcon": MigrationIcon, "ActionIcon": ActionIcon, - - "SortArrow": SortArrow, + "SortArrow": SortArrow, + "ContrastColor": util.ContrastColor, // ----------------------------------------------------------------- // time / number / format diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 5446741287..c4c5376afd 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -135,16 +135,9 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { var ( archivedCSSClass string - textColor = "#111" + textColor = util.ContrastColor(label.Color) labelScope = label.ExclusiveScope() ) - r, g, b := util.HexToRBGColor(label.Color) - - // Determine if label text should be light or dark to be readable on background color - // this doesn't account for saturation or transparency - if util.UseLightTextOnBackground(r, g, b) { - textColor = "#eee" - } description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) @@ -168,7 +161,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m // Make scope and item background colors slightly darker and lighter respectively. // More contrast needed with higher luminance, empirically tweaked. - luminance := util.GetLuminance(r, g, b) + luminance := util.GetRelativeLuminance(label.Color) contrast := 0.01 + luminance*0.03 // Ensure we add the same amount of contrast also near 0 and 1. darken := contrast + math.Max(luminance+contrast-1.0, 0.0) @@ -178,6 +171,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) opacity := GetLabelOpacityByte(label.IsArchived()) + r, g, b := util.HexToRBGColor(label.Color) scopeBytes := []byte{ uint8(math.Min(math.Round(r*darkenFactor), 255)), uint8(math.Min(math.Round(g*darkenFactor), 255)), diff --git a/modules/util/color.go b/modules/util/color.go index 240b045c28..9c520dce78 100644 --- a/modules/util/color.go +++ b/modules/util/color.go @@ -4,22 +4,10 @@ package util import ( "fmt" - "math" "strconv" "strings" ) -// Check similar implementation in web_src/js/utils/color.js and keep synchronization - -// Return R, G, B values defined in reletive luminance -func getLuminanceRGB(channel float64) float64 { - sRGB := channel / 255 - if sRGB <= 0.03928 { - return sRGB / 12.92 - } - return math.Pow((sRGB+0.055)/1.055, 2.4) -} - // Get color as RGB values in 0..255 range from the hex color string (with or without #) func HexToRBGColor(colorString string) (float64, float64, float64) { hexString := colorString @@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) { return r, g, b } -// return luminance given RGB channels -// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance -func GetLuminance(r, g, b float64) float64 { - R := getLuminanceRGB(r) - G := getLuminanceRGB(g) - B := getLuminanceRGB(b) - luminance := 0.2126*R + 0.7152*G + 0.0722*B - return luminance +// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance +// Keep this in sync with web_src/js/utils/color.js +func GetRelativeLuminance(color string) float64 { + r, g, b := HexToRBGColor(color) + return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255 } -// Reference from: https://firsching.ch/github_labels.html -// In the future WCAG 3 APCA may be a better solution. -// Check if text should use light color based on RGB of background -func UseLightTextOnBackground(r, g, b float64) bool { - return GetLuminance(r, g, b) < 0.453 +func UseLightText(backgroundColor string) bool { + return GetRelativeLuminance(backgroundColor) < 0.453 +} + +// Given a background color, returns a black or white foreground color that the highest +// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. +// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 +func ContrastColor(backgroundColor string) string { + if UseLightText(backgroundColor) { + return "#fff" + } + return "#000" } diff --git a/modules/util/color_test.go b/modules/util/color_test.go index d96ac36730..be6e6b122a 100644 --- a/modules/util/color_test.go +++ b/modules/util/color_test.go @@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) { } } -func Test_UseLightTextOnBackground(t *testing.T) { +func Test_UseLightText(t *testing.T) { cases := []struct { - r float64 - g float64 - b float64 - expected bool + color string + expected string }{ - {215, 58, 74, true}, - {0, 117, 202, true}, - {207, 211, 215, false}, - {162, 238, 239, false}, - {112, 87, 255, true}, - {0, 134, 114, true}, - {228, 230, 105, false}, - {216, 118, 227, true}, - {255, 255, 255, false}, - {43, 134, 133, true}, - {43, 135, 134, true}, - {44, 135, 134, true}, - {59, 182, 179, true}, - {124, 114, 104, true}, - {126, 113, 108, true}, - {129, 112, 109, true}, - {128, 112, 112, true}, + {"#d73a4a", "#fff"}, + {"#0075ca", "#fff"}, + {"#cfd3d7", "#000"}, + {"#a2eeef", "#000"}, + {"#7057ff", "#fff"}, + {"#008672", "#fff"}, + {"#e4e669", "#000"}, + {"#d876e3", "#000"}, + {"#ffffff", "#000"}, + {"#2b8684", "#fff"}, + {"#2b8786", "#fff"}, + {"#2c8786", "#000"}, + {"#3bb6b3", "#000"}, + {"#7c7268", "#fff"}, + {"#7e716c", "#fff"}, + {"#81706d", "#fff"}, + {"#807070", "#fff"}, + {"#84b6eb", "#000"}, } for n, c := range cases { - result := UseLightTextOnBackground(c.r, c.g, c.b) - assert.Equal(t, c.expected, result, "case %d: error should match", n) + assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n) } } diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index d89750862e..861c7ef5a9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -66,13 +66,13 @@