diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 38457e13..3bc0a917 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -78,6 +78,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; + /** + * Gets the left position of a cell. Since character width is stored as a + * float in order to prevent bad letter spacing, drawing shapes in the cell + * need to be rounded. + * @param x The column of the cell. + */ + private _getCellLeft(x: number): number { + return Math.round(x * this.scaledCharWidth); + } + /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. @@ -86,7 +96,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to fill. */ protected fillCells(x: number, y: number, width: number, height: number): void { - this._ctx.fillRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight); + const cellLeft = this._getCellLeft(x); + this._ctx.fillRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); } /** @@ -96,10 +111,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { + const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - x * this.scaledCharWidth, + cellLeft, (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - width * this.scaledCharWidth, + this._getCellLeft(x + width) - cellLeft, window.devicePixelRatio); } @@ -111,7 +127,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - x * this.scaledCharWidth, + this._getCellLeft(x), y * this.scaledLineHeight, window.devicePixelRatio, this.scaledLineHeight); @@ -124,11 +140,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { + const cellLeft = this._getCellLeft(x); this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - x * this.scaledCharWidth + window.devicePixelRatio / 2, + cellLeft + window.devicePixelRatio / 2, y * this.scaledLineHeight + (window.devicePixelRatio / 2), - (width * this.scaledCharWidth) - window.devicePixelRatio, + this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio, (height * this.scaledLineHeight) - window.devicePixelRatio); } @@ -147,7 +164,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to clear. */ protected clearCells(x: number, y: number, width: number, height: number): void { - this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight); + const cellLeft = this._getCellLeft(x); + this._ctx.clearRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); } /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b610fd69..a2b8a7fe 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -77,10 +77,17 @@ export class Renderer extends EventEmitter implements IRenderer { return; } - // Calculate the scaled character dimensions, if devicePixelRatio is a - // floating point number then the value is ceiled to ensure there is enough - // space to draw the character to the cell - this.dimensions.scaledCharWidth = Math.ceil(this._terminal.charMeasure.width * window.devicePixelRatio); + // Calculate the scaled character width. Width is kept as a decimal to + // provide better letter spacing, otherwise the text can look odd. + // Characters drawn using this decimal number do have the potential to + // overlap, but only by a single pixel. As such, it's not a big deal when + // they do as that pixel is always cleared as necessary before drawing the + // character. + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + + // Calculate the scaled character height. Height is ceiled in case + // devicePixelRatio is a floating point number in order to ensure there is + // enough space to draw the character to the cell. this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); // Calculate the scaled line height, if lineHeight is not 1 then the value @@ -96,7 +103,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; + this.dimensions.scaledCanvasWidth = Math.round(this._terminal.cols * this.dimensions.scaledCharWidth); // The the size of the canvas on the page. It's very important that this // rounds to nearest integer and not ceils as browsers often set diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index e8fcff86..e5b03f87 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -60,7 +60,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { return; } if (this._width !== geometry.width || this._height !== geometry.height) { - this._width = Math.ceil(geometry.width); + this._width = geometry.width; this._height = Math.ceil(geometry.height); this.emit('charsizechanged'); }