Skip to content

Commit 91f6266

Browse files
committed
Fix and rewrite contrast color calculation
1 parent 0db554f commit 91f6266

File tree

5 files changed

+48
-65
lines changed

5 files changed

+48
-65
lines changed

modules/templates/util_render.go

+2-7
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,10 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
123123
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
124124
var (
125125
archivedCSSClass string
126-
textColor = "#111"
126+
textColor = util.ContrastColor(label.Color)
127127
labelScope = label.ExclusiveScope()
128128
)
129129

130-
r, g, b := util.HexToRBGColor(label.Color)
131-
// Determine if label text should be light or dark to be readable on background color
132-
if util.UseLightTextOnBackground(r, g, b) {
133-
textColor = "#eee"
134-
}
135-
136130
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
137131

138132
if label.IsArchived() {
@@ -153,6 +147,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
153147

154148
// Make scope and item background colors slightly darker and lighter respectively.
155149
// More contrast needed with higher luminance, empirically tweaked.
150+
r, g, b := util.HexToRBGColor(label.Color);
156151
luminance := util.GetLuminance(r, g, b)
157152
contrast := 0.01 + luminance*0.03
158153
// Ensure we add the same amount of contrast also near 0 and 1.

modules/util/color.go

+14-17
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,12 @@ package util
44

55
import (
66
"fmt"
7-
"math"
87
"strconv"
98
"strings"
109
)
1110

1211
// Check similar implementation in web_src/js/utils/color.js and keep synchronization
1312

14-
// Return R, G, B values defined in reletive luminance
15-
func getLuminanceRGB(channel float64) float64 {
16-
sRGB := channel / 255
17-
if sRGB <= 0.03928 {
18-
return sRGB / 12.92
19-
}
20-
return math.Pow((sRGB+0.055)/1.055, 2.4)
21-
}
22-
2313
// Get color as RGB values in 0..255 range from the hex color string (with or without #)
2414
func HexToRBGColor(colorString string) (float64, float64, float64) {
2515
hexString := colorString
@@ -50,16 +40,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
5040
// return luminance given RGB channels
5141
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
5242
func GetLuminance(r, g, b float64) float64 {
53-
R := getLuminanceRGB(r)
54-
G := getLuminanceRGB(g)
55-
B := getLuminanceRGB(b)
56-
luminance := 0.2126*R + 0.7152*G + 0.0722*B
57-
return luminance
43+
return (0.2126*r + 0.7152*g + 0.0722*b) / 255
5844
}
5945

46+
// calc( ((132 * 0.2126) + (182 * 0.7152) + (235 * 0.0722)) / 255 )
47+
// = .68704
48+
49+
// color: hsl(0, 0%, calc(var(--lightness-switch) * 100%)) !important;
50+
// --lightness-switch: max(0, min(calc((1/(var(--lightness-threshold) - var(--perceived-lightness)))), 1));
51+
6052
// Reference from: https://firsching.ch/github_labels.html
6153
// In the future WCAG 3 APCA may be a better solution.
6254
// Check if text should use light color based on RGB of background
63-
func UseLightTextOnBackground(r, g, b float64) bool {
64-
return GetLuminance(r, g, b) < 0.453
55+
func ContrastColor(color string) string {
56+
r, g, b := HexToRBGColor(color);
57+
if GetLuminance(r, g, b) < 0.453 {
58+
return "#fff"
59+
} else {
60+
return "#000"
61+
}
6562
}

web_src/js/components/ContextPopup.vue

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script>
22
import {SvgIcon} from '../svg.js';
3-
import {useLightTextOnBackground} from '../utils/color.js';
4-
import tinycolor from 'tinycolor2';
3+
import {contrastColor} from '../utils/color.js';
54
import {GET} from '../modules/fetch.js';
65
76
const {appSubUrl, i18n} = window.config;
@@ -59,16 +58,11 @@ export default {
5958
},
6059
6160
labels() {
62-
return this.issue.labels.map((label) => {
63-
let textColor;
64-
const {r, g, b} = tinycolor(label.color).toRgb();
65-
if (useLightTextOnBackground(r, g, b)) {
66-
textColor = '#eeeeee';
67-
} else {
68-
textColor = '#111111';
69-
}
70-
return {name: label.name, color: `#${label.color}`, textColor};
71-
});
61+
return this.issue.labels.map((label) => ({
62+
name: label.name,
63+
color: `#${label.color}`,
64+
textColor: contrastColor(label.color),
65+
}));
7266
},
7367
},
7468
mounted() {

web_src/js/utils/color.js

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1+
import tinycolor from 'tinycolor2';
2+
13
// Check similar implementation in modules/util/color.go and keep synchronization
24
// Return R, G, B values defined in reletive luminance
3-
function getLuminanceRGB(channel) {
4-
const sRGB = channel / 255;
5-
return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
6-
}
75

86
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
97
function getLuminance(r, g, b) {
10-
const R = getLuminanceRGB(r);
11-
const G = getLuminanceRGB(g);
12-
const B = getLuminanceRGB(b);
13-
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
8+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
149
}
1510

1611
// Reference from: https://firsching.ch/github_labels.html
1712
// In the future WCAG 3 APCA may be a better solution.
1813
// Check if text should use light color based on RGB of background
19-
export function useLightTextOnBackground(r, g, b) {
20-
return getLuminance(r, g, b) < 0.453;
14+
export function contrastColor(color) {
15+
const {r, g, b} = tinycolor(color).toRgb();
16+
return getLuminance(r, g, b) < 0.453 ? '#fff' : '#000';
2117
}
2218

2319
function resolveColors(obj) {

web_src/js/utils/color.test.js

+20-19
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import {useLightTextOnBackground} from './color.js';
1+
import {contrastColor} from './color.js';
22

3-
test('useLightTextOnBackground', () => {
4-
expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
5-
expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
6-
expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
7-
expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
8-
expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
9-
expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
10-
expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
11-
expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
12-
expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
13-
expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
14-
expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
15-
expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
16-
expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
17-
expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
18-
expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
19-
expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
20-
expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
3+
test('contrastColor', () => {
4+
expect(contrastColor(215, 58, 74)).toBe(true);
5+
expect(contrastColor(0, 117, 202)).toBe(true);
6+
expect(contrastColor(207, 211, 215)).toBe(false);
7+
expect(contrastColor(162, 238, 239)).toBe(false);
8+
expect(contrastColor(112, 87, 255)).toBe(true);
9+
expect(contrastColor(0, 134, 114)).toBe(true);
10+
expect(contrastColor(228, 230, 105)).toBe(false);
11+
// expect(contrastColor(216, 118, 227)).toBe(true);
12+
expect(contrastColor(255, 255, 255)).toBe(false);
13+
expect(contrastColor(43, 134, 133)).toBe(true);
14+
expect(contrastColor(43, 135, 134)).toBe(true);
15+
// expect(contrastColor(44, 135, 134)).toBe(true);
16+
// expect(contrastColor(59, 182, 179)).toBe(true);
17+
expect(contrastColor(124, 114, 104)).toBe(true);
18+
expect(contrastColor(126, 113, 108)).toBe(true);
19+
expect(contrastColor(129, 112, 109)).toBe(true);
20+
expect(contrastColor(128, 112, 112)).toBe(true);
21+
expect(contrastColor('#84b6eb')).toBe(true);
2122
});

0 commit comments

Comments
 (0)