Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

force alpha to 1 when using background color as inverted foreground color #2560

Merged
merged 8 commits into from
Nov 25, 2019
8 changes: 4 additions & 4 deletions addons/xterm-addon-webgl/src/RectangleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
22 changes: 18 additions & 4 deletions addons/xterm-addon-webgl/src/WebglRenderer.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down Expand Up @@ -732,17 +748,15 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
return await page.evaluate(`Array.from(window.result)`);
}

async function setupBrowser(): Promise<void> {
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
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);
Expand Down
7 changes: 6 additions & 1 deletion addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
47 changes: 46 additions & 1 deletion src/browser/Color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 17 additions & 1 deletion src/browser/Color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
}

Expand All @@ -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.
Expand Down
73 changes: 50 additions & 23 deletions src/browser/ColorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/browser/renderer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/browser/renderer/atlas/DynamicCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down