Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and rewrite contrast color calculation, fix project-related bugs #30326

Merged
merged 3 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2371,8 +2371,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