Skip to content

Commit

Permalink
Merge pull request #2560 from ivanwonder/reverse-transparency
Browse files Browse the repository at this point in the history
force alpha to 1 when using background color as inverted foreground color
  • Loading branch information
Tyriar authored Nov 25, 2019
2 parents 3b1479a + 1ff1809 commit 8b5c48f
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 38 deletions.
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

0 comments on commit 8b5c48f

Please sign in to comment.