diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index bb241790d41..1f3c71afd9f 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -120,15 +120,15 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { itemHTML := ut.RenderEmoji(label.Name[len(labelScope)+1:]) // Make scope and item background colors slightly darker and lighter respectively. - // More contrast needed with higher luminance, empirically tweaked. - luminance := util.GetRelativeLuminance(label.Color) - contrast := 0.01 + luminance*0.03 + // More contrast needed with higher brightness, empirically tweaked. + brightness := util.GetPerceivedBrightness(label.Color) + contrast := 0.01 + brightness*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) - lighten := contrast + math.Max(contrast-luminance, 0.0) + darken := contrast + math.Max(brightness+contrast-1.0, 0.0) + lighten := contrast + math.Max(contrast-brightness, 0.0) // Compute the factor to keep RGB values proportional. - darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) - lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) + darkenFactor := math.Max(brightness-darken, 0.0) / math.Max(brightness, 1.0/255.0) + lightenFactor := math.Min(brightness+lighten, 1.0) / math.Max(brightness, 1.0/255.0) r, g, b := util.HexToRBGColor(label.Color) scopeBytes := []byte{ diff --git a/modules/util/color.go b/modules/util/color.go index 4d2c2517265..f7b31fa51e5 100644 --- a/modules/util/color.go +++ b/modules/util/color.go @@ -5,6 +5,7 @@ package util import ( "fmt" + "math" "strconv" "strings" ) @@ -37,20 +38,34 @@ func HexToRBGColor(colorString string) (float64, float64, float64) { return r, g, b } -// GetRelativeLuminance 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 { +// linearizeChannel undoes the sRGB transfer function, channel is in 0..255 range +func linearizeChannel(channel float64) float64 { + srgb := channel / 255 + if srgb <= 0.04045 { + return srgb / 12.92 + } + return math.Pow((srgb+0.055)/1.055, 2.4) +} + +// getRelativeLuminance returns relative luminance for a SRGB color - https://www.w3.org/TR/WCAG20/#relativeluminancedef +// Keep this in sync with web_src/js/utils/color.ts +func getRelativeLuminance(color string) float64 { + r, g, b := HexToRBGColor(color) + return 0.2126*linearizeChannel(r) + 0.7152*linearizeChannel(g) + 0.0722*linearizeChannel(b) +} + +// GetPerceivedBrightness weights the gamma-encoded channels, so it stays in the same space as the +// raw channels and can scale them proportionally. Not a luminance, don't use it for contrast. +func GetPerceivedBrightness(color string) float64 { r, g, b := HexToRBGColor(color) return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255 } func UseLightText(backgroundColor string) bool { - return GetRelativeLuminance(backgroundColor) < 0.453 + return getRelativeLuminance(backgroundColor) < 0.36 // matches APCA better than WCAG's own 0.179 } // ContrastColor 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" diff --git a/modules/util/color_test.go b/modules/util/color_test.go index abd55512184..4f927dc3fd0 100644 --- a/modules/util/color_test.go +++ b/modules/util/color_test.go @@ -45,11 +45,13 @@ func Test_UseLightText(t *testing.T) { {"#7057ff", "#fff"}, {"#008672", "#fff"}, {"#e4e669", "#000"}, - {"#d876e3", "#000"}, + {"#d876e3", "#fff"}, {"#ffffff", "#000"}, {"#2b8684", "#fff"}, {"#2b8786", "#fff"}, - {"#2c8786", "#000"}, + {"#2c8786", "#fff"}, + {"#2bb3b2", "#fff"}, + {"#2bb4b3", "#000"}, {"#3bb6b3", "#000"}, {"#7c7268", "#fff"}, {"#7e716c", "#fff"}, diff --git a/web_src/js/utils/color.test.ts b/web_src/js/utils/color.test.ts index 9e4fbcca64f..7e1fc87b081 100644 --- a/web_src/js/utils/color.test.ts +++ b/web_src/js/utils/color.test.ts @@ -8,11 +8,13 @@ test('contrastColor', () => { expect(contrastColor('#7057ff')).toBe('#fff'); expect(contrastColor('#008672')).toBe('#fff'); expect(contrastColor('#e4e669')).toBe('#000'); - expect(contrastColor('#d876e3')).toBe('#000'); + expect(contrastColor('#d876e3')).toBe('#fff'); expect(contrastColor('#ffffff')).toBe('#000'); expect(contrastColor('#2b8684')).toBe('#fff'); expect(contrastColor('#2b8786')).toBe('#fff'); - expect(contrastColor('#2c8786')).toBe('#000'); + expect(contrastColor('#2c8786')).toBe('#fff'); + expect(contrastColor('#2bb3b2')).toBe('#fff'); + expect(contrastColor('#2bb4b3')).toBe('#000'); expect(contrastColor('#3bb6b3')).toBe('#000'); expect(contrastColor('#7c7268')).toBe('#fff'); expect(contrastColor('#7e716c')).toBe('#fff'); diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 096356983a8..68f229fa9fc 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -1,20 +1,24 @@ import {colord} from 'colord'; import type {AnyColor} from 'colord'; -/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */ +/** Undoes the sRGB transfer function, channel is in 0..255 range. */ +function linearizeChannel(channel: number): number { + const srgb = channel / 255; + return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4; +} + +/** Returns relative luminance for a SRGB color - https://www.w3.org/TR/WCAG20/#relativeluminancedef */ // Keep this in sync with modules/util/color.go function getRelativeLuminance(color: AnyColor): number { const {r, g, b} = colord(color).toRgb(); - return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; + return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b); } function useLightText(backgroundColor: AnyColor): boolean { - return getRelativeLuminance(backgroundColor) < 0.453; + return getRelativeLuminance(backgroundColor) < 0.36; // matches APCA better than WCAG's own 0.179 } /** Given a background color, returns a black or white foreground color with 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 export function contrastColor(backgroundColor: AnyColor): string { return useLightText(backgroundColor) ? '#fff' : '#000'; }