Support minimum contrast ratio in webgl

This commit is contained in:
Daniel Imms
2019-11-11 22:47:26 -08:00
parent 2283ea45a6
commit 531409c2c7
5 changed files with 291 additions and 31 deletions
@@ -16,7 +16,7 @@ let page: puppeteer.Page;
const width = 800;
const height = 600;
describe.only('WebGL Renderer Integration Tests', function(): void {
describe('WebGL Renderer Integration Tests', function(): void {
it('dispose removes renderer canvases', async () => {
await setupBrowser();
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
@@ -562,6 +562,76 @@ describe.only('WebGL Renderer Integration Tests', function(): void {
}
});
});
describe('minimumContrastRatio', async () => {
before(async () => setupBrowser());
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
it('should adjust 0-15 colors on black background', async () => {
const theme: ITheme = {
black: '#2e3436',
red: '#cc0000',
green: '#4e9a06',
yellow: '#c4a000',
blue: '#3465a4',
magenta: '#75507b',
cyan: '#06989a',
white: '#d3d7cf',
brightBlack: '#555753',
brightRed: '#ef2929',
brightGreen: '#8ae234',
brightYellow: '#fce94f',
brightBlue: '#729fcf',
brightMagenta: '#ad7fa8',
brightCyan: '#34e2e2',
brightWhite: '#eeeeec'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
);
// Validate before minimumContrastRatio is applied
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]);
await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]);
await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]);
await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]);
await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]);
await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]);
await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]);
await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]);
await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]);
await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]);
await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]);
await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]);
await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
// Setting and check for minimum contrast values, note that these are note
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
await pollFor(page, () => getCellColor(1, 1), [179, 182, 182, 255]);
await pollFor(page, () => getCellColor(2, 1), [234, 163, 163, 255]);
await pollFor(page, () => getCellColor(3, 1), [196, 221, 174, 255]);
await pollFor(page, () => getCellColor(4, 1), [231, 219, 163, 255]);
await pollFor(page, () => getCellColor(5, 1), [133, 162, 200, 255]);
await pollFor(page, () => getCellColor(6, 1), [180, 159, 182, 255]);
await pollFor(page, () => getCellColor(7, 1), [106, 192, 194, 255]);
await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]);
await pollFor(page, () => getCellColor(1, 2), [180, 181, 179, 255]);
await pollFor(page, () => getCellColor(2, 2), [246, 160, 160, 255]);
await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]);
await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]);
await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]);
await pollFor(page, () => getCellColor(6, 2), [188, 150, 183, 255]);
// Unchanged
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
});
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
@@ -36,6 +36,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
allowTransparency: terminal.getOption('allowTransparency'),
drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
colors: clonedColors
};
}
@@ -54,6 +55,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
a.minimumContrastRatio === b.minimumContrastRatio &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
+1
View File
@@ -26,5 +26,6 @@ export interface ICharAtlasConfig {
scaledCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
@@ -207,7 +207,12 @@ export class WebglCharAtlas implements IDisposable {
}
}
private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse);
if (minimumContrastCss) {
return minimumContrastCss;
}
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
@@ -217,7 +222,7 @@ export class WebglCharAtlas implements IDisposable {
return this._getColorFromAnsiIndex(fgColor).css;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`;
return toCss(arr[0], arr[1], arr[2]);
case Attributes.CM_DEFAULT:
default:
if (inverse) {
@@ -227,6 +232,57 @@ export class WebglCharAtlas implements IDisposable {
}
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor).rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
this._config.colors.foreground.rgba;
}
return this._config.colors.background.rgba;
}
}
private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(fgColor).rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
this._config.colors.background.rgba;
}
return this._config.colors.foreground.rgba;
}
}
private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): string | undefined {
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse);
const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse);
console.log('ratio', this._config.minimumContrastRatio);
if (this._config.minimumContrastRatio === 1) {
return undefined;
}
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio);
console.log('get min', result, bgRgba, fgRgba);
if (!result) {
return undefined;
}
return toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
}
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
@@ -270,7 +326,7 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top';
this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse, bold);
this._tmpCtx.fillStyle = this._getForegroundCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold);
// Apply alpha to dim the character
if (dim) {
@@ -473,3 +529,120 @@ function getBgColor(bg: number): number {
default: return -1; // CM_DEFAULT defaults to -1
}
}
export function toCss(r: number, g: number, b: number): string {
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;
}
/**
* 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 rgbRelativeLuminance(rgb: number): number {
return rgbRelativeLuminance2(
(rgb >> 16) & 0xFF,
(rgb >> 8 ) & 0xFF,
(rgb ) & 0xFF);
}
export function rgbRelativeLuminance2(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;
}
/**
* 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);
}
function rgbaToColor(r: number, g: number, b: number): IColor {
return {
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
};
}
export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgbRelativeLuminance(bgRgba >> 8);
const fgL = rgbRelativeLuminance(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 ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgbaToColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Increase by 10% (ceil) 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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF;
}
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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(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.floor((255 - fgR) * 0.1));
fgG = Math.min(0xFF, fgG + Math.floor((255 - fgG) * 0.1));
fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF;
}
+40 -27
View File
@@ -86,28 +86,47 @@ export function contrastRatio(l1: number, l2: number): number {
// TODO: Cache [bg][fg]: result, should probably be owned by ColorManager?
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const bgL = rgbRelativeLuminance(bg.rgba >> 8);
const fgL = rgbRelativeLuminance(fg.rgba >> 8);
function rgbaToColor(r: number, g: number, b: number): IColor {
return {
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
};
}
export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgbRelativeLuminance(bgRgba >> 8);
const fgL = rgbRelativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bg, fg, ratio);
return reduceLuminance(bgRgba, fgRgba, ratio);
}
return increaseLuminance(bg, fg, ratio);
return increaseLuminance(bgRgba, fgRgba, ratio);
}
return undefined;
}
export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor {
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgbaToColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
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 = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
let fgR = (fg.rgba >> 24) & 0xFF;
let fgG = (fg.rgba >> 16) & 0xFF;
let fgB = (fg.rgba >> 8) & 0xFF;
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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Increase by 10% (ceil) until the ratio is hit
@@ -116,21 +135,18 @@ export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor {
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return {
css: toCss(fgR, fgG, fgB),
rgba: toRgba(fgR, fgG, fgB)
};
return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF;
}
export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor {
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 = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
let fgR = (fg.rgba >> 24) & 0xFF;
let fgG = (fg.rgba >> 16) & 0xFF;
let fgB = (fg.rgba >> 8) & 0xFF;
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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
// Increase by 10% until the ratio is hit
@@ -139,8 +155,5 @@ export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor
fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return {
css: toCss(fgR, fgG, fgB),
rgba: toRgba(fgR, fgG, fgB)
};
return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF;
}