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

Modify luminance calculation and extract related functions into single files #24586

Merged
merged 48 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
17ecae7
change lightness calculation function
HesterG May 5, 2023
8ab875e
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 5, 2023
ad2b5e7
save changes
HesterG May 5, 2023
22c507f
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 5, 2023
d2ffc63
add some tests
HesterG May 5, 2023
40ef2e2
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 5, 2023
29c5a8d
add more tests
HesterG May 5, 2023
37933b4
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 5, 2023
4d3ed67
use same logic as github and modify tests
HesterG May 5, 2023
75f1f66
remove unused
HesterG May 5, 2023
5f1aa4d
fix
HesterG May 5, 2023
1efcd07
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 6, 2023
c1fd625
modify tests
HesterG May 6, 2023
b6aab0c
save changes and add tests
HesterG May 6, 2023
d437286
save changes
HesterG May 6, 2023
dc60ea7
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 6, 2023
550dde9
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 8, 2023
21fa83b
modify tests
HesterG May 8, 2023
61e7d42
rename function
HesterG May 8, 2023
51f5419
adjust functions
HesterG May 8, 2023
690d6d4
rename functions
HesterG May 8, 2023
9433b8d
adjust functions and tests
HesterG May 8, 2023
cfb0f00
fix lint
HesterG May 8, 2023
9bc8c9f
comment
HesterG May 8, 2023
ed4c615
Merge branch 'main' into modify-lightness-calculation
HesterG May 8, 2023
1182b08
Merge branch 'main' into modify-lightness-calculation
HesterG May 8, 2023
8394fa9
Merge branch 'go-gitea:main' into modify-lightness-calculation
HesterG May 9, 2023
8dce9d4
save change for js
HesterG May 9, 2023
6f109da
modify backend
HesterG May 9, 2023
1b9395a
fix lint
HesterG May 9, 2023
1c337dd
add commments
HesterG May 9, 2023
d427c9d
Merge branch 'main' into modify-lightness-calculation
HesterG May 9, 2023
745eb19
update hexstring
HesterG May 9, 2023
cb6790e
remove err return for HexToRBGColor and fix lint
HesterG May 9, 2023
9dd72c4
fix lint
HesterG May 9, 2023
520931a
try improve regexp
HesterG May 9, 2023
efa389b
fix lint
HesterG May 9, 2023
87581ce
update re
HesterG May 9, 2023
524bec8
Merge branch 'main' into modify-lightness-calculation
HesterG May 9, 2023
1c27944
Update web_src/js/utils/color.js
HesterG May 10, 2023
bebecd7
Update web_src/js/utils/color.js
HesterG May 10, 2023
9757085
Update web_src/js/utils/color.js
HesterG May 10, 2023
32e381e
Merge branch 'main' into modify-lightness-calculation
HesterG May 10, 2023
c13f8c7
unify frontend and backend
HesterG May 10, 2023
02d7036
remove print
HesterG May 10, 2023
5ae6973
Merge branch 'main' into modify-lightness-calculation
GiteaBot May 10, 2023
ac49366
Merge branch 'main' into modify-lightness-calculation
GiteaBot May 10, 2023
0a26397
Merge branch 'main' into modify-lightness-calculation
GiteaBot May 10, 2023
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
27 changes: 0 additions & 27 deletions models/issues/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,33 +159,6 @@ func (l *Label) BelongsToRepo() bool {
return l.RepoID > 0
}

// Get color as RGB values in 0..255 range
func (l *Label) ColorRGB() (float64, float64, float64, error) {
color, err := strconv.ParseUint(l.Color[1:], 16, 64)
if err != nil {
return 0, 0, 0, err
}

r := float64(uint8(0xFF & (uint32(color) >> 16)))
g := float64(uint8(0xFF & (uint32(color) >> 8)))
b := float64(uint8(0xFF & uint32(color)))
return r, g, b, nil
}

// Determine if label text should be light or dark to be readable on background color
func (l *Label) UseLightTextColor() bool {
if strings.HasPrefix(l.Color, "#") {
if r, g, b, err := l.ColorRGB(); err == nil {
// Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast
// In the future WCAG 3 APCA may be a better solution
brightness := (0.299*r + 0.587*g + 0.114*b) / 255
return brightness < 0.35
}
}

return false
}

