From 1c2c69ae441c2f7ee93dfbb43fb7b83dd14d139d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 3 Jan 2019 04:34:10 +0100 Subject: [PATCH] Simplify picking contrasting label text color Pick between white and black depending on which one is the first to satisfy the contrast ratio of 7.0 (or 4.5 as fallback). --- commands/issue.go | 54 +++++++++++-------------- commands/issue_test.go | 4 +- utils/color.go | 92 +++--------------------------------------- utils/color_test.go | 31 -------------- 4 files changed, 32 insertions(+), 149 deletions(-) delete mode 100644 utils/color_test.go diff --git a/commands/issue.go b/commands/issue.go index 362a386c..8da7bea9 100644 --- a/commands/issue.go +++ b/commands/issue.go @@ -606,38 +606,32 @@ func colorizeLabel(label github.IssueLabel, color *utils.Color) string { fgColorCode, bgColorCode, label.Name) } +type contrastCandidate struct { + color *utils.Color + contrast float64 +} + 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 + candidates := []contrastCandidate{} + appendCandidate := func(c *utils.Color) { + candidates = append(candidates, contrastCandidate{ + color: c, + contrast: color.ContrastRatio(c), + }) + } + + appendCandidate(utils.White) + appendCandidate(utils.Black) + + for _, candidate := range candidates { + if candidate.contrast >= 7.0 { + return candidate.color } } - if foundContrastRatio >= 7.0 { - return candidates[ix] - } else { - return candidates[11] + for _, candidate := range candidates { + if candidate.contrast >= 4.5 { + return candidate.color + } } + return utils.Black } diff --git a/commands/issue_test.go b/commands/issue_test.go index 2c0c674c..5c2c4fae 100644 --- a/commands/issue_test.go +++ b/commands/issue_test.go @@ -66,7 +66,7 @@ func TestFormatIssue(t *testing.T) { }, format: format, colorize: true, - expect: "\033[32m #42\033[m An issue with labels \033[38;2;64;64;64;48;2;128;0;0m bug \033[m \033[38;2;0;0;0;48;2;85;255;85m reproduced \033[m\n", + expect: "\033[32m #42\033[m An issue with labels \033[38;2;255;255;255;48;2;128;0;0m bug \033[m \033[38;2;0;0;0;48;2;85;255;85m reproduced \033[m\n", }, { name: "not colorized", @@ -181,7 +181,7 @@ func TestFormatIssue_customFormatString(t *testing.T) { issue: issue, format: "%l", colorize: true, - expect: "\033[38;2;68;68;68;48;2;136;0;0m bug \033[m \033[38;2;68;68;68;48;2;0;136;0m feature \033[m", + expect: "\033[38;2;255;255;255;48;2;136;0;0m bug \033[m \033[38;2;255;255;255;48;2;0;136;0m feature \033[m", }, { name: "label not colorized", diff --git a/utils/color.go b/utils/color.go index c0ac8afe..98007f39 100644 --- a/utils/color.go +++ b/utils/color.go @@ -7,8 +7,14 @@ import ( "strconv" ) +var ( + Black, White *Color +) + func init() { initColorCube() + Black, _ = NewColor("000000") + White, _ = NewColor("ffffff") } type Color struct { @@ -38,12 +44,6 @@ func NewColor(hex string) (*Color, error) { }, nil } -func (c *Color) Brightness() float32 { - return (0.299*float32(c.Red) + - 0.587*float32(c.Green) + - 0.114*float32(c.Blue)) / 255 -} - func (c *Color) Distance(other *Color) float64 { return math.Sqrt(float64(math.Pow(float64(c.Red-other.Red), 2) + math.Pow(float64(c.Green-other.Green), 2) + @@ -78,86 +78,6 @@ func (c *Color) ContrastRatio(other *Color) float64 { 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 := uint8((rPrime + m) * 255) - G := uint8((gPrime + m) * 255) - B := uint8((bPrime + m) * 255) - - return &Color{R, G, B} -} - -func (c *HslColor) ScaleLightness(x float64) *HslColor { - newLightness := 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 := round(math.Min(math.Max(0, c.Lightness*(1.0+x)), 1.0)) - return &HslColor{c.Hue, newSaturation, c.Lightness} -} - -func round(n float64) float64 { - i := math.Trunc(n) - if math.Abs(n-i) >= 0.5 { - return i + math.Copysign(1, n) - } - return i -} - var x6colorIndexes = [6]uint8{0, 95, 135, 175, 215, 255} var x6colorCube [216]Color diff --git a/utils/color_test.go b/utils/color_test.go deleted file mode 100644 index 587ed1ab..00000000 --- a/utils/color_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "github.com/bmizerany/assert" - "testing" -) - -func TestColorBrightness(t *testing.T) { - c, err := NewColor("880000") - assert.Equal(t, nil, err) - actual := c.Brightness() - assert.Equal(t, float32(0.15946665406227112), actual) -} - -func TestRoundDown(t *testing.T) { - assert.Equal(t, 3.0, round(3.0)) - assert.Equal(t, 3.0, round(3.01)) - assert.Equal(t, 3.0, round(3.49)) -} - -func TestRoundUp(t *testing.T) { - assert.Equal(t, 3.0, round(2.5)) - assert.Equal(t, 3.0, round(2.51)) - assert.Equal(t, 3.0, round(2.99)) -} - -func TestRoundNegative(t *testing.T) { - assert.Equal(t, -2.0, round(-2.49)) - assert.Equal(t, -3.0, round(-2.5)) - assert.Equal(t, -3.0, round(-2.51)) -} -- GitLab