Skip to content

Commit

Permalink
Merge pull request #991 from Tyriar/blurriness_fix
Browse files Browse the repository at this point in the history
Support sub-pixel anti-aliasing and fix blurry text issues
  • Loading branch information
Tyriar committed Sep 14, 2017
2 parents 136c82f + 8bdc8fb commit e8d784a
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 172 deletions.
14 changes: 4 additions & 10 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT

this.charMeasure = new CharMeasure(document, this.helperContainer);

this.renderer = new Renderer(this, this.options.theme);
this.options.theme = null;
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
this.renderer = new Renderer(this);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);

this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('blur', () => this.renderer.onBlur());
Expand Down Expand Up @@ -652,15 +655,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Measure the character size
this.charMeasure.measure(this.options);

// Set the theme if it was set via setOption/constructor before open. This
// must be run after CharMeasure.measure as it depends on char dimensions.
setTimeout(() => {
if (this.options.theme) {
this._setTheme(this.options.theme);
this.options.theme = null;
}
}, 0);

// Setup loop that draws to screen
this.refresh(0, this.rows - 1);

Expand Down
71 changes: 0 additions & 71 deletions src/renderer/BackgroundRenderLayer.ts

This file was deleted.

112 changes: 71 additions & 41 deletions src/renderer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,29 @@ export const INVERTED_DEFAULT_COLOR = -1;
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx: CanvasRenderingContext2D;
private scaledCharWidth: number;
private scaledCharHeight: number;
private scaledLineHeight: number;
private scaledLineDrawY: number;
private _scaledCharWidth: number;
private _scaledCharHeight: number;
private _scaledLineHeight: number;
private _scaledLineDrawY: number;

private _charAtlas: HTMLCanvasElement | ImageBitmap;

constructor(
container: HTMLElement,
id: string,
zIndex: number,
protected colors: IColorSet
private _alpha: boolean,
protected _colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d');
this._ctx = this._canvas.getContext('2d', {_alpha});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Draw the background if this is an opaque layer
if (!_alpha) {
this.clearAll();
}
container.appendChild(this._canvas);
}

Expand All @@ -53,7 +58,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
this._charAtlas = null;
const result = acquireCharAtlas(terminal, this.colors, this.scaledCharWidth, this.scaledCharHeight);
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
Expand All @@ -62,17 +67,22 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}

public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
this.scaledCharWidth = dim.scaledCharWidth;
this.scaledCharHeight = dim.scaledCharHeight;
this.scaledLineHeight = dim.scaledLineHeight;
this.scaledLineDrawY = dim.scaledLineDrawY;
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledLineHeight = dim.scaledLineHeight;
this._scaledLineDrawY = dim.scaledLineDrawY;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;

// Draw the background if this is an opaque layer
if (!this._alpha) {
this.clearAll();
}

if (charSizeChanged) {
this._refreshCharAtlas(terminal, this.colors);
this._refreshCharAtlas(terminal, this._colors);
}
}

Expand All @@ -85,7 +95,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column of the cell.
*/
private _getCellLeft(x: number): number {
return Math.round(x * this.scaledCharWidth);
return Math.round(x * this._scaledCharWidth);
}

/**
Expand All @@ -99,9 +109,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const cellLeft = this._getCellLeft(x);
this._ctx.fillRect(
cellLeft,
y * this.scaledLineHeight,
y * this._scaledLineHeight,
this._getCellLeft(x + width) - cellLeft,
height * this.scaledLineHeight);
height * this._scaledLineHeight);
}

/**
Expand All @@ -114,7 +124,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const cellLeft = this._getCellLeft(x);
this._ctx.fillRect(
cellLeft,
(y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
(y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
this._getCellLeft(x + width) - cellLeft,
window.devicePixelRatio);
}
Expand All @@ -128,9 +138,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected fillLeftLineAtCell(x: number, y: number): void {
this._ctx.fillRect(
this._getCellLeft(x),
y * this.scaledLineHeight,
y * this._scaledLineHeight,
window.devicePixelRatio,
this.scaledLineHeight);
this._scaledLineHeight);
}

/**
Expand All @@ -144,16 +154,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.lineWidth = window.devicePixelRatio;
this._ctx.strokeRect(
cellLeft + window.devicePixelRatio / 2,
y * this.scaledLineHeight + (window.devicePixelRatio / 2),
y * this._scaledLineHeight + (window.devicePixelRatio / 2),
this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio,
(height * this.scaledLineHeight) - window.devicePixelRatio);
(height * this._scaledLineHeight) - window.devicePixelRatio);
}

/**
* Clears the entire canvas.
*/
protected clearAll(): void {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}

/**
Expand All @@ -165,11 +180,20 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected clearCells(x: number, y: number, width: number, height: number): void {
const cellLeft = this._getCellLeft(x);
this._ctx.clearRect(
cellLeft,
y * this.scaledLineHeight,
this._getCellLeft(x + width) - cellLeft,
height * this.scaledLineHeight);
if (this._alpha) {
this._ctx.clearRect(
cellLeft,
y * this._scaledLineHeight,
this._getCellLeft(x + width) - cellLeft,
height * this._scaledLineHeight);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillRect(
cellLeft,
y * this._scaledLineHeight,
this._getCellLeft(x + width) - cellLeft,
height * this._scaledLineHeight);
}
}

/**
Expand All @@ -191,9 +215,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// can bleed into other cells. This code will clip the following fillText,
// ensuring that its contents don't go beyond the cell bounds.
this._ctx.beginPath();
this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight);
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight);
this._ctx.clip();
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this.scaledCharWidth, y * this.scaledCharHeight);
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight);
}

/**
Expand All @@ -206,9 +230,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bold: boolean): void {
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void {
// Clear the cell next to this character if it's wide
if (width === 2) {
this.clearCells(x + 1, y, 1, 1);
Expand All @@ -226,15 +252,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const isAscii = code < 256;
const isBasicColor = (colorIndex > 1 && fg < 16);
const isDefaultColor = fg >= 256;
if (isAscii && (isBasicColor || isDefaultColor)) {
const isDefaultBackground = bg >= 256;
if (isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight,
x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight);
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight,
x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight);
} else {
this._drawUncachedChar(terminal, char, width, fg, x, y);
this._drawUncachedChar(terminal, char, width, fg, x, y, bold);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
Expand All @@ -252,30 +279,33 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number): void {
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void {
this._ctx.save();
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (bold) {
this._ctx.font = `bold ${this._ctx.font}`;
}
this._ctx.textBaseline = 'top';

if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this.colors.background;
this._ctx.fillStyle = this._colors.background;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this.colors.ansi[fg];
this._ctx.fillStyle = this._colors.ansi[fg];
} else {
this._ctx.fillStyle = this.colors.foreground;
this._ctx.fillStyle = this._colors.foreground;
}

// Since uncached characters are not coming off the char atlas with source
// coordinates, it means that text drawn to the canvas (particularly '_')
// can bleed into other cells. This code will clip the following fillText,
// ensuring that its contents don't go beyond the cell bounds.
this._ctx.beginPath();
this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, width * this.scaledCharWidth, this.scaledCharHeight);
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, width * this._scaledCharWidth, this._scaledCharHeight);
this._ctx.clip();

// Draw the character
this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY);
this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY);
this._ctx.restore();
}
}
Expand Down
Loading

0 comments on commit e8d784a

Please sign in to comment.