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 #30237

Merged
merged 34 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c4e0c2c
Fix and rewrite contrast color calculation
silverwind Apr 1, 2024
30a101d
fix the js tests
silverwind Apr 2, 2024
1ad9c2a
more tweaks
silverwind Apr 2, 2024
5eed905
fix go tests
silverwind Apr 2, 2024
7336836
Merge branch 'main' into labcol
silverwind Apr 2, 2024
08e0af6
fix
silverwind Apr 2, 2024
936ec25
more tweaks
silverwind Apr 2, 2024
0fc3c60
add ContrastColor as template helper and use it in projects
silverwind Apr 2, 2024
ba3ad96
unexport
silverwind Apr 2, 2024
a110ddb
restart ci
silverwind Apr 2, 2024
55bf86a
remove unused css
silverwind Apr 2, 2024
8047ff0
important for consistency
silverwind Apr 2, 2024
f6983bc
fix color on card
silverwind Apr 2, 2024
fd0d84d
fix test
silverwind Apr 2, 2024
ea8c998
move rgbToHex to colors.js
silverwind Apr 2, 2024
49d452f
lint
silverwind Apr 2, 2024
25a3b91
rename func
silverwind Apr 2, 2024
180d923
Update modules/util/color.go
silverwind Apr 2, 2024
f3bcb9b
Update modules/util/color.go
silverwind Apr 2, 2024
a13ca31
Update web_src/js/utils/color.js
silverwind Apr 2, 2024
45b1536
Update web_src/js/utils/color.js
silverwind Apr 2, 2024
5963c0e
Update web_src/js/utils/color.js
silverwind Apr 4, 2024
25a8e3b
Merge branch 'main' into labcol
silverwind Apr 4, 2024
4ac9916
use tinycolor
silverwind Apr 4, 2024
4f80376
remove debug
silverwind Apr 4, 2024
4e4ade5
only set inline styles when custom color is set
silverwind Apr 4, 2024
9fee7a8
set color on the divider as well
silverwind Apr 4, 2024
955d634
fix issue with color in PUT
silverwind Apr 4, 2024
525bbb5
format
silverwind Apr 4, 2024
7830f64
fix ContextPopup
silverwind Apr 4, 2024
9f8fc6a
Merge branch 'main' into labcol
GiteaBot Apr 7, 2024
aa8cad1
Merge branch 'main' into labcol
silverwind Apr 7, 2024
a26de2a
use labels-list
silverwind Apr 7, 2024
0950ba0
Merge branch 'main' into labcol
GiteaBot Apr 7, 2024
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)
silverwind marked this conversation as resolved.
Show resolved Hide resolved
}

// 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)
}
}
4 changes: 2 additions & 2 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" style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important" 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
25 changes: 9 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 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 @@ -214,8 +214,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 @@ -214,8 +214,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
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
38 changes: 4 additions & 34 deletions web_src/js/features/repo-projects.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import $ from 'jquery';
import {useLightTextOnBackground} from '../utils/color.js';
import tinycolor from 'tinycolor2';
import {contrastColor, rgbToHex} from '../utils/color.js';
import {createSortable} from '../modules/sortable.js';
import {POST, DELETE, PUT} from '../modules/fetch.js';

Expand Down Expand Up @@ -102,16 +101,10 @@ export function initRepoProject() {

for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
const projectHeader = modal.closest('.project-column-header');
const projectTitleLabel = projectHeader?.querySelector('.project-column-title');
const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
const projectTitleInput = modal.querySelector('.project-column-title-input');
const projectColorInput = modal.querySelector('#new_project_column_color');
const boardColumn = modal.closest('.project-column');
const bgColor = boardColumn?.style.backgroundColor;

if (bgColor) {
setLabelColor(projectHeader, rgbToHex(bgColor));
}

modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
e.preventDefault();
try {
Expand All @@ -126,10 +119,8 @@ export function initRepoProject() {
} finally {
projectTitleLabel.textContent = projectTitleInput?.value;
projectTitleInput.closest('form')?.classList.remove('dirty');
if (projectColorInput?.value) {
setLabelColor(projectHeader, projectColorInput.value);
}
boardColumn.style = `background: ${projectColorInput.value} !important`;
boardColumn.style.setProperty('background', projectColorInput.value, 'important');
boardColumn.style.setProperty('color', contrastColor(projectColorInput.value), 'important');
$('.ui.modal').modal('hide');
}
});
Expand Down Expand Up @@ -182,24 +173,3 @@ export function initRepoProject() {
createNewColumn(url, $columnTitle, $projectColorInput);
});
}

function setLabelColor(label, color) {
const {r, g, b} = tinycolor(color).toRgb();
if (useLightTextOnBackground(r, g, b)) {
label.classList.remove('dark-label');
label.classList.add('light-label');
} else {
label.classList.remove('light-label');
label.classList.add('dark-label');
}
}

function rgbToHex(rgb) {
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
}

function hex(x) {
const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
}
40 changes: 24 additions & 16 deletions web_src/js/utils/color.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
// 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;
import tinycolor from 'tinycolor2';

// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
// Keep this in sync with modules/util/color.go
function getRelativeLuminance(color) {
const {r, g, b} = tinycolor(color).toRgb();
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
}

// 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;
function useLightText(backgroundColor) {
return getRelativeLuminance(backgroundColor) < 0.453;
}

// 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;
// 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
export function contrastColor(backgroundColor) {
return useLightText(backgroundColor) ? '#fff' : '#000';
silverwind marked this conversation as resolved.
Show resolved Hide resolved
}

function resolveColors(obj) {
Expand All @@ -33,3 +31,13 @@ export const chartJsColors = resolveColors({
additions: '--color-green',
deletions: '--color-red',
});

function hex(x) {
const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
}

export function rgbToHex(rgb) {
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
}
silverwind marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading