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

Clear glyph pixels when all channels differ below a threshold #3897

Merged
merged 2 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 52 additions & 36 deletions addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'common/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { channels, rgba } from 'common/Color';
import { channels, color, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';

Expand Down Expand Up @@ -217,10 +217,10 @@ export class WebglCharAtlas implements IDisposable {
}
}

private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
if (minimumContrastCss) {
return minimumContrastCss;
private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor {
const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
if (minimumContrastColor) {
return minimumContrastColor;
}

switch (fgColorMode) {
Expand All @@ -229,21 +229,17 @@ export class WebglCharAtlas implements IDisposable {
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).css;
return this._getColorFromAnsiIndex(fgColor);
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
return channels.toCss(arr[0], arr[1], arr[2]);
return rgba.toColor(arr[0], arr[1], arr[2]);
case Attributes.CM_DEFAULT:
default:
if (inverse) {
const bg = this._config.colors.background.css;
if (bg.length === 9) {
// Remove bg alpha channel if present
return bg.slice(0, 7);
}
return bg;
// Inverse should always been opaque, even when transparency is used
return color.opaque(this._config.colors.background);
}
return this._config.colors.foreground.css;
return this._config.colors.foreground;
}
}

Expand Down Expand Up @@ -282,13 +278,13 @@ export class WebglCharAtlas implements IDisposable {
}
}

private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined {
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
return undefined;
}

// Try get from cache first
const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg);
const adjustedColor = this._config.colors.contrastCache.getColor(bg, fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
Expand All @@ -298,18 +294,18 @@ export class WebglCharAtlas implements IDisposable {
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);

if (!result) {
this._config.colors.contrastCache.setCss(bg, fg, null);
this._config.colors.contrastCache.setColor(bg, fg, null);
return undefined;
}

const css = channels.toCss(
const color = rgba.toColor(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
this._config.colors.contrastCache.setCss(bg, fg, css);
this._config.colors.contrastCache.setColor(bg, fg, color);

return css;
return color;
}

private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
Expand Down Expand Up @@ -377,7 +373,8 @@ export class WebglCharAtlas implements IDisposable {
this._tmpCtx.textBaseline = TEXT_BASELINE;

const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;

// Apply alpha to dim the character
if (dim) {
Expand All @@ -401,12 +398,12 @@ export class WebglCharAtlas implements IDisposable {
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible,
// try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency);
if (isBeyondCellBounds) {
for (let offset = 1; offset <= 5; offset++) {
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency);
if (!isBeyondCellBounds) {
break;
}
Expand Down Expand Up @@ -441,14 +438,8 @@ export class WebglCharAtlas implements IDisposable {
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
);

// TODO: Support transparency
// let isEmpty = false;
// if (!this._config.allowTransparency) {
// isEmpty = clearColor(imageData, backgroundColor);
// }

// Clear out the background color and determine if the glyph is empty.
const isEmpty = clearColor(imageData, backgroundColor);
const isEmpty = clearColor(imageData, backgroundColor, foregroundColor, this._config.allowTransparency);

// Handle empty glyphs
if (isEmpty) {
Expand Down Expand Up @@ -588,23 +579,48 @@ export class WebglCharAtlas implements IDisposable {
}

/**
* Makes a partiicular rgb color in an ImageData completely transparent.
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, color: IColor): boolean {
function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTransparency: boolean): boolean {
// Get color channels
const r = bg.rgba >>> 24;
const g = bg.rgba >>> 16 & 0xFF;
const b = bg.rgba >>> 8 & 0xFF;
const fgR = fg.rgba >>> 24;
const fgG = fg.rgba >>> 16 & 0xFF;
const fgB = fg.rgba >>> 8 & 0xFF;

// Calculate a threshold that when below a color will be treated as transpart when the sum of
// channel value differs. This helps improve rendering when glyphs overlap with others. This
// threshold is calculated relative to the difference between the background and foreground to
// ensure important details of the glyph are always shown, even when the contrast ratio is low.
// The number 12 is largely arbitrary to ensure the pixels that escape the cell in the test case
// were covered (fg=#8ae234, bg=#c4a000).
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);

// Set alpha channel of relevent pixels to 0
let isEmpty = true;
const r = color.rgba >>> 24;
const g = color.rgba >>> 16 & 0xFF;
const b = color.rgba >>> 8 & 0xFF;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
// Check the threshold only when transparency is not allowed only as overlapping isn't an
// issue for transparency glyphs.
if (!allowTransparency &&
(Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b)) < threshold) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
}
}
}

return isEmpty;
}

Expand Down
26 changes: 13 additions & 13 deletions addons/xterm-addon-webgl/test/WebglRenderer.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe('WebGL Renderer Integration Tests', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
data += `\\x1b[38;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -312,7 +312,7 @@ describe('WebGL Renderer Integration Tests', 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 += `\\x1b[7;48;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -348,7 +348,7 @@ describe('WebGL Renderer Integration Tests', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`;
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand All @@ -369,7 +369,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
data += `\\x1b[38;2;${i};0;0m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -405,7 +405,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
data += `\\x1b[38;2;0;${i};0m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -441,7 +441,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
data += `\\x1b[38;2;0;0;${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -477,7 +477,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
data += `\\x1b[38;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -567,7 +567,7 @@ describe('WebGL Renderer Integration Tests', async () => {
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 += `\\x1b[7;48;2;0;${i};0m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -603,7 +603,7 @@ describe('WebGL Renderer Integration Tests', async () => {
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 += `\\x1b[7;48;2;0;0;${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -639,7 +639,7 @@ describe('WebGL Renderer Integration Tests', async () => {
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 += `\\x1b[7;48;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -674,7 +674,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`;
data += `\\x1b[8;48;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
Expand Down Expand Up @@ -870,7 +870,7 @@ describe('WebGL Renderer Integration Tests', async () => {
background: '#ff000080'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\\x1b[7m█\x1b[0m`;
const data = `\\x1b[7m█\\x1b[0m`;
await writeSync(page, data);
// Inverse background should be opaque
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
Expand All @@ -889,7 +889,7 @@ describe('WebGL Renderer Integration Tests', async () => {
selectionForeground: '#ff0000'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\\x1b[7m█\x1b[0m`;
const data = `\\x1b[7m█\\x1b[0m`;
await writeSync(page, data);
await page.evaluate(`window.term.selectAll()`);
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
Expand Down
9 changes: 5 additions & 4 deletions src/browser/renderer/atlas/DynamicCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@ export class NoneCharAtlas extends BaseCharAtlas {
}

/**
* Makes a partiicular rgb color in an ImageData completely transparent.
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, color: IColor): boolean {
Expand All @@ -392,9 +393,9 @@ function clearColor(imageData: ImageData, color: IColor): boolean {
const g = color.rgba >>> 16 & 0xFF;
const b = color.rgba >>> 8 & 0xFF;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
if (Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b) < 35) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
Expand Down