Actually compute proper foreground color for labels (#16729)
This commit is contained in:
parent
20efc6b56c
commit
29b971b6d5
2 changed files with 53 additions and 12 deletions
|
@ -8,6 +8,7 @@ package models
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -138,19 +139,44 @@ func (label *Label) BelongsToRepo() bool {
|
||||||
return label.RepoID > 0
|
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
|
// ForegroundColor calculates the text color for labels based
|
||||||
// on their background color.
|
// on their background color.
|
||||||
func (label *Label) ForegroundColor() template.CSS {
|
func (label *Label) ForegroundColor() template.CSS {
|
||||||
if strings.HasPrefix(label.Color, "#") {
|
if strings.HasPrefix(label.Color, "#") {
|
||||||
if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
|
if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
|
||||||
r := float32(0xFF & (color >> 16))
|
// NOTE: see web_src/js/components/ContextPopup.vue for similar implementation
|
||||||
g := float32(0xFF & (color >> 8))
|
luminance := Luminance(uint32(color))
|
||||||
b := float32(0xFF & color)
|
|
||||||
luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
|
|
||||||
|
|
||||||
if luminance < 0.66 {
|
// prefer white or black based upon contrast
|
||||||
|
if luminance < LuminanceThreshold {
|
||||||
return template.CSS("#fff")
|
return template.CSS("#fff")
|
||||||
}
|
}
|
||||||
|
return template.CSS("#000")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,22 @@ import {SvgIcon} from '../svg.js';
|
||||||
|
|
||||||
const {AppSubUrl} = window.config;
|
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 {
|
export default {
|
||||||
name: 'ContextPopup',
|
name: 'ContextPopup',
|
||||||
|
|
||||||
|
@ -74,14 +90,13 @@ export default {
|
||||||
|
|
||||||
labels() {
|
labels() {
|
||||||
return this.issue.labels.map((label) => {
|
return this.issue.labels.map((label) => {
|
||||||
const red = parseInt(label.color.substring(0, 2), 16);
|
let textColor;
|
||||||
const green = parseInt(label.color.substring(2, 4), 16);
|
if (luminance(label.color) < luminanceThreshold) {
|
||||||
const blue = parseInt(label.color.substring(4, 6), 16);
|
textColor = '#ffffff';
|
||||||
let color = '#ffffff';
|
} else {
|
||||||
if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) {
|
textColor = '#000000';
|
||||||
color = '#000000';
|
|
||||||
}
|
}
|
||||||
return {name: label.name, color: `#${label.color}`, textColor: color};
|
return {name: label.name, color: `#${label.color}`, textColor};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Reference in a new issue