From d0ef868215f1caa4b457603f3b2791407bf49609 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Jan 2018 13:43:34 -0800 Subject: [PATCH 1/2] Refactor char atlas to enable generation with worker This doesn't actually add workers but it separates the code nicely such that the minimal amount of relevant code can be pulled in by worker code. Part of #955 --- src/SelectionManager.ts | 2 +- src/Terminal.ts | 5 +- src/renderer/CharAtlas.ts | 140 +++++------------------------- src/shared/CharAtlasGenerator.ts | 133 ++++++++++++++++++++++++++++ src/{ => shared}/utils/Browser.ts | 11 ++- src/utils/Generic.ts | 13 --- 6 files changed, 165 insertions(+), 139 deletions(-) create mode 100644 src/shared/CharAtlasGenerator.ts rename src/{ => shared}/utils/Browser.ts (75%) delete mode 100644 src/utils/Generic.ts diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index cd6fc996..83aaf6a3 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -4,7 +4,7 @@ */ import { MouseHelper } from './utils/MouseHelper'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; diff --git a/src/Terminal.ts b/src/Terminal.ts index f95cc0ea..220af9e3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -35,7 +35,7 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; @@ -44,7 +44,6 @@ import { BellSound } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; -import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; import { IRenderer } from './renderer/Interfaces'; // Let it work inside Node.js for automated testing purposes. @@ -574,8 +573,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.document = this.parent.ownerDocument; this.body = this.document.body; - initializeCharAtlas(this.document); - // Create main element container this.element = this.document.createElement('div'); this.element.classList.add('terminal'); diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 9ac1e161..3bc4291a 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -5,7 +5,8 @@ import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; -import { isFirefox } from '../utils/Browser'; +import { isFirefox } from '../shared/utils/Browser'; +import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; export const CHAR_ATLAS_CELL_SPACING = 1; @@ -63,8 +64,26 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC } } + const canvasFactory = (width: number, height: number) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + const charAtlasConfig: ICharAtlasRequest = { + scaledCharWidth, + scaledCharHeight, + fontSize: terminal.options.fontSize, + fontFamily: terminal.options.fontFamily, + background: colors.background, + foreground: colors.foreground, + ansiColors: colors.ansi, + devicePixelRatio: window.devicePixelRatio + }; + const newEntry: ICharAtlasCacheEntry = { - bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi), + bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), config: newConfig, ownedBy: [terminal] }; @@ -103,120 +122,3 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } - -let generator: CharAtlasGenerator; - -/** - * Initializes the char atlas generator. - * @param document The document. - */ -export function initialize(document: Document): void { - if (!generator) { - generator = new CharAtlasGenerator(document); - } -} - -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor(private _document: Document) { - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d', {alpha: false}); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { - const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; - this._canvas.width = 255 * cellWidth; - this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; - - this._ctx.fillStyle = background; - this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); - - this._ctx.save(); - this._ctx.fillStyle = foreground; - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); - this._ctx.restore(); - } - // Default color bold - this._ctx.save(); - this._ctx.font = `bold ${this._ctx.font}`; - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); - this._ctx.restore(); - } - this._ctx.restore(); - - // Colors 0-15 - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = `bold ${this._ctx.font}`; - } - const y = (colorIndex + 2) * cellHeight; - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillStyle = ansiColors[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); - this._ctx.restore(); - } - } - this._ctx.restore(); - - // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. Firefox is also - // included here as ImageBitmap appears both buggy and has horrible - // performance (tested on v55). - if (!('createImageBitmap' in window) || isFirefox) { - // Regenerate canvas and context as they are now owned by the char atlas - const result = this._canvas; - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - return result; - } - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - - // Remove the background color from the image so characters may overlap - const r = parseInt(background.substr(1, 2), 16); - const g = parseInt(background.substr(3, 2), 16); - const b = parseInt(background.substr(5, 2), 16); - this._clearColor(charAtlasImageData, r, g, b); - - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } - - private _clearColor(imageData: ImageData, r: number, g: number, b: number): void { - 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; - } - } - } -} diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/CharAtlasGenerator.ts new file mode 100644 index 00000000..9452f8c6 --- /dev/null +++ b/src/shared/CharAtlasGenerator.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { isFirefox } from './utils/Browser'; + +declare const Promise: any; + +export interface IOffscreenCanvas { + width: number; + height: number; + getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D; + transferToImageBitmap(): ImageBitmap; +} + +export interface ICharAtlasRequest { + scaledCharWidth: number; + scaledCharHeight: number; + fontSize: number; + fontFamily: string; + background: string; + foreground: string; + ansiColors: string[]; + devicePixelRatio: number; +} + +export const CHAR_ATLAS_CELL_SPACING = 1; + +/** + * Generates a char atlas. + * @param context The window or worker context. + * @param canvasFactory A function to generate a canvas with a width or height. + * @param request The config for the new char atlas. + */ +export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise { + const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const canvas = canvasFactory( + /*255 ascii chars*/255 * cellWidth, + (/*default+default bold*/2 + /*0-15*/16) * cellHeight + ); + const ctx = canvas.getContext('2d', {alpha: false}); + + ctx.fillStyle = request.background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.fillStyle = request.foreground; + ctx.font = `${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; + ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.restore(); + } + // Default color bold + ctx.save(); + ctx.font = `bold ${ctx.font}`; + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.restore(); + } + ctx.restore(); + + // Colors 0-15 + ctx.font = `${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + ctx.font = `bold ${ctx.font}`; + } + const y = (colorIndex + 2) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = request.ansiColors[colorIndex]; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.restore(); + } + } + ctx.restore(); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in context) || isFirefox) { + // Don't attempt to clear background colors if createImageBitmap is not supported + if (canvas instanceof HTMLCanvasElement) { + // Just return the HTMLCanvas if it's a HTMLCanvasElement + return canvas; + } else { + // Transfer to an ImageBitmap is this is an OffscreenCanvas + return new Promise(r => r(canvas.transferToImageBitmap())); + } + } + + const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Remove the background color from the image so characters may overlap + const r = parseInt(request.background.substr(1, 2), 16); + const g = parseInt(request.background.substr(3, 2), 16); + const b = parseInt(request.background.substr(5, 2), 16); + clearColor(charAtlasImageData, r, g, b); + + return context.createImageBitmap(charAtlasImageData); +} + +/** + * Makes a partiicular rgb color in an ImageData completely transparent. + */ +function clearColor(imageData: ImageData, r: number, g: number, b: number): void { + 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; + } + } +} diff --git a/src/utils/Browser.ts b/src/shared/utils/Browser.ts similarity index 75% rename from src/utils/Browser.ts rename to src/shared/utils/Browser.ts index 48c0c374..1d4774d2 100644 --- a/src/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -3,8 +3,6 @@ * @license MIT */ -import { contains } from './Generic'; - const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; @@ -20,3 +18,12 @@ export const isIpad = platform === 'iPad'; export const isIphone = platform === 'iPhone'; export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); export const isLinux = platform.indexOf('Linux') >= 0; + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +export function contains(arr: any[], el: any): boolean { + return arr.indexOf(el) >= 0; +}; diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts deleted file mode 100644 index 82373617..00000000 --- a/src/utils/Generic.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -/** - * Return if the given array contains the given element - * @param {Array} array The array to search for the given element. - * @param {Object} el The element to look for into the array - */ -export function contains(arr: any[], el: any): boolean { - return arr.indexOf(el) >= 0; -}; From e49d84428451af0c0d0b092e62bca56ac359bfe5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Jan 2018 13:46:27 -0800 Subject: [PATCH 2/2] Add note in README about code structure --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1719efb2..05c26f84 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,10 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. +### Code structure + +`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker. + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.