mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
Our implementation of the reverse desiredLuminanceAPCA is unreliable and we might need to find a better approach later. Bug: chromium:1170879 Change-Id: I51bfa6931e503cd7d6116b69e7b659365a50123f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2678089 Reviewed-by: Mathias Bynens <mathias@chromium.org> Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
275 lines
8.4 KiB
JavaScript
275 lines
8.4 KiB
JavaScript
// Copyright 2020 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
/**
|
|
* Combine the two given colors according to alpha blending.
|
|
* @param {!Array<number>} fgRGBA
|
|
* @param {!Array<number>} bgRGBA
|
|
* @return {!Array<number>}
|
|
*/
|
|
export function blendColors(fgRGBA, bgRGBA) {
|
|
const alpha = fgRGBA[3];
|
|
return [
|
|
((1 - alpha) * bgRGBA[0]) + (alpha * fgRGBA[0]),
|
|
((1 - alpha) * bgRGBA[1]) + (alpha * fgRGBA[1]),
|
|
((1 - alpha) * bgRGBA[2]) + (alpha * fgRGBA[2]),
|
|
alpha + (bgRGBA[3] * (1 - alpha)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<number>} rgba
|
|
* @return {!Array<number>}
|
|
*/
|
|
export function rgbaToHsla([r, g, b, a]) {
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const diff = max - min;
|
|
const sum = max + min;
|
|
|
|
let h;
|
|
if (min === max) {
|
|
h = 0;
|
|
} else if (r === max) {
|
|
h = ((1 / 6 * (g - b) / diff) + 1) % 1;
|
|
} else if (g === max) {
|
|
h = (1 / 6 * (b - r) / diff) + 1 / 3;
|
|
} else {
|
|
h = (1 / 6 * (r - g) / diff) + 2 / 3;
|
|
}
|
|
|
|
const l = 0.5 * sum;
|
|
|
|
let s;
|
|
if (l === 0) {
|
|
s = 0;
|
|
} else if (l === 1) {
|
|
s = 0;
|
|
} else if (l <= 0.5) {
|
|
s = diff / sum;
|
|
} else {
|
|
s = diff / (2 - sum);
|
|
}
|
|
|
|
return [h, s, l, a];
|
|
}
|
|
|
|
/**
|
|
* Calculate the luminance of this color using the WCAG algorithm.
|
|
* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
|
* @param {!Array<number>} rgba
|
|
* @return {number}
|
|
*/
|
|
export function luminance([rSRGB, gSRGB, bSRGB]) {
|
|
const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055) / 1.055), 2.4);
|
|
const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055) / 1.055), 2.4);
|
|
const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055) / 1.055), 2.4);
|
|
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
}
|
|
|
|
/**
|
|
* Calculate the contrast ratio between a foreground and a background color.
|
|
* Returns the ratio to 1, for example for two two colors with a contrast ratio of 21:1, this function will return 21.
|
|
* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
|
* @param {!Array<number>} fgRGBA
|
|
* @param {!Array<number>} bgRGBA
|
|
* @return {number}
|
|
*/
|
|
export function contrastRatio(fgRGBA, bgRGBA) {
|
|
const blendedFg = blendColors(fgRGBA, bgRGBA);
|
|
const fgLuminance = luminance(blendedFg);
|
|
const bgLuminance = luminance(bgRGBA);
|
|
const contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05);
|
|
return contrastRatio;
|
|
}
|
|
|
|
// Constants for basic APCA version.
|
|
// See https://github.com/Myndex/SAPC-APCA
|
|
const mainTRC = 2.4;
|
|
const normBgExp = 0.55;
|
|
const normFgExp = 0.58;
|
|
const revBgExp = 0.62;
|
|
const revFgExp = 0.57;
|
|
const blkThrs = 0.03;
|
|
const blkClmp = 1.45;
|
|
const scaleBoW = 1.25;
|
|
const scaleWoB = 1.25;
|
|
const deltaLuminanceMin = 0.0005;
|
|
const loConThresh = 0.078;
|
|
const loConFactor = 12.82051282051282;
|
|
const loConOffset = 0.06;
|
|
const loClip = 0.001;
|
|
|
|
/**
|
|
* Calculate relative luminance of a color.
|
|
* See https://github.com/Myndex/SAPC-APCA
|
|
* @param {!Array<number>} rgba
|
|
* @return {number}
|
|
*/
|
|
export function luminanceAPCA([rSRGB, gSRGB, bSRGB]) {
|
|
const r = Math.pow(rSRGB, mainTRC);
|
|
const g = Math.pow(gSRGB, mainTRC);
|
|
const b = Math.pow(bSRGB, mainTRC);
|
|
|
|
return 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
|
|
}
|
|
|
|
/**
|
|
* Calculate the contrast ratio between a foreground and a background color.
|
|
* Returns the percentage of the predicted visual contrast.
|
|
* See https://github.com/Myndex/SAPC-APCA
|
|
*
|
|
* @param {!Array<number>} fgRGBA
|
|
* @param {!Array<number>} bgRGBA
|
|
* @return {number}
|
|
*/
|
|
export function contrastRatioAPCA(fgRGBA, bgRGBA) {
|
|
return contrastRatioByLuminanceAPCA(luminanceAPCA(fgRGBA), luminanceAPCA(bgRGBA));
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
*/
|
|
function clampLuminance(value) {
|
|
return value > blkThrs ? value : (value + Math.pow(blkThrs - value, blkClmp));
|
|
}
|
|
|
|
/**
|
|
* @param {number} fgLuminance
|
|
* @param {number} bgLuminance
|
|
*/
|
|
export function contrastRatioByLuminanceAPCA(fgLuminance, bgLuminance) {
|
|
fgLuminance = clampLuminance(fgLuminance);
|
|
bgLuminance = clampLuminance(bgLuminance);
|
|
if (Math.abs(fgLuminance - bgLuminance) < deltaLuminanceMin) {
|
|
return 0;
|
|
}
|
|
let result = 0;
|
|
if (bgLuminance >= fgLuminance) { // Black text on white.
|
|
result = (Math.pow(bgLuminance, normBgExp) - Math.pow(fgLuminance, normFgExp)) * scaleBoW;
|
|
result = result < loClip ?
|
|
0 :
|
|
(result < loConThresh ? result - result * loConFactor * loConOffset : result - loConOffset);
|
|
} else {
|
|
// White text on black.
|
|
result = (Math.pow(bgLuminance, revBgExp) - Math.pow(fgLuminance, revFgExp)) * scaleWoB;
|
|
result = result > -loClip ?
|
|
0 :
|
|
(result > -loConThresh ? result - result * loConFactor * loConOffset : result + loConOffset);
|
|
}
|
|
return result * 100;
|
|
}
|
|
|
|
/**
|
|
* Compute a desired luminance given a given luminance and a desired contrast
|
|
* percentage according to APCA.
|
|
* @param {number} luminance The given luminance.
|
|
* @param {number} contrast The desired contrast percentage.
|
|
* @param {boolean} lighter Whether the desired luminance is lighter or darker
|
|
* than the given luminance. If no luminance can be found which meets this
|
|
* requirement, a luminance which meets the inverse requirement will be
|
|
* returned.
|
|
* @return {number} The desired luminance.
|
|
*/
|
|
export function desiredLuminanceAPCA(luminance, contrast, lighter) {
|
|
luminance = clampLuminance(luminance);
|
|
contrast /= 100;
|
|
function computeLuminance() {
|
|
if (!lighter) { // Black text on white.
|
|
return Math.pow(Math.abs(Math.pow(luminance, normBgExp) - (contrast + loConOffset) / scaleBoW), 1 / normFgExp);
|
|
}
|
|
// White text on black.
|
|
return Math.pow(Math.abs(Math.pow(luminance, revBgExp) - (-contrast - loConOffset) / scaleWoB), 1 / revFgExp);
|
|
}
|
|
let desiredLuminance = computeLuminance();
|
|
if (desiredLuminance < 0 || desiredLuminance > 1) {
|
|
lighter = !lighter;
|
|
desiredLuminance = computeLuminance();
|
|
}
|
|
return desiredLuminance;
|
|
}
|
|
|
|
// clang-format off
|
|
const contrastAPCALookupTable = [
|
|
// See https://github.com/Myndex/SAPC-APCA
|
|
// font size in px | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 weights
|
|
[12, -1, -1, -1, -1, 100, 90, 80, -1, -1],
|
|
[14, -1, -1, -1, 100, 90, 80, 60, 60, -1],
|
|
[16, -1, -1, 100, 90, 80, 60, 55, 50, 50],
|
|
[18, -1, -1, 90, 80, 60, 55, 50, 40, 40],
|
|
[24, -1, 100, 80, 60, 55, 50, 40, 38, 35],
|
|
[30, -1, 90, 70, 55, 50, 40, 38, 35, 40],
|
|
[36, -1, 80, 60, 50, 40, 38, 35, 30, 25],
|
|
[48, 100, 70, 55, 40, 38, 35, 30, 25, 20],
|
|
[60, 90, 60, 50, 38, 35, 30, 25, 20, 20],
|
|
[72, 80, 55, 40, 35, 30, 25, 20, 20, 20],
|
|
[96, 70, 50, 35, 30, 25, 20, 20, 20, 20],
|
|
[120, 60, 40, 30, 25, 20, 20, 20, 20, 20],
|
|
];
|
|
// clang-format on
|
|
|
|
contrastAPCALookupTable.reverse();
|
|
|
|
/**
|
|
* @param {string} fontSize
|
|
* @param {string} fontWeight
|
|
* @return {?number}
|
|
*/
|
|
export function getAPCAThreshold(fontSize, fontWeight) {
|
|
const size = parseFloat(fontSize.replace('px', ''));
|
|
const weight = parseFloat(fontWeight);
|
|
|
|
// Go over the table backwards to find the first matching font size and then the weight.
|
|
// Fonts larger than 96px, use the thresholds for 96px.
|
|
// Fonts smaller than 12px, don't get any threshold meaning the font size needs to be increased.
|
|
for (const [rowSize, ...rowWeights] of contrastAPCALookupTable) {
|
|
if (size >= rowSize) {
|
|
for (const [idx, keywordWeight] of [900, 800, 700, 600, 500, 400, 300, 200, 100].entries()) {
|
|
if (weight >= keywordWeight) {
|
|
const threshold = rowWeights[rowWeights.length - 1 - idx];
|
|
return threshold === -1 ? null : threshold;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {string} fontSize
|
|
* @param {string} fontWeight
|
|
* @return {boolean}
|
|
*/
|
|
export function isLargeFont(fontSize, fontWeight) {
|
|
const boldWeights = ['bold', 'bolder', '600', '700', '800', '900'];
|
|
|
|
const fontSizePx = parseFloat(fontSize.replace('px', ''));
|
|
const isBold = (boldWeights.indexOf(fontWeight) !== -1);
|
|
|
|
const fontSizePt = fontSizePx * 72 / 96;
|
|
if (isBold) {
|
|
return fontSizePt >= 14;
|
|
}
|
|
return fontSizePt >= 18;
|
|
}
|
|
|
|
const contrastThresholds = {
|
|
largeFont: {aa: 3.0, aaa: 4.5},
|
|
normalFont: {aa: 4.5, aaa: 7.0}
|
|
};
|
|
|
|
/**
|
|
* @param {string} fontSize
|
|
* @param {string} fontWeight
|
|
* @return {!{aa: number, aaa: number}}
|
|
*/
|
|
export function getContrastThreshold(fontSize, fontWeight) {
|
|
if (isLargeFont(fontSize, fontWeight)) {
|
|
return contrastThresholds.largeFont;
|
|
}
|
|
return contrastThresholds.normalFont;
|
|
}
|