Skip to content

Commit 36887ed

Browse files
silverwinddelvhGiteaBot
authored
Fix and rewrite contrast color calculation, fix project-related bugs (#30237)
1. The previous color contrast calculation function was incorrect at least for the `#84b6eb` where it output low-contrast white instead of black. I've rewritten these functions now to accept hex colors and to match GitHub's calculation and to output pure white/black for maximum contrast. Before and after: <img width="94" alt="Screenshot 2024-04-02 at 01 53 46" src="https://github.com/go-gitea/gitea/assets/115237/00b39e15-a377-4458-95cf-ceec74b78228"><img width="90" alt="Screenshot 2024-04-02 at 01 51 30" src="https://github.com/go-gitea/gitea/assets/115237/1677067a-8d8f-47eb-82c0-76330deeb775"> 2. Fix project-related issues: - Expose the new `ContrastColor` function as template helper and use it for project cards, replacing the previous JS solution which eliminates a flash of wrong color on page load. - Fix a bug where if editing a project title, the counter would get lost. - Move `rgbToHex` function to color utils. @HesterG fyi --------- Co-authored-by: delvh <[email protected]> Co-authored-by: Giteabot <[email protected]>
1 parent 019857a commit 36887ed

File tree

14 files changed

+135
-191
lines changed

14 files changed

+135
-191
lines changed

modules/templates/helper.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap {
5353
"JsonUtils": NewJsonUtils,
5454

5555
// -----------------------------------------------------------------
56-
// svg / avatar / icon
56+
// svg / avatar / icon / color
5757
"svg": svg.RenderHTML,
5858
"EntryIcon": base.EntryIcon,
5959
"MigrationIcon": MigrationIcon,
6060
"ActionIcon": ActionIcon,
61-
62-
"SortArrow": SortArrow,
61+
"SortArrow": SortArrow,
62+
"ContrastColor": util.ContrastColor,
6363

6464
// -----------------------------------------------------------------
6565
// time / number / format

modules/templates/util_render.go

+3-8
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,7 +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.
156-
luminance := util.GetLuminance(r, g, b)
150+
luminance := util.GetRelativeLuminance(label.Color)
157151
contrast := 0.01 + luminance*0.03
158152
// Ensure we add the same amount of contrast also near 0 and 1.
159153
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
@@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
162156
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
163157
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
164158

159+
r, g, b := util.HexToRBGColor(label.Color)
165160
scopeBytes := []byte{
166161
uint8(math.Min(math.Round(r*darkenFactor), 255)),
167162
uint8(math.Min(math.Round(g*darkenFactor), 255)),

modules/util/color.go

+17-25
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,10 @@ package util
44

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

12-
// Check similar implementation in web_src/js/utils/color.js and keep synchronization
13-
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-
2311
// Get color as RGB values in 0..255 range from the hex color string (with or without #)
2412
func HexToRBGColor(colorString string) (float64, float64, float64) {
2513
hexString := colorString
@@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
4735
return r, g, b
4836
}
4937

50-
// return luminance given RGB channels
51-
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
52-
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
38+
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
39+
// Keep this in sync with web_src/js/utils/color.js
40+
func GetRelativeLuminance(color string) float64 {
41+
r, g, b := HexToRBGColor(color)
42+
return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
5843
}
5944

60-
// Reference from: https://firsching.ch/github_labels.html
61-
// In the future WCAG 3 APCA may be a better solution.
62-
// 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
45+
func UseLightText(backgroundColor string) bool {
46+
return GetRelativeLuminance(backgroundColor) < 0.453
47+
}
48+
49+
// Given a background color, returns a black or white foreground color that the highest
50+
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
51+
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
52+
func ContrastColor(backgroundColor string) string {
53+
if UseLightText(backgroundColor) {
54+
return "#fff"
55+
}
56+
return "#000"
6557
}

modules/util/color_test.go

+22-24
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
3333
}
3434
}
3535

36-
func Test_UseLightTextOnBackground(t *testing.T) {
36+
func Test_UseLightText(t *testing.T) {
3737
cases := []struct {
38-
r float64
39-
g float64
40-
b float64
41-
expected bool
38+
color string
39+
expected string
4240
}{
43-
{215, 58, 74, true},
44-
{0, 117, 202, true},
45-
{207, 211, 215, false},
46-
{162, 238, 239, false},
47-
{112, 87, 255, true},
48-
{0, 134, 114, true},
49-
{228, 230, 105, false},
50-
{216, 118, 227, true},
51-
{255, 255, 255, false},
52-
{43, 134, 133, true},
53-
{43, 135, 134, true},
54-
{44, 135, 134, true},
55-
{59, 182, 179, true},
56-
{124, 114, 104, true},
57-
{126, 113, 108, true},
58-
{129, 112, 109, true},
59-
{128, 112, 112, true},
41+
{"#d73a4a", "#fff"},
42+
{"#0075ca", "#fff"},
43+
{"#cfd3d7", "#000"},
44+
{"#a2eeef", "#000"},
45+
{"#7057ff", "#fff"},
46+
{"#008672", "#fff"},
47+
{"#e4e669", "#000"},
48+
{"#d876e3", "#000"},
49+
{"#ffffff", "#000"},
50+
{"#2b8684", "#fff"},
51+
{"#2b8786", "#fff"},
52+
{"#2c8786", "#000"},
53+
{"#3bb6b3", "#000"},
54+
{"#7c7268", "#fff"},
55+
{"#7e716c", "#fff"},
56+
{"#81706d", "#fff"},
57+
{"#807070", "#fff"},
58+
{"#84b6eb", "#000"},
6059
}
6160
for n, c := range cases {
62-
result := UseLightTextOnBackground(c.r, c.g, c.b)
63-
assert.Equal(t, c.expected, result, "case %d: error should match", n)
61+
assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
6462
}
6563
}

templates/projects/view.tmpl

+3-5
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@
6666
<div id="project-board">
6767
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
6868
{{range .Columns}}
69-
<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
69+
<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
7070
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
7171
<div class="ui large label project-column-title tw-py-1">
7272
<div class="ui small circular grey label project-column-issue-count">
7373
{{.NumIssues ctx}}
7474
</div>
75-
{{.Title}}
75+
<span class="project-column-title-label">{{.Title}}</span>
7676
</div>
7777
{{if $canWriteProject}}
7878
<div class="ui dropdown jump item">
@@ -153,9 +153,7 @@
153153
</div>
154154
{{end}}
155155
</div>
156-
157-
<div class="divider"></div>
158-
156+
<div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div>
159157
<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
160158
{{range (index $.IssuesMap .ID)}}
161159
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">

web_src/css/features/projects.css

+11-16
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,27 @@
2222
cursor: default;
2323
}
2424

25+
.project-column .issue-card {
26+
color: var(--color-text);
27+
}
28+
2529
.project-column-header {
2630
display: flex;
2731
align-items: center;
2832
justify-content: space-between;
2933
}
3034

31-
.project-column-header.dark-label {
32-
color: var(--color-project-board-dark-label) !important;
33-
}
34-
35-
.project-column-header.dark-label .project-column-title {
36-
color: var(--color-project-board-dark-label) !important;
37-
}
38-
39-
.project-column-header.light-label {
40-
color: var(--color-project-board-light-label) !important;
41-
}
42-
43-
.project-column-header.light-label .project-column-title {
44-
color: var(--color-project-board-light-label) !important;
45-
}
46-
4735
.project-column-title {
4836
background: none !important;
4937
line-height: 1.25 !important;
5038
cursor: inherit;
5139
}
5240

41+
.project-column-title,
42+
.project-column-issue-count {
43+
color: inherit !important;
44+
}
45+
5346
.project-column > .cards {
5447
flex: 1;
5548
display: flex;
@@ -64,6 +57,8 @@
6457

6558
.project-column > .divider {
6659
margin: 5px 0;
60+
border-color: currentcolor;
61+
opacity: .5;
6762
}
6863

6964
.project-column:first-child {

web_src/css/repo.css

+14-1
Original file line numberDiff line numberDiff line change
@@ -2273,8 +2273,21 @@
22732273
height: 0.5em;
22742274
}
22752275

2276+
.labels-list {
2277+
display: flex;
2278+
flex-wrap: wrap;
2279+
gap: 0.25em;
2280+
}
2281+
2282+
.labels-list a {
2283+
display: flex;
2284+
text-decoration: none;
2285+
}
2286+
22762287
.labels-list .label {
2277-
margin: 2px 0;
2288+
padding: 0 6px;
2289+
margin: 0 !important;
2290+
min-height: 20px;
22782291
display: inline-flex !important;
22792292
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
22802293
}

web_src/css/repo/issue-list.css

-17
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,6 @@
3434
}
3535
}
3636