// Return scope substring of label name, or empty string if none exists
func (l *Label) ExclusiveScope() string {
if !l.Exclusive {
Expand Down
9 changes: 0 additions & 9 deletions models/issues/label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,6 @@ func TestLabel_CalOpenIssues(t *testing.T) {
assert.EqualValues(t, 2, label.NumOpenIssues)
}

func TestLabel_TextColor(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
assert.False(t, label.UseLightTextColor())

label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
assert.True(t, label.UseLightTextColor())
}

func TestLabel_ExclusiveScope(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
Expand Down
57 changes: 28 additions & 29 deletions modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)

// RenderCommitMessage renders commit message with XSS-safe and special links.
Expand Down Expand Up @@ -133,7 +134,9 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
labelScope := label.ExclusiveScope()

textColor := "#111"
if label.UseLightTextColor() {
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"
}

Expand All @@ -150,34 +153,30 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
scopeText := RenderEmoji(ctx, labelScope)
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])

itemColor := label.Color
scopeColor := label.Color
if r, g, b, err := label.ColorRGB(); err == nil {
// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
luminance := (0.299*r + 0.587*g + 0.114*b) / 255
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)
lighten := contrast + math.Max(contrast-luminance, 0.0)
// Compute factor to keep RGB values proportional.
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)

scopeBytes := []byte{
uint8(math.Min(math.Round(r*darkenFactor), 255)),
uint8(math.Min(math.Round(g*darkenFactor), 255)),
uint8(math.Min(math.Round(b*darkenFactor), 255)),
}
itemBytes := []byte{
uint8(math.Min(math.Round(r*lightenFactor), 255)),
uint8(math.Min(math.Round(g*lightenFactor), 255)),
uint8(math.Min(math.Round(b*lightenFactor), 255)),
}

itemColor = "#" + hex.EncodeToString(itemBytes)
scopeColor = "#" + hex.EncodeToString(scopeBytes)
}
// 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)
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)
lighten := contrast + math.Max(contrast-luminance, 0.0)
// Compute factor to keep RGB values proportional.
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)

scopeBytes := []byte{
uint8(math.Min(math.Round(r*darkenFactor), 255)),
uint8(math.Min(math.Round(g*darkenFactor), 255)),
uint8(math.Min(math.Round(b*darkenFactor), 255)),
}
itemBytes := []byte{
uint8(math.Min(math.Round(r*lightenFactor), 255)),
uint8(math.Min(math.Round(g*lightenFactor), 255)),
uint8(math.Min(math.Round(b*lightenFactor), 255)),
}

itemColor := "#" + hex.EncodeToString(itemBytes)
scopeColor := "#" + hex.EncodeToString(scopeBytes)

s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
Expand Down
65 changes: 65 additions & 0 deletions modules/util/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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
if strings.HasPrefix(colorString, "#") {
hexString = colorString[1:]
}
// only support transfer of rgb, rgba, rrggbb and rrggbbaa
// if not in these formats, use default values 0, 0, 0
if len(hexString) != 3 && len(hexString) != 4 && len(hexString) != 6 && len(hexString) != 8 {
return 0, 0, 0
}
if len(hexString) == 3 || len(hexString) == 4 {
hexString = fmt.Sprintf("%c%c%c%c%c%c", hexString[0], hexString[0], hexString[1], hexString[1], hexString[2], hexString[2])
}
if len(hexString) == 8 {
hexString = hexString[0:6]
}
color, err := strconv.ParseUint(hexString, 16, 64)
if err != nil {
return 0, 0, 0
}
r := float64(uint8(0xFF & (uint32(color) >> 16)))
g := float64(uint8(0xFF & (uint32(color) >> 8)))
b := float64(uint8(0xFF & uint32(color)))
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
}

// 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
}
65 changes: 65 additions & 0 deletions modules/util/color_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_HexToRBGColor(t *testing.T) {
cases := []struct {
colorString string
expectedR float64
expectedG float64
expectedB float64
}{
{"2b8685", 43, 134, 133},
{"1e1", 17, 238, 17},
{"#1e1", 17, 238, 17},
{"1e16", 17, 238, 17},
{"3bb6b3", 59, 182, 179},
{"#3bb6b399", 59, 182, 179},
{"#0", 0, 0, 0},
{"#00000", 0, 0, 0},
{"#1234567", 0, 0, 0},
}
for n, c := range cases {
r, g, b := HexToRBGColor(c.colorString)
assert.Equal(t, c.expectedR, r, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r)
assert.Equal(t, c.expectedG, g, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g)
assert.Equal(t, c.expectedB, b, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b)
}
}

