Skip to content

Commit

Permalink
Fix and rewrite contrast color calculation, fix project-related bugs (g…
Browse files Browse the repository at this point in the history
…o-gitea#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]>
  • Loading branch information
3 people committed Apr 7, 2024
1 parent 019857a commit 36887ed
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 191 deletions.
6 changes: 3 additions & 3 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap {
"JsonUtils": NewJsonUtils,

// -----------------------------------------------------------------
// svg / avatar / icon
// svg / avatar / icon / color
"svg": svg.RenderHTML,
"EntryIcon": base.EntryIcon,
"MigrationIcon": MigrationIcon,
"ActionIcon": ActionIcon,

"SortArrow": SortArrow,
"SortArrow": SortArrow,
"ContrastColor": util.ContrastColor,

// -----------------------------------------------------------------
// time / number / format
Expand Down
11 changes: 3 additions & 8 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,7 +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.
luminance := util.GetLuminance(r, g, b)
luminance := util.GetRelativeLuminance(label.Color)
contrast := 0.01 + luminance*0.03
// Ensure we add the same amount of contrast also near 0 and 1.
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
Expand All @@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)

r, g, b := util.HexToRBGColor(label.Color)
scopeBytes := []byte{
uint8(math.Min(math.Round(r*darkenFactor), 255)),
uint8(math.Min(math.Round(g*darkenFactor), 255)),
Expand Down
42 changes: 17 additions & 25 deletions modules/util/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,10 @@ 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 @@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
return r, g, b
}

// 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
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
// Keep this in sync with web_src/js/utils/color.js
func GetRelativeLuminance(color string) float64 {
r, g, b := HexToRBGColor(color)
return (0.2126729*r + 0.7151522*g + 0.0721750*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
func UseLightTextOnBackground(r, g, b float64) bool {
return GetLuminance(r, g, b) < 0.453
func UseLightText(backgroundColor string) bool {
return GetRelativeLuminance(backgroundColor) < 0.453
}

// Given a background color, returns a black or white foreground color that the highest
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
func ContrastColor(backgroundColor string) string {
if UseLightText(backgroundColor) {
return "#fff"
}
return "#000"
}
46 changes: 22 additions & 24 deletions modules/util/color_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
}
}

func Test_UseLightTextOnBackground(t *testing.T) {
func Test_UseLightText(t *testing.T) {
cases := []struct {
r float64
g float64
b float64
expected bool
color string
expected string
}{
{215, 58, 74, true},
{0, 117, 202, true},
{207, 211, 215, false},
{162, 238, 239, false},
{112, 87, 255, true},
{0, 134, 114, true},
{228, 230, 105, false},
{216, 118, 227, true},
{255, 255, 255, false},
{43, 134, 133, true},
{43, 135, 134, true},
{44, 135, 134, true},
{59, 182, 179, true},
{124, 114, 104, true},
{126, 113, 108, true},
{129, 112, 109, true},
{128, 112, 112, true},
{"#d73a4a", "#fff"},
{"#0075ca", "#fff"},
{"#cfd3d7", "#000"},
{"#a2eeef", "#000"},
{"#7057ff", "#fff"},
{"#008672", "#fff"},
{"#e4e669", "#000"},
{"#d876e3", "#000"},
{"#ffffff", "#000"},
{"#2b8684", "#fff"},
{"#2b8786", "#fff"},
{"#2c8786", "#000"},
{"#3bb6b3", "#000"},
{"#7c7268", "#fff"},
{"#7e716c", "#fff"},
{"#81706d", "#fff"},
{"#807070", "#fff"},
{"#84b6eb", "#000"},
}
for n, c := range cases {
result := UseLightTextOnBackground(c.r, c.g, c.b)
assert.Equal(t, c.expected, result, "case %d: error should match", n)
assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
}
}
8 changes: 3 additions & 5 deletions templates/projects/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@
<div id="project-board">
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
{{range .Columns}}
<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
<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}}">
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
<div class="ui large label project-column-title tw-py-1">
<div class="ui small circular grey label project-column-issue-count">
{{.NumIssues ctx}}
</div>
{{.Title}}
<span class="project-column-title-label">{{.Title}}</span>
</div>
{{if $canWriteProject}}
<div class="ui dropdown jump item">
Expand Down Expand Up @@ -153,9 +153,7 @@
</div>
{{end}}
</div>

<div class="divider"></div>

<div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div>
<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
{{range (index $.IssuesMap .ID)}}
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
Expand Down
27 changes: 11 additions & 16 deletions web_src/css/features/projects.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,27 @@
cursor: default;
}

