Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute proper foreground color for labels #16729

Merged
merged 4 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions models/issue_label.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package models
import (
"fmt"
"html/template"
"math"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -138,19 +139,44 @@ func (label *Label) BelongsToRepo() bool {
return label.RepoID > 0
}

// SrgbToLinear converts a component of an sRGB color to its linear intensity
// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
func SrgbToLinear(color uint8) float64 {
flt := float64(color) / 255
if flt <= 0.04045 {
return flt / 12.92
}
return math.Pow((flt+0.055)/1.055, 2.4)
}

// Luminance returns the luminance of an sRGB color
func Luminance(color uint32) float64 {
r := SrgbToLinear(uint8(0xFF & (color >> 16)))
g := SrgbToLinear(uint8(0xFF & (color >> 8)))
b := SrgbToLinear(uint8(0xFF & color))

// luminance ratios for sRGB
return 0.2126*r + 0.7152*g + 0.0722*b
}

// LuminanceThreshold is the luminance at which white and black appear to have the same contrast
// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05
// i.e. math.Sqrt(1.05*0.05) - 0.05
const LuminanceThreshold float64 = 0.179

// ForegroundColor calculates the text color for labels based
// on their background color.
func (label *Label) ForegroundColor() template.CSS {
if strings.HasPrefix(label.Color, "#") {
if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
r := float32(0xFF & (color >> 16))
g := float32(0xFF & (color >> 8))
b := float32(0xFF & color)
luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
// NOTE: see web_src/js/components/ContextPopup.vue for similar implementation
luminance := Luminance(uint32(color))

if luminance < 0.66 {
// prefer white or black based upon contrast
if luminance < LuminanceThreshold {
return template.CSS("#fff")
}
return template.CSS("#000")
}
}

Expand Down
29 changes: 22 additions & 7 deletions web_src/js/components/ContextPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ import {SvgIcon} from '../svg.js';

const {AppSubUrl} = window.config;

// NOTE: see models/issue_label.go for similar implementation
const srgbToLinear = (color) => {
color /= 255;
if (color <= 0.04045) {
return color / 12.92;
}
return ((color + 0.055) / 1.055) ** 2.4;
};
const luminance = (colorString) => {
const r = srgbToLinear(parseInt(colorString.substring(0, 2), 16));
const g = srgbToLinear(parseInt(colorString.substring(2, 4), 16));
const b = srgbToLinear(parseInt(colorString.substring(4, 6), 16));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const luminanceThreshold = 0.179;

export default {
name: 'ContextPopup',

Expand Down Expand Up @@ -74,14 +90,13 @@ export default {

labels() {
return this.issue.labels.map((label) => {
const red = parseInt(label.color.substring(0, 2), 16);
const green = parseInt(label.color.substring(2, 4), 16);
const blue = parseInt(label.color.substring(4, 6), 16);
let color = '#ffffff';
if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) {
color = '#000000';
let textColor;
if (luminance(label.color) < luminanceThreshold) {
textColor = '#ffffff';
} else {
textColor = '#000000';
}
return {name: label.name, color: `#${label.color}`, textColor: color};
return {name: label.name, color: `#${label.color}`, textColor};
});
}
},
Expand Down