207 lines
6.9 KiB
TypeScript
207 lines
6.9 KiB
TypeScript
/**
|
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { IColor } from 'browser/Types';
|
|
|
|
/**
|
|
* Helper functions where the source type is "channels" (individual color channels as numbers).
|
|
*/
|
|
export namespace channels {
|
|
export function toCss(r: number, g: number, b: number, a?: number): string {
|
|
if (a !== undefined) {
|
|
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
|
|
}
|
|
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
|
|
}
|
|
|
|
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
|
|
// >>> 0 forces an unsigned int
|
|
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper functions where the source type is `IColor`.
|
|
*/
|
|
export namespace color {
|
|
export function blend(bg: IColor, fg: IColor): IColor {
|
|
const a = (fg.rgba & 0xFF) / 255;
|
|
if (a === 1) {
|
|
return {
|
|
css: fg.css,
|
|
rgba: fg.rgba
|
|
};
|
|
}
|
|
const fgR = (fg.rgba >> 24) & 0xFF;
|
|
const fgG = (fg.rgba >> 16) & 0xFF;
|
|
const fgB = (fg.rgba >> 8) & 0xFF;
|
|
const bgR = (bg.rgba >> 24) & 0xFF;
|
|
const bgG = (bg.rgba >> 16) & 0xFF;
|
|
const bgB = (bg.rgba >> 8) & 0xFF;
|
|
const r = bgR + Math.round((fgR - bgR) * a);
|
|
const g = bgG + Math.round((fgG - bgG) * a);
|
|
const b = bgB + Math.round((fgB - bgB) * a);
|
|
const css = channels.toCss(r, g, b);
|
|
const rgba = channels.toRgba(r, g, b);
|
|
return { css, rgba };
|
|
}
|
|
|
|
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
|
|
const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
return rgba.toColor(
|
|
(result >> 24 & 0xFF),
|
|
(result >> 16 & 0xFF),
|
|
(result >> 8 & 0xFF)
|
|
);
|
|
}
|
|
|
|
export function opaque(color: IColor): IColor {
|
|
const rgbaColor = (color.rgba | 0xFF) >>> 0;
|
|
const [r, g, b] = rgba.toChannels(rgbaColor);
|
|
return {
|
|
css: channels.toCss(r, g, b),
|
|
rgba: rgbaColor
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
|
|
*/
|
|
export namespace css {
|
|
export function toColor(css: string): IColor {
|
|
return {
|
|
css,
|
|
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper functions where the source type is "rgb" (number: 0xrrggbb).
|
|
*/
|
|
export namespace rgb {
|
|
/**
|
|
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
|
|
* between two colors.
|
|
* @param rgb The color to use.
|
|
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
|
*/
|
|
export function relativeLuminance(rgb: number): number {
|
|
return relativeLuminance2(
|
|
(rgb >> 16) & 0xFF,
|
|
(rgb >> 8 ) & 0xFF,
|
|
(rgb ) & 0xFF);
|
|
}
|
|
|
|
/**
|
|
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
|
|
* between two colors.
|
|
* @param r The red channel (0x00 to 0xFF).
|
|
* @param g The green channel (0x00 to 0xFF).
|
|
* @param b The blue channel (0x00 to 0xFF).
|
|
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
|
*/
|
|
export function relativeLuminance2(r: number, g: number, b: number): number {
|
|
const rs = r / 255;
|
|
const gs = g / 255;
|
|
const bs = b / 255;
|
|
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
|
|
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
|
|
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
|
|
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
|
|
*/
|
|
export namespace rgba {
|
|
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
|
|
const bgL = rgb.relativeLuminance(bgRgba >> 8);
|
|
const fgL = rgb.relativeLuminance(fgRgba >> 8);
|
|
const cr = contrastRatio(bgL, fgL);
|
|
if (cr < ratio) {
|
|
if (fgL < bgL) {
|
|
return reduceLuminance(bgRgba, fgRgba, ratio);
|
|
}
|
|
return increaseLuminance(bgRgba, fgRgba, ratio);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
|
|
// This is a naive but fast approach to reducing luminance as converting to
|
|
// HSL and back is expensive
|
|
const bgR = (bgRgba >> 24) & 0xFF;
|
|
const bgG = (bgRgba >> 16) & 0xFF;
|
|
const bgB = (bgRgba >> 8) & 0xFF;
|
|
let fgR = (fgRgba >> 24) & 0xFF;
|
|
let fgG = (fgRgba >> 16) & 0xFF;
|
|
let fgB = (fgRgba >> 8) & 0xFF;
|
|
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
|
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
|
|
// Reduce by 10% until the ratio is hit
|
|
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
|
|
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
|
|
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
|
|
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
|
}
|
|
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
|
}
|
|
|
|
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
|
|
// This is a naive but fast approach to increasing luminance as converting to
|
|
// HSL and back is expensive
|
|
const bgR = (bgRgba >> 24) & 0xFF;
|
|
const bgG = (bgRgba >> 16) & 0xFF;
|
|
const bgB = (bgRgba >> 8) & 0xFF;
|
|
let fgR = (fgRgba >> 24) & 0xFF;
|
|
let fgG = (fgRgba >> 16) & 0xFF;
|
|
let fgB = (fgRgba >> 8) & 0xFF;
|
|
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
|
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
|
|
// Increase by 10% until the ratio is hit
|
|
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
|
|
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
|
|
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
|
|
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
|
}
|
|
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
|
}
|
|
|
|
export function toChannels(value: number): [number, number, number, number] {
|
|
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
|
}
|
|
|
|
export function toColor(r: number, g: number, b: number): IColor {
|
|
return {
|
|
css: channels.toCss(r, g, b),
|
|
rgba: channels.toRgba(r, g, b)
|
|
};
|
|
}
|
|
}
|
|
|
|
export function toPaddedHex(c: number): string {
|
|
const s = c.toString(16);
|
|
return s.length < 2 ? '0' + s : s;
|
|
}
|
|
|
|
/**
|
|
* Gets the contrast ratio between two relative luminance values.
|
|
* @param l1 The first relative luminance.
|
|
* @param l2 The first relative luminance.
|
|
* @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
|
*/
|
|
export function contrastRatio(l1: number, l2: number): number {
|
|
if (l1 < l2) {
|
|
return (l2 + 0.05) / (l1 + 0.05);
|
|
}
|
|
return (l1 + 0.05) / (l2 + 0.05);
|
|
}
|