.project-column .issue-card {
color: var(--color-text);
}

.project-column-header {
display: flex;
align-items: center;
justify-content: space-between;
}

.project-column-header.dark-label {
color: var(--color-project-board-dark-label) !important;
}

.project-column-header.dark-label .project-column-title {
color: var(--color-project-board-dark-label) !important;
}

.project-column-header.light-label {
color: var(--color-project-board-light-label) !important;
}

.project-column-header.light-label .project-column-title {
color: var(--color-project-board-light-label) !important;
}

.project-column-title {
background: none !important;
line-height: 1.25 !important;
cursor: inherit;
}

.project-column-title,
.project-column-issue-count {
color: inherit !important;
}

.project-column > .cards {
flex: 1;
display: flex;
Expand All @@ -64,6 +57,8 @@

.project-column > .divider {
margin: 5px 0;
border-color: currentcolor;
opacity: .5;
}

.project-column:first-child {
Expand Down
15 changes: 14 additions & 1 deletion web_src/css/repo.css
Original file line number Diff line number Diff line change
Expand Up @@ -2273,8 +2273,21 @@
height: 0.5em;
}

.labels-list {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}

.labels-list a {
display: flex;
text-decoration: none;
}

.labels-list .label {
margin: 2px 0;
padding: 0 6px;
margin: 0 !important;
min-height: 20px;
display: inline-flex !important;
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
}
Expand Down
17 changes: 0 additions & 17 deletions web_src/css/repo/issue-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,6 @@
}
}

#issue-list .flex-item-title .labels-list {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}

#issue-list .flex-item-title .labels-list a {
display: flex;
text-decoration: none;
}

#issue-list .flex-item-title .labels-list .label {
padding: 0 6px;
margin: 0;
min-height: 20px;
}

#issue-list .flex-item-body .branches {
display: inline-flex;
}
Expand Down
2 changes: 0 additions & 2 deletions web_src/css/themes/theme-gitea-dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,6 @@
--color-placeholder-text: var(--color-text-light-3);
--color-editor-line-highlight: var(--color-primary-light-5);
--color-project-board-bg: var(--color-secondary-light-2);
--color-project-board-dark-label: #0e1011;
--color-project-board-light-label: #dde0e2;
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
--color-reaction-bg: #e8e8ff12;
--color-reaction-hover-bg: var(--color-primary-light-4);
Expand Down
2 changes: 0 additions & 2 deletions web_src/css/themes/theme-gitea-light.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,6 @@
--color-placeholder-text: var(--color-text-light-3);
--color-editor-line-highlight: var(--color-primary-light-6);
--color-project-board-bg: var(--color-secondary-light-4);
--color-project-board-dark-label: #0e1114;
--color-project-board-light-label: #eaeef2;
--color-caret: var(--color-text-dark);
--color-reaction-bg: #0000170a;
--color-reaction-hover-bg: var(--color-primary-light-5);
Expand Down
20 changes: 7 additions & 13 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 Expand Up @@ -108,7 +102,7 @@ export default {
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
<p>{{ body }}</p>
<div>
<div class="labels-list">
<div
v-for="label in labels"
:key="label.name"
Expand Down
Loading

0 comments on commit 36887ed

Please sign in to comment.