func Test_UseLightTextOnBackground(t *testing.T) {
cases := []struct {
r float64
g float64
b float64
expected bool
}{
{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},
}
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)
}
}
5 changes: 3 additions & 2 deletions web_src/js/components/ContextPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<script>
import $ from 'jquery';
import {SvgIcon} from '../svg.js';
import {useLightTextOnBackground} from '../utils.js';
import {useLightTextOnBackground, hexToRGBColor} from '../utils/color.js';

const {appSubUrl, i18n} = window.config;

Expand Down Expand Up @@ -77,7 +77,8 @@ export default {
labels() {
return this.issue.labels.map((label) => {
let textColor;
if (useLightTextOnBackground(label.color)) {
const [r, g, b] = hexToRGBColor(label.color);
if (useLightTextOnBackground(r, g, b)) {
textColor = '#eeeeee';
} else {
textColor = '#111111';
Expand Down
5 changes: 3 additions & 2 deletions web_src/js/features/repo-projects.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import $ from 'jquery';
import {useLightTextOnBackground} from '../utils.js';
import {useLightTextOnBackground, hexToRGBColor} from '../utils/color.js';

const {csrfToken} = window.config;

Expand Down Expand Up @@ -190,7 +190,8 @@ export function initRepoProject() {
}

function setLabelColor(label, color) {
if (useLightTextOnBackground(color)) {
const [r, g, b] = hexToRGBColor(color);
if (useLightTextOnBackground(r, g, b)) {
label.removeClass('dark-label').addClass('light-label');
} else {
label.removeClass('light-label').addClass('dark-label');
Expand Down
14 changes: 0 additions & 14 deletions web_src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,3 @@ export function toAbsoluteUrl(url) {
return `${window.location.origin}${url}`;
}

// determine if light or dark text color should be used on a given background color
// NOTE: see models/issue_label.go for similar implementation
export function useLightTextOnBackground(backgroundColor) {
if (backgroundColor[0] === '#') {
backgroundColor = backgroundColor.substring(1);
}
// Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast
// In the future WCAG 3 APCA may be a better solution.
const r = parseInt(backgroundColor.substring(0, 2), 16);
const g = parseInt(backgroundColor.substring(2, 4), 16);
const b = parseInt(backgroundColor.substring(4, 6), 16);
const brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return brightness < 0.35;
}
42 changes: 42 additions & 0 deletions web_src/js/utils/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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;
}

// Get color as RGB values in 0..255 range from the hex color string (with or without #)
export function hexToRGBColor(backgroundColorStr) {
let backgroundColor = backgroundColorStr;
if (backgroundColorStr[0] === '#') {
backgroundColor = backgroundColorStr.substring(1);
}
// only support transfer of rgb, rgba, rrggbb and rrggbbaa
// if not in these formats, use default values 0, 0, 0
if (![3, 4, 6, 8].includes(backgroundColor.length)) {
return [0, 0, 0];
}
if ([3, 4].includes(backgroundColor.length)) {
const [r, g, b] = backgroundColor;
backgroundColor = `${r}${r}${g}${g}${b}${b}`;
}
const r = parseInt(backgroundColor.substring(0, 2), 16);
const g = parseInt(backgroundColor.substring(2, 4), 16);
const b = parseInt(backgroundColor.substring(4, 6), 16);
return [r, g, b];
HesterG marked this conversation as resolved.
Show resolved Hide resolved
}

// 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;
}
34 changes: 34 additions & 0 deletions web_src/js/utils/color.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {test, expect} from 'vitest';
import {hexToRGBColor, useLightTextOnBackground} from './color.js';

test('hexToRGBColor', () => {
expect(hexToRGBColor('2b8685')).toEqual([43, 134, 133]);
expect(hexToRGBColor('1e1')).toEqual([17, 238, 17]);
expect(hexToRGBColor('#1e1')).toEqual([17, 238, 17]);
expect(hexToRGBColor('1e16')).toEqual([17, 238, 17]);
expect(hexToRGBColor('3bb6b3')).toEqual([59, 182, 179]);
expect(hexToRGBColor('#3bb6b399')).toEqual([59, 182, 179]);
expect(hexToRGBColor('#0')).toEqual([0, 0, 0]);
expect(hexToRGBColor('#00000')).toEqual([0, 0, 0]);
expect(hexToRGBColor('#1234567')).toEqual([0, 0, 0]);
});

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);
});