diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index b52a506e2c..2246cddd9f 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -22,13 +22,13 @@ const enum VertexAttribLocations { const vertexShaderSource = `#version 300 es layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; -layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color; +layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color; layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; uniform mat4 u_projection; uniform vec2 u_resolution; -out vec3 v_color; +out vec4 v_color; void main() { vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution; @@ -39,12 +39,12 @@ void main() { const fragmentShaderSource = `#version 300 es precision lowp float; -in vec3 v_color; +in vec4 v_color; out vec4 outColor; void main() { - outColor = vec4(v_color, 1); + outColor = v_color; }`; interface IVertices { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index e26e054a98..bf877dd87e 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -702,6 +702,22 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); }); }); + + describe('allowTransparency', async () => { + before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true})); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + it('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + const data = `\\x1b[7m█\x1b[0m`; + await writeSync(data); + // Inverse background should be opaque + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -732,7 +748,7 @@ async function getCellColor(col: number, row: number): Promise { return await page.evaluate(`Array.from(window.result)`); } -async function setupBrowser(): Promise { +async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, args: [`--window-size=${width},${height}`, `--no-sandbox`] @@ -740,9 +756,7 @@ async function setupBrowser(): Promise { page = (await browser.pages())[0]; await page.setViewport({ width, height }); await page.goto(APP); - await openTerminal({ - rendererType: 'dom' - }); + await openTerminal(options); await page.evaluate(` window.addon = new WebglAddon(true); window.term.loadAddon(window.addon); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 097c420d5b..c96c2a6957 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -228,7 +228,12 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - return this._config.colors.background.css; + const bg = this._config.colors.background.css; + if (bg.length === 9) { + // Remove bg alpha channel if present + return bg.substr(0, 7); + } + return bg; } return this._config.colors.foreground.css; } diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 673fffc894..cbeeaba95c 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, ensureContrastRatioRgba } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba, opaque, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -135,6 +135,51 @@ describe('Color', () => { assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); }); }); + + describe('fromRgba', () => { + it('should convert an rgba number to an rgba array', () => { + assert.deepEqual(fromRgba(0x00000000), [0x00, 0x00, 0x00, 0x00]); + assert.deepEqual(fromRgba(0x10101010), [0x10, 0x10, 0x10, 0x10]); + assert.deepEqual(fromRgba(0x20202020), [0x20, 0x20, 0x20, 0x20]); + assert.deepEqual(fromRgba(0x30303030), [0x30, 0x30, 0x30, 0x30]); + assert.deepEqual(fromRgba(0x40404040), [0x40, 0x40, 0x40, 0x40]); + assert.deepEqual(fromRgba(0x50505050), [0x50, 0x50, 0x50, 0x50]); + assert.deepEqual(fromRgba(0x60606060), [0x60, 0x60, 0x60, 0x60]); + assert.deepEqual(fromRgba(0x70707070), [0x70, 0x70, 0x70, 0x70]); + assert.deepEqual(fromRgba(0x80808080), [0x80, 0x80, 0x80, 0x80]); + assert.deepEqual(fromRgba(0x90909090), [0x90, 0x90, 0x90, 0x90]); + assert.deepEqual(fromRgba(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]); + assert.deepEqual(fromRgba(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]); + assert.deepEqual(fromRgba(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]); + assert.deepEqual(fromRgba(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]); + assert.deepEqual(fromRgba(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]); + assert.deepEqual(fromRgba(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]); + assert.deepEqual(fromRgba(0xffffffff), [0xff, 0xff, 0xff, 0xff]); + }); + }); + + describe('opaque', () => { + it('should make the color opaque', () => { + assert.deepEqual(opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepEqual(opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepEqual(opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepEqual(opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepEqual(opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepEqual(opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepEqual(opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF }); + }); + }); + describe('rgbRelativeLuminance', () => { it('should calculate the relative luminance of the color', () => { assert.equal(rgbRelativeLuminance(0x000000), 0); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index d4cbfe0e13..e20f62e6e2 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -39,7 +39,10 @@ export function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } -export function toCss(r: number, g: number, b: number): string { +export function toCss(r: number, g: number, b: number, a?: number): string { + if (a !== undefined) { + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`; + } return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`; } @@ -48,6 +51,19 @@ export function toRgba(r: number, g: number, b: number, a: number = 0xFF): numbe return (r << 24 | g << 16 | b << 8 | a) >>> 0; } +export function fromRgba(value: number): [number, number, number, number] { + return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; +} + +export function opaque(color: IColor): IColor { + const rgba = (color.rgba | 0xFF) >>> 0; + const [r, g, b] = fromRgba(rgba); + return { + css: toCss(r, g, b), + rgba + }; +} + /** * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio * between two colors. diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 92994cfc44..b4bcdde044 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,7 +5,7 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { fromCss, toCss, blend, toRgba } from 'browser/Color'; +import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; const DEFAULT_FOREGROUND = fromCss('#ffffff'); @@ -159,28 +159,55 @@ export class ColorManager implements IColorManager { this._ctx.fillRect(0, 0, 1, 1); const data = this._ctx.getImageData(0, 0, 1, 1).data; - if (!allowTransparency && data[3] !== 0xFF) { - // Ideally we'd just ignore the alpha channel, but... - // - // Browsers may not give back exactly the same RGB values we put in, because most/all - // convert the color to a pre-multiplied representation. getImageData converts that back to - // a un-premultipled representation, but the precision loss may make the RGB channels unuable - // on their own. - // - // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns - // into 0x00000000. - // - // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, - // pixels that have just been set using putImageData() might be returned to an equivalent - // getImageData() as different values." - // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation - // - // So let's just use the fallback color in this case instead. - console.warn( - `Color: ${css} is using transparency, but allowTransparency is false. ` + - `Using fallback ${fallback.css}.` - ); - return fallback; + // Check if the printed color was transparent + if (data[3] !== 0xFF) { + if (!allowTransparency) { + // Ideally we'd just ignore the alpha channel, but... + // + // Browsers may not give back exactly the same RGB values we put in, because most/all + // convert the color to a pre-multiplied representation. getImageData converts that back to + // a un-premultipled representation, but the precision loss may make the RGB channels unuable + // on their own. + // + // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns + // into 0x00000000. + // + // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, + // pixels that have just been set using putImageData() might be returned to an equivalent + // getImageData() as different values." + // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation + // + // So let's just use the fallback color in this case instead. + console.warn( + `Color: ${css} is using transparency, but allowTransparency is false. ` + + `Using fallback ${fallback.css}.` + ); + return fallback; + } + let r: number; + let g: number; + let b: number; + let a: number; + let rgba: number; + if (css.length === 5) { + const num = parseInt(css.substr(1), 16); + r = ((num >> 12) & 0xF) * 16; + g = ((num >> 8) & 0xF) * 16; + b = ((num >> 4) & 0xF) * 16; + a = (num & 0xF) * 16; + rgba = toRgba(r, g, b, a); + } else { + rgba = parseInt(css.substr(1), 16); + r = (rgba >> 24) & 0xFF; + g = (rgba >> 16) & 0xFF; + b = (rgba >> 8) & 0xFF; + a = (rgba ) & 0xFF; + } + + return { + rgba, + css: toCss(r, g, b, a) + }; } return { diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 599ab114ac..f55a130ddb 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -15,7 +15,7 @@ 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'; +import { toCss, ensureContrastRatioRgba, opaque } from 'browser/Color'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -325,7 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fgOverride) { this._ctx.fillStyle = fgOverride.css; } else if (cell.isBgDefault()) { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = opaque(this._colors.background).css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 40103bc721..84a5016026 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -11,6 +11,7 @@ import { LRUMap } from 'browser/renderer/atlas/LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'browser/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { opaque } 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. @@ -222,7 +223,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { private _getForegroundColor(glyph: IGlyphIdentifier): IColor { if (glyph.fg === INVERTED_DEFAULT_COLOR) { - return this._config.colors.background; + return opaque(this._config.colors.background); } else if (glyph.fg < 256) { // 256 color support return this._getColorFromAnsiIndex(glyph.fg); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 23c5a3a191..018e63e5e0 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -11,6 +11,7 @@ import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { opaque } from 'browser/Color'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -230,7 +231,7 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${opaque(this._colors.background).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; this._themeStyleElement.innerHTML = styles;