[PORT] gitea##30237: Fix and rewrite contrast color calculation, fix project-related bugs

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="00b39e15-a377-4458-95cf-ceec74b78228"><img
width="90" alt="Screenshot 2024-04-02 at 01 51 30"
src="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.

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>

---
Conflict resolution: Trivial.
(cherry picked from commit 36887ed3921d03f1864360c95bd2ecf853bfbe72)
This commit is contained in:
silverwind 2024-04-07 18:19:25 +02:00 committed by Gusted
parent 6f4827b3e0
commit f6c0c39f1a
No known key found for this signature in database
GPG key ID: FD821B732837125F
14 changed files with 136 additions and 195 deletions

View file

@ -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';
}
function resolveColors(obj) {

View file

@ -1,21 +1,22 @@
import {useLightTextOnBackground} from './color.js';
import {contrastColor} from './color.js';
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);
test('contrastColor', () => {
expect(contrastColor('#d73a4a')).toBe('#fff');
expect(contrastColor('#0075ca')).toBe('#fff');
expect(contrastColor('#cfd3d7')).toBe('#000');
expect(contrastColor('#a2eeef')).toBe('#000');
expect(contrastColor('#7057ff')).toBe('#fff');
expect(contrastColor('#008672')).toBe('#fff');
expect(contrastColor('#e4e669')).toBe('#000');
expect(contrastColor('#d876e3')).toBe('#000');
expect(contrastColor('#ffffff')).toBe('#000');
expect(contrastColor('#2b8684')).toBe('#fff');
expect(contrastColor('#2b8786')).toBe('#fff');
expect(contrastColor('#2c8786')).toBe('#000');
expect(contrastColor('#3bb6b3')).toBe('#000');
expect(contrastColor('#7c7268')).toBe('#fff');
expect(contrastColor('#7e716c')).toBe('#fff');
expect(contrastColor('#81706d')).toBe('#fff');
expect(contrastColor('#807070')).toBe('#fff');
expect(contrastColor('#84b6eb')).toBe('#000');
});