Make the clear threshold relative to fg/bg difference

This commit is contained in:
Daniel Imms
2022-07-10 08:49:09 -07:00
parent 89b320a5e2
commit ea79194925
2 changed files with 66 additions and 51 deletions
@@ -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';
@@ -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) {
@@ -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;
}
}
@@ -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;
}
@@ -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;
@@ -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) {
@@ -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;
}
@@ -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) {
@@ -592,20 +583,44 @@ export class WebglCharAtlas implements IDisposable {
* 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 (Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b) < 35) {
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;
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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';
}
@@ -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]);
@@ -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]);