blob: c9054e89add00362a809e389d845ac6e7d1b4b02 [file] [log] [blame]
// 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 sRGBtrc = 2.218;
const normBgExp = 0.38;
const normFgExp = 0.43;
const revBgExp = 0.5;
const revFgExp = 0.43;
const blkThrs = 0.02;
const blkClmp = 1.33;
const scaleBoW = 161.8;
const scaleWoB = 161.8;
/**
* 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, sRGBtrc);
const g = Math.pow(gSRGB, sRGBtrc);
const b = Math.pow(bSRGB, sRGBtrc);
return 0.2126 * r + 0.7152 * g + 0.0722 * 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} fgLuminance
* @param {number} bgLuminance
*/
export function contrastRatioByLuminanceAPCA(fgLuminance, bgLuminance) {
if (bgLuminance >= fgLuminance) { // Black text on white.
fgLuminance =
(fgLuminance > blkThrs) ? fgLuminance : fgLuminance + Math.pow(Math.abs(fgLuminance - blkThrs), blkClmp);
return (Math.pow(bgLuminance, normBgExp) - Math.pow(fgLuminance, normFgExp)) * scaleBoW;
}
// White text on black.
bgLuminance =
(bgLuminance > blkThrs) ? bgLuminance : (bgLuminance + Math.pow(Math.abs(bgLuminance - blkThrs), blkClmp));
return (Math.pow(bgLuminance, revBgExp) - Math.pow(fgLuminance, revFgExp)) * scaleWoB;
}
/**
* 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) {
function computeLuminance() {
if (!lighter) { // Black text on white.
return Math.pow(Math.pow(luminance, normBgExp) - contrast / scaleBoW, 1 / normFgExp);
}
// White text on black.
luminance = (luminance > blkThrs) ? luminance : (luminance + Math.pow(Math.abs(luminance - blkThrs), blkClmp));
return Math.pow(contrast / scaleWoB + Math.pow(luminance, revBgExp), 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, 120, 100, 90, 80, 80, 80],
[14, -1, -1, -1, 100, 90, 80, 75, 75, 75],
[16, -1, -1, 120, 90, 80, 75, 70, 70, 70],
[18, -1, -1, 110, 80, 75, 70, 67, 65, 65],
[20, -1, -1, 100, 78, 72, 67, 65, 60, 60],
[22, -1, -1, 90, 77, 71, 65, 62, 57, 57],
[24, -1, 120, 80, 75, 70, 65, 60, 55, 55],
[26, -1, 110, 79, 72, 67, 62, 59, 54, 54],
[28, -1, 100, 77, 70, 65, 60, 57, 53, 53],
[32, -1, 90, 76, 67, 62, 57, 53, 50, 48],
[36, 120, 80, 75, 65, 60, 55, 50, 48, 48],
[42, 110, 77, 73, 62, 57, 52, 48, 46, 42],
[48, 100, 75, 70, 60, 55, 50, 45, 42, 40],
[60, 90, 73, 65, 57, 52, 46, 42, 40, 40],
[72, 70, 60, 55, 50, 45, 40, 40, 40, 40],
[96, 80, 60, 55, 50, 45, 40, 40, 40, 40],
[120, 60, 55, 50, 47, 43, 40, 40, 40, 40],
];
// 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;
}