diff --git a/commands/issue.go b/commands/issue.go index b9d7e7ec484416becdcba3c765896a635a23321e..362a386c11b3205075cdb296c9520e0a0ba67fc9 100644 --- a/commands/issue.go +++ b/commands/issue.go @@ -600,13 +600,44 @@ func formatLabel(label github.IssueLabel, colorize bool) string { func colorizeLabel(label github.IssueLabel, color *utils.Color) string { bgColorCode := utils.RgbToTermColorCode(color) - return fmt.Sprintf("\033[38;5;%d;48;%sm %s \033[m", - getSuitableLabelTextColor(color), bgColorCode, label.Name) + fgColor := pickHighContrastTextColor(color) + fgColorCode := utils.RgbToTermColorCode(fgColor) + return fmt.Sprintf("\033[38;%s;48;%sm %s \033[m", + fgColorCode, bgColorCode, label.Name) } -func getSuitableLabelTextColor(color *utils.Color) int { - if color.Brightness() < 0.65 { - return 15 // white text +func pickHighContrastTextColor(color *utils.Color) *utils.Color { + var candidates [12]*utils.Color + hsl := color.ToHsl() + a := hsl.ScaleLightness(0.5) + candidates[0] = a.ToRgb() + candidates[1] = a.ScaleLightness(0.25).ToRgb() + candidates[2] = a.ScaleSaturation(-0.25).ToRgb() + b := hsl.ScaleSaturation(0.75) + candidates[3] = b.ToRgb() + candidates[4] = b.ScaleLightness(0.25).ToRgb() + candidates[5] = b.ScaleSaturation(-0.25).ToRgb() + c := hsl.ScaleLightness(-0.5) + candidates[6] = c.ToRgb() + candidates[7] = c.ScaleLightness(-0.25).ToRgb() + candidates[8] = c.ScaleSaturation(-0.25).ToRgb() + d := hsl.ScaleSaturation(-0.75) + candidates[9] = d.ToRgb() + candidates[10] = d.ScaleLightness(-0.25).ToRgb() + candidates[11] = d.ScaleSaturation(-0.25).ToRgb() + + foundContrastRatio := -999.0 + ix := -1 + for i := 0; i < 12; i++ { + contrastRatio := color.ContrastRatio(candidates[i]) + if contrastRatio > foundContrastRatio { + ix = i + foundContrastRatio = contrastRatio + } + } + if foundContrastRatio >= 7.0 { + return candidates[ix] + } else { + return candidates[11] } - return 16 // black text } diff --git a/utils/color.go b/utils/color.go index 3a84419c6306c41e2ee4b8a1e9cc17234ed3c42d..c1bdcd17dbd0ce9715a7397263b042bd8e4c9c6d 100644 --- a/utils/color.go +++ b/utils/color.go @@ -50,6 +50,106 @@ func (c *Color) Distance(other *Color) float64 { math.Pow(float64(c.Blue-other.Blue), 2))) } +func rgbComponentToBoldValue(component int64) float64 { + srgb := float64(component) / 255 + if srgb <= 0.03928 { + return srgb / 12.92 + } else { + return math.Pow(((srgb + 0.055) / 1.055), 2.4) + } +} + +func (c *Color) Luminance() float64 { + return 0.2126*rgbComponentToBoldValue(c.Red) + + 0.7152*rgbComponentToBoldValue(c.Green) + + 0.0722*rgbComponentToBoldValue(c.Blue) +} + +func (c *Color) ContrastRatio(other *Color) float64 { + L := c.Luminance() + otherL := other.Luminance() + var L1, L2 float64 + if L > otherL { + L1, L2 = L, otherL + } else { + L1, L2 = otherL, L + } + ratio := (L1 + 0.05) / (L2 + 0.05) + return ratio +} + +type HslColor struct { + Hue float64 + Saturation float64 + Lightness float64 +} + +func (c *Color) ToHsl() *HslColor { + rPrime := float64(c.Red) / 255 + gPrime := float64(c.Green) / 255 + bPrime := float64(c.Blue) / 255 + cMax := math.Max(rPrime, math.Max(gPrime, bPrime)) + cMin := math.Min(rPrime, math.Min(gPrime, bPrime)) + delta := cMax - cMin + + var H float64 + if delta == 0 { + H = 0 + } else if cMax == rPrime { + H = 60 * math.Mod((gPrime-bPrime)/delta, 6) + } else if cMax == gPrime { + H = 60 * (((bPrime - rPrime) / delta) + 2) + } else { + H = 60 * (((rPrime - gPrime) / delta) + 4) + } + var L float64 + L = (cMax + cMin) / 2 + var S float64 + if delta == 0 { + S = 0 + } else { + S = delta / (1 - math.Abs((2*L)-1)) + } + + return &HslColor{H, S, L} +} + +func (c *HslColor) ToRgb() *Color { + C := (1 - math.Abs(2*c.Lightness-1)) * c.Saturation + X := C * (1 - math.Abs(math.Mod(c.Hue/60, 2)-1)) + m := c.Lightness - (C / 2) + var rPrime, gPrime, bPrime float64 + switch { + case c.Hue < 60.0: + rPrime, gPrime, bPrime = C, X, 0 + case c.Hue < 120.0: + rPrime, gPrime, bPrime = X, C, 0 + case c.Hue < 180.0: + rPrime, gPrime, bPrime = 0, C, X + case c.Hue < 240.0: + rPrime, gPrime, bPrime = 0, X, C + case c.Hue < 300.0: + rPrime, gPrime, bPrime = X, 0, C + case c.Hue < 360.0: + rPrime, gPrime, bPrime = C, 0, X + } + R := int64((rPrime + m) * 255) + G := int64((gPrime + m) * 255) + B := int64((bPrime + m) * 255) + + return &Color{R, G, B} +} + +func (c *HslColor) ScaleLightness(x float64) *HslColor { + newLightness := math.Round(math.Min(math.Max(0, c.Lightness*(1.0+x)), 1.0)) + return &HslColor{c.Hue, c.Saturation, newLightness} +} + +func (c *HslColor) ScaleSaturation(x float64) *HslColor { + newSaturation := math.Round(math.Min(math.Max(0, c.Lightness*(1.0+x)), 1.0)) + return &HslColor{c.Hue, newSaturation, c.Lightness} +} + var x6colorIndexes = [6]int64{0, 95, 135, 175, 215, 255} var x6colorCube [216]Color