From e46ff9ec852f1ee4f8c076950e445b9c4442c868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:03:12 -0800 Subject: [PATCH 01/34] Add minimumContrastRatio to DOM renderer Part of #322 --- src/browser/Color.test.ts | 24 ++++- src/browser/Color.ts | 99 +++++++++++++++++ src/browser/renderer/dom/DomRenderer.ts | 2 +- .../dom/DomRendererRowFactory.test.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 100 +++++++++++++----- src/common/services/OptionsService.ts | 3 + src/common/services/Services.ts | 1 + typings/xterm.d.ts | 8 ++ 8 files changed, 209 insertions(+), 30 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 44cd52f3..a8e4fa03 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -135,4 +135,26 @@ describe('Color', () => { assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); }); }); + describe('rgbRelativeLuminance', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(rgbRelativeLuminance(0x000000), 0); + + // TODO: Fill in tests + + assert.equal(rgbRelativeLuminance(0xFFFFFF), 1); + }); + }); + describe('contrastRatio', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(contrastRatio(0, 0), 1); + + // TODO: Fill in tests + + assert.equal(contrastRatio(0, 1), 21); + }); + it('should work regardless of the parameter order', () => { + assert.equal(contrastRatio(0, 1), 21); + assert.equal(contrastRatio(1, 0), 21); + }); + }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index a8ce2d16..4063e4c1 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -47,3 +47,102 @@ export function toRgba(r: number, g: number, b: number, a: number = 0xFF): numbe // >>> 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. + * + * // TODO: Is this link right? + * @see https://www.w3.org/TR/WCAG20/#contrastratio + */ +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); +} + +// 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); + const cr = contrastRatio(bgL, fgL); + if (cr < ratio) { + if (fgL < bgL) { + return reduceLuminance(bg, fg, ratio); + } + return increaseLuminance(bg, fg, ratio); + } + return undefined; +} + +export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor { + // 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; + 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 { + css: toCss(fgR, fgG, fgB), + rgba: toRgba(fgR, fgG, fgB) + }; +} + +export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor { + // 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; + 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 { + css: toCss(fgR, fgG, fgB), + rgba: toRgba(fgR, fgG, fgB) + }; +} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index e8fc85f6..23c5a3a1 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -79,7 +79,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._injectCss(); - this._rowFactory = new DomRendererRowFactory(document, this._optionsService); + this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._screenElement.appendChild(this._rowContainer); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b76d644d..bb9d97b6 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -19,7 +19,7 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true })); + rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {} as any); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 3e3b5df0..2c206f33 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -6,9 +6,11 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; +import { rgbRelativeLuminance, fromCss, contrastRatio, ensureContrastRatio } from 'browser/Color'; +import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -24,11 +26,16 @@ export class DomRendererRowFactory { private _workCell: CellData = new CellData(); constructor( - private _document: Document, - private _optionsService: IOptionsService + private readonly _document: Document, + private readonly _optionsService: IOptionsService, + private _colors: IColorSet ) { } + public setColors(colors: IColorSet): void { + this._colors = colors; + } + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); @@ -97,36 +104,75 @@ export class DomRendererRowFactory { charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; - const swapColor = this._workCell.isInverse(); - - // fg - if (this._workCell.isFgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isFgPalette()) { - let fg = this._workCell.getFgColor(); - if (this._workCell.isBold() && fg < 8 && !swapColor && this._optionsService.options.drawBoldTextInBrightColors) { - fg += 8; - } - charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`); - } else if (swapColor) { - charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + let fg = this._workCell.getFgColor(); + let fgColorMode = this._workCell.getFgColorMode(); + let bg = this._workCell.getBgColor(); + let bgColorMode = this._workCell.getBgColorMode(); + const isInverse = !!this._workCell.isInverse(); + if (isInverse) { + const temp = fg; + fg = bg; + bg = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; } - // bg - if (this._workCell.isBgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isBgPalette()) { - charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`); - } else if (swapColor) { - charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + // Foreground + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) { + fg += 8; + } + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) { + charElement.classList.add(`xterm-fg-${fg}`); + } + break; + case Attributes.CM_RGB: + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:rgb(${(AttributeData.toColorRGB(fg)).join(',')});`); + break; + case Attributes.CM_DEFAULT: + default: + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) { + if (isInverse) { + charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + } + } + } + + // Background + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + charElement.classList.add(`xterm-bg-${bg}`); + break; + case Attributes.CM_RGB: + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:rgb(${(AttributeData.toColorRGB(bg)).join(',')});`); + break; + case Attributes.CM_DEFAULT: + default: + if (isInverse) { + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + } } fragment.appendChild(charElement); } return fragment; } + + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean { + if (this._optionsService.options.minimumContrastRatio === 1) { + return false; + } + + const adustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + if (adustedColor) { + element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adustedColor.css}`); + return true; + } + + return false; + } } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index d9ea60d5..9174398e 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,6 +37,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenReaderMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, + minimumContrastRatio: 6, disableStdin: false, allowTransparency: false, tabStopWidth: 8, @@ -116,6 +117,8 @@ export class OptionsService implements IOptionsService { throw new Error(`${key} cannot be less than 1, value: ${value}`); } break; + case 'minimumContrastRatio': + value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); case 'scrollback': value = Math.min(value, 4294967295); if (value < 0) { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e4ff90d0..1a39cced 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -229,6 +229,7 @@ export interface ITerminalOptions { logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; + minimumContrastRatio: number; rendererType: RendererType; rightClickSelectsWord: boolean; rows: number; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9728a72c..b49ed04b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -148,6 +148,14 @@ declare module 'xterm' { */ macOptionClickForcesSelection?: boolean; + /** + * The minimum contrast ratio for text in the terminal, setting this will + * change the foreground color dynamically depending on whether the contrast + * ratio is met. This can be to set 0.1 increments between 1 (default, do + * nothing) and 21 (foreground will be black or white). + */ + minimumContrastRatio?: number; + /** * The type of renderer to use, this allows using the fallback DOM renderer * when canvas is too slow for the environment. The following features do From 9cd66e72bbfac8f06c752fe8c88bda307d49fa9a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:14:35 -0800 Subject: [PATCH 02/34] Add tests for relative luminance --- src/browser/Color.test.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index a8e4fa03..975315c6 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -138,18 +138,28 @@ describe('Color', () => { describe('rgbRelativeLuminance', () => { it('should calculate the relative luminance of the color', () => { assert.equal(rgbRelativeLuminance(0x000000), 0); - - // TODO: Fill in tests - + assert.equal(rgbRelativeLuminance(0x101010).toFixed(4), '0.0052'); + assert.equal(rgbRelativeLuminance(0x202020).toFixed(4), '0.0144'); + assert.equal(rgbRelativeLuminance(0x303030).toFixed(4), '0.0296'); + assert.equal(rgbRelativeLuminance(0x404040).toFixed(4), '0.0513'); + assert.equal(rgbRelativeLuminance(0x505050).toFixed(4), '0.0802'); + assert.equal(rgbRelativeLuminance(0x606060).toFixed(4), '0.1170'); + assert.equal(rgbRelativeLuminance(0x707070).toFixed(4), '0.1620'); + assert.equal(rgbRelativeLuminance(0x808080).toFixed(4), '0.2159'); + assert.equal(rgbRelativeLuminance(0x909090).toFixed(4), '0.2789'); + assert.equal(rgbRelativeLuminance(0xA0A0A0).toFixed(4), '0.3515'); + assert.equal(rgbRelativeLuminance(0xB0B0B0).toFixed(4), '0.4342'); + assert.equal(rgbRelativeLuminance(0xC0C0C0).toFixed(4), '0.5271'); + assert.equal(rgbRelativeLuminance(0xD0D0D0).toFixed(4), '0.6308'); + assert.equal(rgbRelativeLuminance(0xE0E0E0).toFixed(4), '0.7454'); + assert.equal(rgbRelativeLuminance(0xF0F0F0).toFixed(4), '0.8714'); assert.equal(rgbRelativeLuminance(0xFFFFFF), 1); }); }); describe('contrastRatio', () => { it('should calculate the relative luminance of the color', () => { assert.equal(contrastRatio(0, 0), 1); - - // TODO: Fill in tests - + assert.equal(contrastRatio(0, 0.5), 11); assert.equal(contrastRatio(0, 1), 21); }); it('should work regardless of the parameter order', () => { From a6dcb21f1ca8c0cd70437d92a3f2da12fca3ca46 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:30:36 -0800 Subject: [PATCH 03/34] Fix tests --- src/browser/Color.ts | 4 +-- .../dom/DomRendererRowFactory.test.ts | 34 ++++++++++++++++--- .../renderer/dom/DomRendererRowFactory.ts | 2 +- src/common/services/OptionsService.ts | 2 +- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 4063e4c1..c74e8745 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -75,9 +75,7 @@ export function rgbRelativeLuminance2(r: number, g: number, b: number): number { * Gets the contrast ratio between two relative luminance values. * @param l1 The first relative luminance. * @param l2 The first relative luminance. - * - * // TODO: Is this link right? - * @see https://www.w3.org/TR/WCAG20/#contrastratio + * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef */ export function contrastRatio(l1: number, l2: number): number { if (l1 < l2) { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index bb9d97b6..180db0fc 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -11,6 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockOptionsService } from 'common/TestUtils.test'; +import { fromCss } from 'browser/Color'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -19,7 +20,30 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {} as any); + rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), { + background: fromCss('#010101'), + foreground: fromCss('#020202'), + ansi: [ + // dark: + fromCss('#2e3436'), + fromCss('#cc0000'), + fromCss('#4e9a06'), + fromCss('#c4a000'), + fromCss('#3465a4'), + fromCss('#75507b'), + fromCss('#06989a'), + fromCss('#d3d7cf'), + // bright: + fromCss('#555753'), + fromCss('#ef2929'), + fromCss('#8ae234'), + fromCss('#fce94f'), + fromCss('#729fcf'), + fromCss('#ad7fa8'), + fromCss('#34e2e2'), + fromCss('#eeeeec') + ] + } as any); lineData = createEmptyLineData(2); }); @@ -142,7 +166,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -153,7 +177,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -163,7 +187,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -199,7 +223,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 2c206f33..b5caccaf 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -9,7 +9,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; -import { rgbRelativeLuminance, fromCss, contrastRatio, ensureContrastRatio } from 'browser/Color'; +import { ensureContrastRatio } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 9174398e..b0b57694 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,7 +37,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenReaderMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, - minimumContrastRatio: 6, + minimumContrastRatio: 1, disableStdin: false, allowTransparency: false, tabStopWidth: 8, From b1bcb899e0aa478ee49023ede4c346bbc6f20c66 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 21:29:02 -0800 Subject: [PATCH 04/34] Fix inverse in webgl, add tests --- .../src/RectangleRenderer.ts | 26 +- .../src/WebglRenderer.api.ts | 233 +++++++++++++++++- addons/xterm-addon-webgl/src/WebglRenderer.ts | 32 +-- .../src/atlas/WebglCharAtlas.ts | 74 ++++-- 4 files changed, 309 insertions(+), 56 deletions(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 8dcc5c08..356e1747 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -248,23 +248,26 @@ export class RectangleRenderer { let currentStartX = -1; let currentBg = 0; let currentFg = 0; + let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; - if (bg !== currentBg) { + const inverse = !!(fg & FgFlags.INVERSE); + if (bg !== currentBg || ((inverse || currentInverse) && fg !== currentFg)) { // A rectangle needs to be drawn if going from non-default to another color - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y); } currentStartX = x; currentBg = bg; currentFg = fg; + currentInverse = inverse; } } // Finish rectangle if it's still going - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y); } @@ -274,12 +277,21 @@ export class RectangleRenderer { private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { let rgba: number | undefined; - const colorMode = bg & Attributes.CM_MASK; if (fg & FgFlags.INVERSE) { - // Inverted color - rgba = this._colors.foreground.rgba; + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + rgba = (fg & Attributes.RGB_MASK) << 8; + break; + case Attributes.CM_DEFAULT: + default: + rgba = this._colors.foreground.rgba; + } } else { - switch (colorMode) { + switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 304973fe..556061e7 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -16,7 +16,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe('WebGL Renderer Integration Tests', function(): void { +describe.only('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); @@ -76,6 +76,52 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + it('foreground 0-15 bright', async () => { const theme: ITheme = { brightBlack: '#010203', @@ -162,6 +208,46 @@ describe('WebGL Renderer Integration Tests', function(): void { } }); + it('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + it('foreground true color red', async () => { let data = ''; for (let y = 0; y < 16; y++) { @@ -305,6 +391,151 @@ describe('WebGL Renderer Integration Tests', function(): void { } } }); + + it('foreground true color red inverse', async function(): Promise { + this.timeout(60000); + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); }); }); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 5eae61db..3a71dcd8 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -252,35 +252,13 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.lineLengths[y] = x + 1; } - // Resolve bg and fg - let bg = this._workCell.bg; - let fg = this._workCell.fg; - // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) { continue; } - // If inverse flag is on, the foreground should become the background. - if (this._workCell.isInverse()) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - // Apply drawBoldTextInBrightColors - if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { - fg += 8; - } - // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { code = code | COMBINED_CHAR_BIT_MASK; @@ -288,10 +266,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; - this._glyphRenderer.updateCell(x, y, code, bg, fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars); } } this._rectangleRenderer.updateBackgrounds(this._model); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 16b7ba07..b4d3f822 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -176,51 +176,47 @@ export class WebglCharAtlas implements IDisposable { return this._config.colors.ansi[idx]; } - private _getBackgroundColor(bg: number, fg: number): IColor { + private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor { if (this._config.allowTransparency) { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. return TRANSPARENT_COLOR; - } else if (fg & FgFlags.INVERSE) { - return this._config.colors.foreground; } - const colorMode = bg & Attributes.CM_MASK; - switch (colorMode) { + switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(bg & Attributes.PCOLOR_MASK); + return this._getColorFromAnsiIndex(bgColor); case Attributes.CM_RGB: - const rgb = bg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(bgColor); // TODO: This object creation is slow return { - rgba: rgb << 8, + rgba: bgColor << 8, css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}` }; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.foreground; + } return this._config.colors.background; } } - private _getForegroundCss(fg: number): string { - if (fg & FgFlags.INVERSE) { - return this._config.colors.background.css; - } - - const colorMode = fg & Attributes.CM_MASK; - switch (colorMode) { + private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean): string { + switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(fg & Attributes.PCOLOR_MASK).css; + return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: - const rgb = fg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(fgColor); return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.background.css; + } return this._config.colors.foreground.css; } } @@ -233,13 +229,33 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; const bold = !!(fg & FgFlags.BOLD); + const inverse = !!(fg & FgFlags.INVERSE); const dim = !!(bg & BgFlags.DIM); const italic = !!(bg & BgFlags.ITALIC); this._tmpCtx.save(); + let fgColor = getFgColor(fg); + let fgColorMode = fg & Attributes.CM_MASK; + let bgColor = getBgColor(bg); + let bgColorMode = bg & Attributes.CM_MASK; + if (inverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + // TODO: Pass drawBoldTextInBrightColors through + // Apply drawBoldTextInBrightColors + // if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { + // fg += 8; + // } + // draw the background - const backgroundColor = this._getBackgroundColor(bg, fg); + const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of // transparency in backgroundColor this._tmpCtx.globalCompositeOperation = 'copy'; @@ -254,7 +270,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(fg); + this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse); // Apply alpha to dim the character if (dim) { @@ -441,3 +457,19 @@ function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } +function getFgColor(fg: number): number { + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +} +function getBgColor(bg: number): number { + switch (bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +} From db775afd70086be49fe2df9aa429523900ac0d42 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 21:35:49 -0800 Subject: [PATCH 05/34] Support drawBoldTextInBrightColors in webgl again This broke as part of this PR --- .../src/WebglRenderer.api.ts | 27 ++++++++++++++++++- .../src/atlas/CharAtlasUtils.ts | 1 + addons/xterm-addon-webgl/src/atlas/Types.d.ts | 1 + .../src/atlas/WebglCharAtlas.ts | 18 ++++++------- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 556061e7..18449e8a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -53,6 +53,32 @@ describe.only('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-7 drawBoldTextInBrightColors', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('drawBoldTextInBrightColors', true); + `); + await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + it('background 0-15', async () => { const theme: ITheme = { black: '#010203', @@ -393,7 +419,6 @@ describe.only('WebGL Renderer Integration Tests', function(): void { }); it('foreground true color red inverse', async function(): Promise { - this.timeout(60000); let data = ''; for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 03867b41..ec7d9a7e 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -35,6 +35,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number fontWeight: terminal.getOption('fontWeight') as FontWeight, fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, allowTransparency: terminal.getOption('allowTransparency'), + drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'), colors: clonedColors }; } diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index 1de843e0..5b96adb0 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -25,5 +25,6 @@ export interface ICharAtlasConfig { scaledCharWidth: number; scaledCharHeight: number; allowTransparency: boolean; + drawBoldTextInBrightColors: boolean; colors: IColorSet; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b4d3f822..6325ca92 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -68,7 +68,10 @@ export class WebglCharAtlas implements IDisposable { private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; - constructor(document: Document, private _config: ICharAtlasConfig) { + constructor( + document: Document, + private _config: ICharAtlasConfig + ) { this.cacheCanvas = document.createElement('canvas'); this.cacheCanvas.width = TEXTURE_WIDTH; this.cacheCanvas.height = TEXTURE_HEIGHT; @@ -204,10 +207,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean): string { + private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(fgColor); @@ -248,12 +254,6 @@ export class WebglCharAtlas implements IDisposable { bgColorMode = temp2; } - // TODO: Pass drawBoldTextInBrightColors through - // Apply drawBoldTextInBrightColors - // if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { - // fg += 8; - // } - // draw the background const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of @@ -270,7 +270,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); + this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { From 2283ea45a6de0dddacd7a112cdca2b5bd5d694e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:18:39 -0800 Subject: [PATCH 06/34] Refresh rows after ratio changes --- src/Terminal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..b08270fd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -329,6 +329,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'lineHeight': case 'fontWeight': case 'fontWeightBold': + case 'minimumContrastRatio': // When the font changes the size of the cells may change which requires a renderer clear if (this._renderService) { this._renderService.clear(); From 531409c2c7c3dea4b05394e0ec1952bfe443fa2a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:47:26 -0800 Subject: [PATCH 07/34] Support minimum contrast ratio in webgl --- .../src/WebglRenderer.api.ts | 72 ++++++- .../src/atlas/CharAtlasUtils.ts | 3 + addons/xterm-addon-webgl/src/atlas/Types.d.ts | 1 + .../src/atlas/WebglCharAtlas.ts | 179 +++++++++++++++++- src/browser/Color.ts | 67 ++++--- 5 files changed, 291 insertions(+), 31 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 18449e8a..25719faa 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -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 { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index ec7d9a7e..e8e19095 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -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; } diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index 5b96adb0..cd73393c 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -26,5 +26,6 @@ export interface ICharAtlasConfig { scaledCharHeight: number; allowTransparency: boolean; drawBoldTextInBrightColors: boolean; + minimumContrastRatio: number; colors: IColorSet; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 6325ca92..f6691067 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -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; +} diff --git a/src/browser/Color.ts b/src/browser/Color.ts index c74e8745..c63ccee3 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -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; } From 9b139a744b4d4862dda4e66b924210ed7ec4d0c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:55:08 -0800 Subject: [PATCH 08/34] Add test for min contrast on white bg --- .../src/WebglRenderer.api.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 25719faa..998d5536 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -631,6 +631,70 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); }); + + it.only('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + 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), [46, 52, 54, 255]); + await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]); + await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]); + await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]); + await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); + await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); + await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); + await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); + await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]); + await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]); + await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]); + await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]); + await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); + await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); + }); }); }); From 2fc1791dde2bc26e23ada463c45319410016b831 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:11:36 -0800 Subject: [PATCH 09/34] Fix floor/ceil in luminance methods --- .../src/atlas/WebglCharAtlas.ts | 27 ++++++++++--------- src/browser/Color.ts | 14 +++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index f6691067..60068de8 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -208,7 +208,7 @@ export class WebglCharAtlas implements IDisposable { } private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); if (minimumContrastCss) { return minimumContrastCss; } @@ -248,10 +248,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } return this._getColorFromAnsiIndex(fgColor).rgba; case Attributes.CM_RGB: return fgColor << 8; @@ -264,15 +267,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): string | undefined { + private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: 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); + const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); 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; } @@ -618,10 +619,10 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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)); + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.floor(fgR * 0.1)); + fgG -= Math.max(0, Math.floor(fgG * 0.1)); + fgB -= Math.max(0, Math.floor(fgB * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; @@ -639,9 +640,9 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) 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)); + 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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; diff --git a/src/browser/Color.ts b/src/browser/Color.ts index c63ccee3..258ea45a 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -129,10 +129,10 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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)); + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.floor(fgR * 0.1)); + fgG -= Math.max(0, Math.floor(fgG * 0.1)); + fgB -= Math.max(0, Math.floor(fgB * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; @@ -150,9 +150,9 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) 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)); + 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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; From 6626f9628d22ba9e41efc033ac34eff7e7c77dee Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:13:21 -0800 Subject: [PATCH 10/34] Use ceil in both --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 6 +++--- src/browser/Color.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 60068de8..1642ce08 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -620,9 +620,9 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(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.floor(fgR * 0.1)); - fgG -= Math.max(0, Math.floor(fgG * 0.1)); - fgB -= Math.max(0, Math.floor(fgB * 0.1)); + 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; diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 258ea45a..3a2b5032 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -130,9 +130,9 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(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.floor(fgR * 0.1)); - fgG -= Math.max(0, Math.floor(fgG * 0.1)); - fgB -= Math.max(0, Math.floor(fgB * 0.1)); + 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; From 5ee9bcf2b58e2d21291471c31c83cd76adfa9187 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:23:33 -0800 Subject: [PATCH 11/34] Remove .only --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 998d5536..88aa3b45 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -632,7 +632,7 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); }); - it.only('should adjust 0-15 colors on white background', async () => { + it('should adjust 0-15 colors on white background', async () => { const theme: ITheme = { background: '#ffffff', black: '#2e3436', From ca5cee7e8625ec7ed0dfd0bdf642513d32372f6d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 08:53:06 -0800 Subject: [PATCH 12/34] Fix contrast tests --- .../src/WebglRenderer.api.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 88aa3b45..72f65c97 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -8,6 +8,7 @@ import { ITerminalOptions } from '../../../src/Types'; import { ITheme } from 'xterm'; import { assert } from 'chai'; import deepEqual = require('deep-equal'); +import { contrastRatio, reduceLuminance, toRgba } from 'atlas/WebglCharAtlas'; const APP = 'http://127.0.0.1:3000/test'; @@ -613,20 +614,20 @@ describe('WebGL Renderer Integration Tests', function(): void { // 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(1, 1), [176, 180, 180, 255]); + await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); + await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]); + await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]); + await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]); + await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]); + await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 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(1, 2), [183, 185, 183, 255]); + await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 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]); + await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]); // Unchanged await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); @@ -748,6 +749,7 @@ export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => await preFn(); } const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); + console.log(result); if (!deepEqual(result, val)) { return new Promise(r => { setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); From c2474626f0678402e9e572ada2a5921af19a06ce Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:07:08 -0800 Subject: [PATCH 13/34] Clear setting as part of run --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 72f65c97..26e966a0 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -8,7 +8,6 @@ import { ITerminalOptions } from '../../../src/Types'; import { ITheme } from 'xterm'; import { assert } from 'chai'; import deepEqual = require('deep-equal'); -import { contrastRatio, reduceLuminance, toRgba } from 'atlas/WebglCharAtlas'; const APP = 'http://127.0.0.1:3000/test'; @@ -588,7 +587,10 @@ describe('WebGL Renderer Integration Tests', function(): void { brightCyan: '#34e2e2', brightWhite: '#eeeeec' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); 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█` @@ -653,7 +655,10 @@ describe('WebGL Renderer Integration Tests', function(): void { brightCyan: '#34e2e2', brightWhite: '#eeeeec' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); 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█` From faaa07be68849e0a85dc17181900f904d15a1ba9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:33:50 -0800 Subject: [PATCH 14/34] Add contrast caching for DOM renderer --- .../src/atlas/CharAtlasUtils.ts | 3 +- src/Terminal.ts | 1 + src/browser/ColorContrastCache.ts | 38 +++++++++++++++++++ src/browser/ColorManager.ts | 16 +++++++- src/browser/Types.d.ts | 10 +++++ .../renderer/dom/DomRendererRowFactory.ts | 14 +++++-- 6 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/browser/ColorContrastCache.ts diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index e8e19095..ad25bf87 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -24,7 +24,8 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number selectionOpaque: NULL_COLOR, // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. - ansi: colors.ansi.slice() + ansi: colors.ansi.slice(), + contrastCache: {} as any }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/src/Terminal.ts b/src/Terminal.ts index b08270fd..927ff47b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -546,6 +546,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._theme = this.options.theme || this._theme; this.options.theme = undefined; this._colorManager = new ColorManager(document, this.options.allowTransparency); + this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e)); this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts new file mode 100644 index 00000000..3198ad83 --- /dev/null +++ b/src/browser/ColorContrastCache.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor, IColorContrastCache } from 'browser/Types'; + +export class ColorContrastCache implements IColorContrastCache { + private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: number | null | undefined } | undefined } = {}; + + public clear(): void { + this._color = {}; + this._rgba = {}; + } + + public setRgba(bg: number, fg: number, value: number | null): void { + if (!this._rgba[bg]) { + this._rgba[bg] = {}; + } + this._rgba[bg]![fg] = value; + } + + public getRgba(bg: number, fg: number): number | null | undefined { + return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; + } + + public setColor(bg: number, fg: number, value: IColor | null): void { + if (!this._color[bg]) { + this._color[bg] = {}; + } + this._color[bg]![fg] = value; + } + + public getColor(bg: number, fg: number): IColor | null | undefined { + return this._color[bg] ? this._color[bg]![fg] : undefined; + } +} diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index c6354f71..92994cfc 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,9 +3,10 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet } from 'browser/Types'; +import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; import { fromCss, toCss, blend, toRgba } from 'browser/Color'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; const DEFAULT_FOREGROUND = fromCss('#ffffff'); const DEFAULT_BACKGROUND = fromCss('#000000'); @@ -72,6 +73,7 @@ export class ColorManager implements IColorManager { public colors: IColorSet; private _ctx: CanvasRenderingContext2D; private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; constructor(document: Document, public allowTransparency: boolean) { const canvas = document.createElement('canvas'); @@ -84,6 +86,7 @@ export class ColorManager implements IColorManager { this._ctx = ctx; this._ctx.globalCompositeOperation = 'copy'; this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + this._contrastCache = new ColorContrastCache(); this.colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, @@ -91,10 +94,17 @@ export class ColorManager implements IColorManager { cursorAccent: DEFAULT_CURSOR_ACCENT, selection: DEFAULT_SELECTION, selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - ansi: DEFAULT_ANSI_COLORS.slice() + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache }; } + public onOptionsChange(key: string): void { + if (key === 'minimumContrastRatio') { + this._contrastCache.clear(); + } + } + /** * Sets the terminal's theme. * @param theme The theme to use. If a partial theme is provided then default @@ -123,6 +133,8 @@ export class ColorManager implements IColorManager { this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + // Clear our the cache + this._contrastCache.clear(); } private _parseColor( diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index ca852b6a..06c3fe38 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types'; export interface IColorManager { colors: IColorSet; + onOptionsChange(key: string): void; } export interface IColor { @@ -24,6 +25,15 @@ export interface IColorSet { /** The selection blended on top of background. */ selectionOpaque: IColor; ansi: IColor[]; + contrastCache: IColorContrastCache +} + +export interface IColorContrastCache { + clear(): void; + setRgba(bg: number, fg: number, value: number | null): void; + getRgba(bg: number, fg: number): number | null | undefined; + setColor(bg: number, fg: number, value: IColor | null): void; + getColor(bg: number, fg: number): IColor | null | undefined; } export interface IPartialColorSet { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index b5caccaf..bb96fb72 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -167,9 +167,17 @@ export class DomRendererRowFactory { return false; } - const adustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); - if (adustedColor) { - element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adustedColor.css}`); + // Try get from cache first + let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + + // Calculate and store in cache + if (adjustedColor === undefined) { + adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } + + if (adjustedColor) { + element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adjustedColor.css}`); return true; } From 8a0122f482a2c9a63dcb535be8d9f29e596e1e5f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:42:55 -0800 Subject: [PATCH 15/34] Add contrast caching to webgl --- .../src/atlas/CharAtlasUtils.ts | 2 +- .../src/atlas/WebglCharAtlas.ts | 27 ++++++++++++++----- src/browser/ColorContrastCache.ts | 6 ++--- src/browser/Types.d.ts | 4 +-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index ad25bf87..8e26b1d6 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -25,7 +25,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. ansi: colors.ansi.slice(), - contrastCache: {} as any + contrastCache: colors.contrastCache }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 1642ce08..b40c27cc 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -207,8 +207,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); if (minimumContrastCss) { return minimumContrastCss; } @@ -267,21 +267,34 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { if (this._config.minimumContrastRatio === 1) { return undefined; } + + // Try get from cache first + const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); + if (!result) { + this._config.colors.contrastCache.setCss(bg, fg, null); return undefined; } - return toCss( + + const css = toCss( (result >> 24) & 0xFF, (result >> 16) & 0xFF, (result >> 8) & 0xFF ); + this._config.colors.contrastCache.setCss(bg, fg, css); + + return css; } private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph; @@ -327,7 +340,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(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts index 3198ad83..b96b66cc 100644 --- a/src/browser/ColorContrastCache.ts +++ b/src/browser/ColorContrastCache.ts @@ -7,21 +7,21 @@ import { IColor, IColorContrastCache } from 'browser/Types'; export class ColorContrastCache implements IColorContrastCache { private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; - private _rgba: { [bg: number]: { [fg: number]: number | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {}; public clear(): void { this._color = {}; this._rgba = {}; } - public setRgba(bg: number, fg: number, value: number | null): void { + public setCss(bg: number, fg: number, value: string | null): void { if (!this._rgba[bg]) { this._rgba[bg] = {}; } this._rgba[bg]![fg] = value; } - public getRgba(bg: number, fg: number): number | null | undefined { + public getCss(bg: number, fg: number): string | null | undefined { return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 06c3fe38..89c72fa3 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -30,8 +30,8 @@ export interface IColorSet { export interface IColorContrastCache { clear(): void; - setRgba(bg: number, fg: number, value: number | null): void; - getRgba(bg: number, fg: number): number | null | undefined; + setCss(bg: number, fg: number, value: string | null): void; + getCss(bg: number, fg: number): string | null | undefined; setColor(bg: number, fg: number, value: IColor | null): void; getColor(bg: number, fg: number): IColor | null | undefined; } From 3b0042fd46df81b44f4fe16a8068034502b9e934 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:46:07 -0800 Subject: [PATCH 16/34] Remove log --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 26e966a0..e26e054a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -754,7 +754,6 @@ export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => await preFn(); } const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); - console.log(result); if (!deepEqual(result, val)) { return new Promise(r => { setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); From 647cff53dbd9c4f964b87b763b5ea97a4158751d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:53:42 -0800 Subject: [PATCH 17/34] Lint --- src/browser/Types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 89c72fa3..31d00af8 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -25,7 +25,7 @@ export interface IColorSet { /** The selection blended on top of background. */ selectionOpaque: IColor; ansi: IColor[]; - contrastCache: IColorContrastCache + contrastCache: IColorContrastCache; } export interface IColorContrastCache { From b3f439af092c1adaf4a1695be44f7a5b8e5753dd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:55:09 -0800 Subject: [PATCH 18/34] Remove unused imports --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 3a71dcd8..46fd2730 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -11,10 +11,9 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { DEFAULT_COLOR, NULL_CELL_CODE, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; From 032185f8b06a6289375e4e899a99638b29d97e9b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:00:40 -0800 Subject: [PATCH 19/34] Link webgl directly to browser/Color --- .../src/atlas/WebglCharAtlas.ts | 118 +----------------- 1 file changed, 1 insertion(+), 117 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b40c27cc..e1b0474f 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -11,6 +11,7 @@ import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'browser/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; +import { toCss, ensureContrastRatioRgba } from 'browser/Color'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. @@ -543,120 +544,3 @@ 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)) { - // 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(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.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(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); - } - return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; -} From b0c3606207f043f234de51860d5a94fbee5b9481 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:03:38 -0800 Subject: [PATCH 20/34] Add example values to .d.ts --- typings/xterm.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b49ed04b..03f89e0c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -151,8 +151,12 @@ declare module 'xterm' { /** * The minimum contrast ratio for text in the terminal, setting this will * change the foreground color dynamically depending on whether the contrast - * ratio is met. This can be to set 0.1 increments between 1 (default, do - * nothing) and 21 (foreground will be black or white). + * ratio is met. Example values: + * + * - 1: The default, do nothing. + * - 4.5: Minimum for WCAG AA compliance. + * - 7: Minimum for WCAG AAA compliance. + * - 21: White on black or black on white. */ minimumContrastRatio?: number; From 56d12fe84651a9a59b28b444c0debfb23a7c3010 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:09:32 -0800 Subject: [PATCH 21/34] Fix color manager unit test --- src/browser/ColorManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index a213c616..39a8f8c2 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -34,7 +34,7 @@ describe('ColorManager', () => { describe('constructor', () => { it('should fill all colors with values', () => { for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi') { + if (key !== 'ansi' && key !== 'contrastCache') { // A #rrggbb or rgba(...) assert.ok((cm.colors)[key].css.length >= 7); } From d1f7d2ef6acf9577ca4e2ca9556eaba9a55a8829 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:10:55 -0800 Subject: [PATCH 22/34] Tweak readme to call out min contrast ratio --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2944003..70ff7891 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b - **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. - **Rich unicode support**: Supports CJK, emojis and IMEs. - **Self-contained**: Requires zero dependencies to work. -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. +- **Accessible**: Screen reader and minimum contrast ratio support can be turned on - **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not From db090c70de99e8623e38d01e44f5132a67a58dd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:21:23 -0800 Subject: [PATCH 23/34] Add unit tests for ColorContrastCache --- src/browser/Color.ts | 2 -- src/browser/ColorContrastCache.test.ts | 43 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/browser/ColorContrastCache.test.ts diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 3a2b5032..09c6a4f1 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -84,8 +84,6 @@ export function contrastRatio(l1: number, l2: number): number { return (l1 + 0.05) / (l2 + 0.05); } -// TODO: Cache [bg][fg]: result, should probably be owned by ColorManager? - function rgbaToColor(r: number, g: number, b: number): IColor { return { css: toCss(r, g, b), diff --git a/src/browser/ColorContrastCache.test.ts b/src/browser/ColorContrastCache.test.ts new file mode 100644 index 00000000..17e5df38 --- /dev/null +++ b/src/browser/ColorContrastCache.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; + +describe('ColorContrastCache', () => { + let cache: ColorContrastCache; + + beforeEach(() => { + cache = new ColorContrastCache(); + }); + + it('should save and get color values', () => { + assert.strictEqual(cache.getColor(0x01, 0x00), undefined); + cache.setColor(0x01, 0x01, null); + assert.strictEqual(cache.getColor(0x01, 0x01), null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + assert.deepEqual(cache.getColor(0x01, 0x02), { css: '#030303', rgba: 0x030303ff}); + }); + + it('should save and get css values', () => { + assert.strictEqual(cache.getCss(0x01, 0x00), undefined); + cache.setCss(0x01, 0x01, null); + assert.strictEqual(cache.getCss(0x01, 0x01), null); + cache.setCss(0x01, 0x02, '#030303'); + assert.deepEqual(cache.getCss(0x01, 0x02), '#030303'); + }); + + it('should clear all values on clear', () => { + cache.setColor(0x01, 0x01, null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + cache.setCss(0x01, 0x01, null); + cache.setCss(0x01, 0x02, '#030303'); + cache.clear(); + assert.strictEqual(cache.getColor(0x01, 0x01), undefined); + assert.strictEqual(cache.getColor(0x01, 0x02), undefined); + assert.strictEqual(cache.getCss(0x01, 0x01), undefined); + assert.strictEqual(cache.getCss(0x01, 0x02), undefined); + }); +}); From 20b0a95bb0ef8decb76720f6187d1e8c8609a523 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:40:35 -0800 Subject: [PATCH 24/34] Add tests for ensureContrastRatioRgba --- src/browser/Color.test.ts | 54 ++++++++++++++++++++++++++++++++++++++- src/browser/Color.ts | 4 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 975315c6..673fffc8 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -167,4 +167,56 @@ describe('Color', () => { assert.equal(contrastRatio(1, 0), 21); }); }); + describe('ensureContrastRatioRgba', () => { + it('should return undefined if the color already meets the contrast ratio (black bg)', () => { + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 1), undefined); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 2), undefined); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 3), undefined); + }); + it('should return a color that meets the contrast ratio (black bg)', () => { + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 4), 0x707070ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 5), 0x7f7f7fff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 6), 0x8c8c8cff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 7), 0x989898ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 9), 0xadadadff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 11), 0xbebebeff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 15), 0xdbdbdbff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 18), 0xeeeeeeff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 20), 0xfafafaff); + assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 21), 0xffffffff); + }); + it('should return undefined if the color already meets the contrast ratio (white bg)', () => { + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 1), undefined); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 2), undefined); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 3), undefined); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 4), undefined); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 5), undefined); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 6), undefined); + }); + it('should return a color that meets the contrast ratio (white bg)', () => { + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 7), 0x565656ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 8), 0x4d4d4dff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 9), 0x454545ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 10), 0x3e3e3eff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 11), 0x373737ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 12), 0x313131ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 13), 0x313131ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 14), 0x272727ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 15), 0x232323ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 16), 0x1f1f1fff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 17), 0x1b1b1bff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 18), 0x151515ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 19), 0x101010ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 20), 0x080808ff); + assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 21), 0x000000ff); + }); + }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 09c6a4f1..aaf5ea2e 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -133,7 +133,7 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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; + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { @@ -153,5 +153,5 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } - return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } From 2c959dfb2e3df7b6ecefa20d69af019702b774f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:52:17 -0800 Subject: [PATCH 25/34] Keep a working AttributeData in WebglCharAtlas --- .../src/atlas/WebglCharAtlas.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index e1b0474f..9910b009 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -68,6 +68,7 @@ export class WebglCharAtlas implements IDisposable { public hasCanvasChanged = false; private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; + private _workAttributeData: AttributeData = new AttributeData(); constructor( document: Document, @@ -305,17 +306,19 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; - const bold = !!(fg & FgFlags.BOLD); - const inverse = !!(fg & FgFlags.INVERSE); - const dim = !!(bg & BgFlags.DIM); - const italic = !!(bg & BgFlags.ITALIC); - this._tmpCtx.save(); - let fgColor = getFgColor(fg); - let fgColorMode = fg & Attributes.CM_MASK; - let bgColor = getBgColor(bg); - let bgColorMode = bg & Attributes.CM_MASK; + this._workAttributeData.fg = fg; + this._workAttributeData.bg = bg; + + const bold = !!this._workAttributeData.isBold(); + const inverse = !!this._workAttributeData.isInverse(); + const dim = !!this._workAttributeData.isDim(); + const italic = !!this._workAttributeData.isItalic(); + let fgColor = this._workAttributeData.getFgColor(); + let fgColorMode = this._workAttributeData.getFgColorMode(); + let bgColor = this._workAttributeData.getBgColor(); + let bgColorMode = this._workAttributeData.getBgColorMode(); if (inverse) { const temp = fgColor; fgColor = bgColor; From 579d74ee33b54c1db62d568ef71c7f0028f93ccd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:53:29 -0800 Subject: [PATCH 26/34] jsdoc rgbRelativeLuminance2 --- src/browser/Color.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index aaf5ea2e..d4cbfe0e 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -61,6 +61,14 @@ export function rgbRelativeLuminance(rgb: number): number { (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 rgbRelativeLuminance2(r: number, g: number, b: number): number { const rs = r / 255; const gs = g / 255; From 7652cd8af3a20d5466634fd417126db9f81e049c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:59:46 -0800 Subject: [PATCH 27/34] Use hash notation over rgb() for dom colors --- .../renderer/dom/DomRendererRowFactory.test.ts | 4 ++-- src/browser/renderer/dom/DomRendererRowFactory.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 180db0fc..c71945cf 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -212,7 +212,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -223,7 +223,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bb96fb72..5e53cf60 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -130,7 +130,7 @@ export class DomRendererRowFactory { } break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:rgb(${(AttributeData.toColorRGB(fg)).join(',')});`); + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:#${padStart(fg.toString(16), '0', 6)};`); break; case Attributes.CM_DEFAULT: default: @@ -148,7 +148,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:rgb(${(AttributeData.toColorRGB(bg)).join(',')});`); + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:#${padStart(bg.toString(16), '0', 6)};`); break; case Attributes.CM_DEFAULT: default: @@ -184,3 +184,10 @@ export class DomRendererRowFactory { return false; } } + +function padStart(text: string, padChar: string, length: number): string { + while (text.length < length) { + text = padChar + text; + } + return text; +} From 24dc87af2c6165216d4fb4b70c84f474f72155ed Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 19:46:24 -0800 Subject: [PATCH 28/34] Only enter updateBackgounds condition if cell changed We want to check when the background changed or when the foreground changed (and either the old or new value was inverse) --- addons/xterm-addon-webgl/src/RectangleRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 356e1747..b52a506e 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -254,7 +254,7 @@ export class RectangleRenderer { const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; const inverse = !!(fg & FgFlags.INVERSE); - if (bg !== currentBg || ((inverse || currentInverse) && fg !== currentFg)) { + if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; From be9700878b7604dfec6133c73c20926fc72af8ae Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 20:02:40 -0800 Subject: [PATCH 29/34] Have inverse bold respect drawBoldTextInBrightColors --- src/browser/renderer/BaseRenderLayer.ts | 8 ++++++-- src/browser/renderer/TextRenderLayer.ts | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index a33bef28..2c748c50 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -282,7 +282,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); } - const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8; fg += drawInBrightColor ? 8 : 0; this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; @@ -325,7 +325,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { if (cell.isFgDefault()) { diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 330400ca..fee261f6 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -227,7 +227,11 @@ export class TextRenderLayer extends BaseRenderLayer { } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { if (cell.isFgDefault()) { From 023fab9095c63ff3e549dbc171225da232b7d2ea Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 20:34:01 -0800 Subject: [PATCH 30/34] Remove unused import --- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 5e53cf60..cffc5624 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,6 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; From c95337a742c6334df50c82121416b97e63dbae55 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 21:57:15 -0800 Subject: [PATCH 31/34] Remove unneeded params from webgl funcs --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 9910b009..686dcb10 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -234,7 +234,7 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: @@ -250,7 +250,7 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: @@ -280,8 +280,8 @@ export class WebglCharAtlas implements IDisposable { return adjustedColor || undefined; } - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold); const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); if (!result) { From e5f4e2ff8f2ac034764744fb4d411c0ac1b62952 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:07:50 -0800 Subject: [PATCH 32/34] Support min contrast ratio in canvas renderer too: --- src/browser/renderer/BaseRenderLayer.ts | 103 ++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 2c748c50..599ab114 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -5,16 +5,17 @@ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; import { ICellData } from 'common/Types'; -import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IColor } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { toCss, ensureContrastRatioRgba } from 'browser/Color'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -262,13 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { + const contrastColor = this._getContrastColor(cell); // skip cache right away if we draw in RGB // Note: to avoid bad runtime JoinedCellData will be skipped // in the cache handler itself (atlasDidDraw == false) and // fall through to uncached later down below - if (cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(cell, x, y); + if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { + this._drawUncachedChars(cell, x, y, contrastColor); return; } @@ -314,13 +316,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(cell: ICellData, x: number, y: number): void { + private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); this._ctx.textBaseline = 'middle'; if (cell.isInverse()) { - if (cell.isBgDefault()) { + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isBgDefault()) { this._ctx.fillStyle = this._colors.background.css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; @@ -332,7 +336,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { - if (cell.isFgDefault()) { + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isFgDefault()) { this._ctx.fillStyle = this._colors.foreground.css; } else if (cell.isFgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; @@ -383,5 +389,88 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`; } + + private _getContrastColor(cell: CellData): IColor | undefined { + if (this._optionsService.options.minimumContrastRatio === 1) { + return undefined; + } + + // Try get from cache first + const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + let fgColor = cell.getFgColor(); + let fgColorMode = cell.getFgColorMode(); + let bgColor = cell.getBgColor(); + let bgColorMode = cell.getBgColorMode(); + const isInverse = !!cell.isInverse(); + const isBold = !!cell.isInverse(); + if (isInverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); + const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio); + + if (!result) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + + const color: IColor = { + css: toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ), + rgba: result + }; + this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + + return color; + } + + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._colors.ansi[bgColor].rgba; + case Attributes.CM_RGB: + return bgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.foreground.rgba; + } + return this._colors.background.rgba; + } + } + + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } + return this._colors.ansi[fgColor].rgba; + case Attributes.CM_RGB: + return fgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.background.rgba; + } + return this._colors.foreground.rgba; + } + } } From 5aa31576722f02213411be860b678d12281b58d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:12:36 -0800 Subject: [PATCH 33/34] Return inverse colors correctly This probably wasn't causing a bug since the opposite values were being returned, it was wrong and made the code confusing though. --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 686dcb10..cda20225 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -244,7 +244,7 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - this._config.colors.foreground.rgba; + return this._config.colors.foreground.rgba; } return this._config.colors.background.rgba; } @@ -263,7 +263,7 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - this._config.colors.background.rgba; + return this._config.colors.background.rgba; } return this._config.colors.foreground.rgba; } From 943928c4d1a6d4f46fb33fd7969fff71aeed7786 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:26:02 -0800 Subject: [PATCH 34/34] Move WindowsMode to common/ Part of #1507 --- src/Terminal.ts | 14 +++++++++----- src/{ => common}/WindowsMode.ts | 15 +++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) rename src/{ => common}/WindowsMode.ts (67%) diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..fc354966 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,7 +43,7 @@ import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeDa import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { applyWindowsMode } from './WindowsMode'; +import { handleWindowsModeLineFeed } from 'common/WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; @@ -281,7 +281,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); if (this.options.windowsMode) { - this._windowsMode = applyWindowsMode(this); + this._enableWindowsMode(); + } + } + + private _enableWindowsMode(): void { + if (!this._windowsMode) { + this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService)); } } @@ -362,9 +368,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp break; case 'windowsMode': if (this.optionsService.options.windowsMode) { - if (!this._windowsMode) { - this._windowsMode = applyWindowsMode(this); - } + this._enableWindowsMode(); } else { this._windowsMode?.dispose(); this._windowsMode = undefined; diff --git a/src/WindowsMode.ts b/src/common/WindowsMode.ts similarity index 67% rename from src/WindowsMode.ts rename to src/common/WindowsMode.ts index 0838cae2..ff0e7591 100644 --- a/src/WindowsMode.ts +++ b/src/common/WindowsMode.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { IDisposable } from 'xterm'; -import { ITerminal } from './Types'; import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IBufferService } from 'common/services/Services'; -export function applyWindowsMode(terminal: ITerminal): IDisposable { +export function handleWindowsModeLineFeed(bufferService: IBufferService): void { // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening @@ -18,11 +17,11 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable { // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - return terminal.onLineFeed(() => { - const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); - const lastChar = line.get(terminal.cols - 1); + const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1); + const lastChar = line?.get(bufferService.cols - 1); - const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); + const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y); + if (nextLine && lastChar) { nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); - }); + } }