From 1b2bac7aef2ba2d492b57aead9af238d32821dfa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Oct 2017 20:07:28 -0700 Subject: [PATCH] Add letterSpacing option This provides an option for fonts to be more spaced out. This is particularly useful for fonts that can seem crammed due to not allowing floating point numbers when drawing, such as Monoid or Operator Mono. --- src/Interfaces.ts | 1 + src/Terminal.ts | 2 + src/Viewport.ts | 4 +- src/renderer/BaseRenderLayer.ts | 90 +++++++++++++++++++++------------ src/renderer/Interfaces.ts | 6 ++- src/renderer/Renderer.ts | 21 +++++--- 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 2c77e8d3..0ab0af5a 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -139,6 +139,7 @@ export interface ITerminalOptions { fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; + letterSpacing?: number; lineHeight?: number; rows?: number; screenKeys?: boolean; diff --git a/src/Terminal.ts b/src/Terminal.ts index 8486f265..5ea01092 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -80,6 +80,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { fontFamily: 'courier-new, courier, monospace', fontSize: 15, lineHeight: 1.0, + letterSpacing: 0, scrollback: 1000, screenKeys: false, debug: false, @@ -410,6 +411,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.clear(); this.charMeasure.measure(this.options); break; + case 'letterSpacing': case 'lineHeight': // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); diff --git a/src/Viewport.ts b/src/Viewport.ts index 064076a2..d20749c3 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -47,7 +47,7 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio; + this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) { this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; @@ -75,7 +75,7 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) { + if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d15f1ee7..6b264e4b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -16,8 +16,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _ctx: CanvasRenderingContext2D; private _scaledCharWidth: number; private _scaledCharHeight: number; - private _scaledLineHeight: number; - private _scaledLineDrawY: number; + private _scaledCellWidth: number; + private _scaledCellHeight: number; + private _scaledCharLeft: number; + private _scaledCharTop: number; private _charAtlas: HTMLCanvasElement | ImageBitmap; @@ -70,10 +72,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + this._scaledCellWidth = dim.scaledCellWidth; + this._scaledCellHeight = dim.scaledCellHeight; this._scaledCharWidth = dim.scaledCharWidth; this._scaledCharHeight = dim.scaledCharHeight; - this._scaledLineHeight = dim.scaledLineHeight; - this._scaledLineDrawY = dim.scaledLineDrawY; + this._scaledCharLeft = dim.scaledCharLeft; + this._scaledCharTop = dim.scaledCharTop; this._canvas.width = dim.scaledCanvasWidth; this._canvas.height = dim.scaledCanvasHeight; this._canvas.style.width = `${dim.canvasWidth}px`; @@ -100,10 +104,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ 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); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } /** @@ -114,9 +118,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( - x * this._scaledCharWidth, - (y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - width * this._scaledCharWidth, + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + width * this._scaledCellWidth, window.devicePixelRatio); } @@ -128,10 +132,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, + x * this._scaledCellWidth, + y * this._scaledCellHeight, window.devicePixelRatio, - this._scaledLineHeight); + this._scaledCellHeight); } /** @@ -143,10 +147,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - x * this._scaledCharWidth + window.devicePixelRatio / 2, - y * this._scaledLineHeight + (window.devicePixelRatio / 2), - width * this._scaledCharWidth - window.devicePixelRatio, - (height * this._scaledLineHeight) - window.devicePixelRatio); + x * this._scaledCellWidth + window.devicePixelRatio / 2, + y * this._scaledCellHeight + (window.devicePixelRatio / 2), + width * this._scaledCellWidth - window.devicePixelRatio, + (height * this._scaledCellHeight) - window.devicePixelRatio); } /** @@ -171,17 +175,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } else { this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } } @@ -204,9 +208,17 @@ 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); + // TODO: Make clip rect use cell size? + this._ctx.rect( + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + 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._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); } /** @@ -242,8 +254,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { 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, charAtlasCellWidth, this._scaledCharHeight, - x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight); + code * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._scaledCharHeight, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + this._scaledCharWidth, + this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold); } @@ -285,11 +303,19 @@ 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(0, y * this._scaledLineHeight + this._scaledLineDrawY, terminal.cols * this._scaledCharWidth, this._scaledCharHeight); + // TODO: Why is this be clipped at char top? + this._ctx.rect( + 0, + y * this._scaledCellHeight + this._scaledCharTop, + terminal.cols * 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._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 74400b8c..be1b39dd 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -86,8 +86,10 @@ export interface IColorSet { export interface IRenderDimensions { scaledCharWidth: number; scaledCharHeight: number; - scaledLineHeight: number; - scaledLineDrawY: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; scaledCanvasWidth: number; scaledCanvasHeight: number; canvasWidth: number; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 168d6dc8..52cad50e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,8 +40,8 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions = { scaledCharWidth: null, scaledCharHeight: null, - scaledLineHeight: null, - scaledLineDrawY: null, + scaledCellHeight: null, + scaledCharTop: null, scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, @@ -91,20 +91,27 @@ export class Renderer extends EventEmitter implements IRenderer { // 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 + // Calculate the scaled cell height, if lineHeight is not 1 then the value // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + + // Calculate the scaled cell width, taking the letterSpacing into account. + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + + // Calculate the x coordinate with a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); // 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.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; // 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