Skip to content

Commit

Permalink
Fix and rewrite contrast color calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
silverwind committed Apr 1, 2024
1 parent 0db554f commit 91f6266
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 65 deletions.
9 changes: 2 additions & 7 deletions modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,10 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
var (
archivedCSSClass string
textColor = "#111"
textColor = util.ContrastColor(label.Color)
labelScope = label.ExclusiveScope()
)

r, g, b := util.HexToRBGColor(label.Color)
// Determine if label text should be light or dark to be readable on background color
if util.UseLightTextOnBackground(r, g, b) {
textColor = "#eee"
}

description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))

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

// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
r, g, b := util.HexToRBGColor(label.Color);
luminance := util.GetLuminance(r, g, b)
contrast := 0.01 + luminance*0.03
// Ensure we add the same amount of contrast also near 0 and 1.
Expand Down
31 changes: 14 additions & 17 deletions modules/util/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,12 @@ package util

import (
"fmt"
"math"
"strconv"
"strings"
)

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

// Return R, G, B values defined in reletive luminance
func getLuminanceRGB(channel float64) float64 {
sRGB := channel / 255
if sRGB <= 0.03928 {
return sRGB / 12.92
}
return math.Pow((sRGB+0.055)/1.055, 2.4)
}

// Get color as RGB values in 0..255 range from the hex color string (with or without #)
func HexToRBGColor(colorString string) (float64, float64, float64) {
hexString := colorString
Expand Down Expand Up @@ -50,16 +40,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
// return luminance given RGB channels
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
func GetLuminance(r, g, b float64) float64 {
R := getLuminanceRGB(r)
G := getLuminanceRGB(g)
B := getLuminanceRGB(b)
luminance := 0.2126*R + 0.7152*G + 0.0722*B
return luminance
return (0.2126*r + 0.7152*g + 0.0722*b) / 255
}

// calc( ((132 * 0.2126) + (182 * 0.7152) + (235 * 0.0722)) / 255 )
// = .68704

// color: hsl(0, 0%, calc(var(--lightness-switch) * 100%)) !important;
// --lightness-switch: max(0, min(calc((1/(var(--lightness-threshold) - var(--perceived-lightness)))), 1));

// Reference from: https://firsching.ch/github_labels.html
// In the future WCAG 3 APCA may be a better solution.
// Check if text should use light color based on RGB of background
func UseLightTextOnBackground(r, g, b float64) bool {
return GetLuminance(r, g, b) < 0.453
func ContrastColor(color string) string {
r, g, b := HexToRBGColor(color);
if GetLuminance(r, g, b) < 0.453 {
return "#fff"
} else {
return "#000"
}
}
18 changes: 6 additions & 12 deletions web_src/js/components/ContextPopup.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script>
import {SvgIcon} from '../svg.js';
import {useLightTextOnBackground} from '../utils/color.js';
import tinycolor from 'tinycolor2';
import {contrastColor} from '../utils/color.js';
import {GET} from '../modules/fetch.js';
const {appSubUrl, i18n} = window.config;
Expand Down Expand Up @@ -59,16 +58,11 @@ export default {
},
labels() {
return this.issue.labels.map((label) => {
let textColor;
const {r, g, b} = tinycolor(label.color).toRgb();
if (useLightTextOnBackground(r, g, b)) {
textColor = '#eeeeee';
} else {
textColor = '#111111';
}
return {name: label.name, color: `#${label.color}`, textColor};
});
return this.issue.labels.map((label) => ({
name: label.name,
color: `#${label.color}`,
textColor: contrastColor(label.color),
}));
},
},
mounted() {
Expand Down
16 changes: 6 additions & 10 deletions web_src/js/utils/color.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import tinycolor from 'tinycolor2';

// Check similar implementation in modules/util/color.go and keep synchronization
// Return R, G, B values defined in reletive luminance
function getLuminanceRGB(channel) {
const sRGB = channel / 255;
return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
}

// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
function getLuminance(r, g, b) {
const R = getLuminanceRGB(r);
const G = getLuminanceRGB(g);
const B = getLuminanceRGB(b);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}

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

function resolveColors(obj) {
Expand Down
39 changes: 20 additions & 19 deletions web_src/js/utils/color.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import {useLightTextOnBackground} from './color.js';
import {contrastColor} from './color.js';

test('useLightTextOnBackground', () => {
expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
test('contrastColor', () => {
expect(contrastColor(215, 58, 74)).toBe(true);
expect(contrastColor(0, 117, 202)).toBe(true);
expect(contrastColor(207, 211, 215)).toBe(false);
expect(contrastColor(162, 238, 239)).toBe(false);
expect(contrastColor(112, 87, 255)).toBe(true);
expect(contrastColor(0, 134, 114)).toBe(true);
expect(contrastColor(228, 230, 105)).toBe(false);
// expect(contrastColor(216, 118, 227)).toBe(true);
expect(contrastColor(255, 255, 255)).toBe(false);
expect(contrastColor(43, 134, 133)).toBe(true);
expect(contrastColor(43, 135, 134)).toBe(true);
// expect(contrastColor(44, 135, 134)).toBe(true);
// expect(contrastColor(59, 182, 179)).toBe(true);
expect(contrastColor(124, 114, 104)).toBe(true);
expect(contrastColor(126, 113, 108)).toBe(true);
expect(contrastColor(129, 112, 109)).toBe(true);
expect(contrastColor(128, 112, 112)).toBe(true);
expect(contrastColor('#84b6eb')).toBe(true);
});

0 comments on commit 91f6266

Please sign in to comment.