37-
#issue-list .flex-item-title .labels-list {
38-
display: flex;
39-
flex-wrap: wrap;
40-
gap: 0.25em;
41-
}
42-
43-
#issue-list .flex-item-title .labels-list a {
44-
display: flex;
45-
text-decoration: none;
46-
}
47-
48-
#issue-list .flex-item-title .labels-list .label {
49-
padding: 0 6px;
50-
margin: 0;
51-
min-height: 20px;
52-
}
53-
5437
#issue-list .flex-item-body .branches {
5538
display: inline-flex;
5639
}

web_src/css/themes/theme-gitea-dark.css

-2
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,6 @@
215215
--color-placeholder-text: var(--color-text-light-3);
216216
--color-editor-line-highlight: var(--color-primary-light-5);
217217
--color-project-board-bg: var(--color-secondary-light-2);
218-
--color-project-board-dark-label: #0e1011;
219-
--color-project-board-light-label: #dde0e2;
220218
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
221219
--color-reaction-bg: #e8e8ff12;
222220
--color-reaction-hover-bg: var(--color-primary-light-4);

web_src/css/themes/theme-gitea-light.css

-2
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,6 @@
215215
--color-placeholder-text: var(--color-text-light-3);
216216
--color-editor-line-highlight: var(--color-primary-light-6);
217217
--color-project-board-bg: var(--color-secondary-light-4);
218-
--color-project-board-dark-label: #0e1114;
219-
--color-project-board-light-label: #eaeef2;
220218
--color-caret: var(--color-text-dark);
221219
--color-reaction-bg: #0000170a;
222220
--color-reaction-hover-bg: var(--color-primary-light-5);

web_src/js/components/ContextPopup.vue

+7-13
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() {
@@ -108,7 +102,7 @@ export default {
108102
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
109103
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
110104
<p>{{ body }}</p>
111-
<div>
105+
<div class="labels-list">
112106
<div
113107
v-for="label in labels"
114108
:key="label.name"

0 commit comments

Comments
 (0)