Skip to content

Commit

Permalink
Merge pull request #1198 from Tyriar/955_worker_refactor
Browse files Browse the repository at this point in the history
 Refactor char atlas to enable generation with worker
  • Loading branch information
Tyriar authored Jan 25, 2018
2 parents 92e85e0 + a22b5c0 commit 8f1a629
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 147 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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.
Expand Down
3 changes: 2 additions & 1 deletion src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
*/

import { ICharset, ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData, FontWeight } from './Types';
import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
import { FontWeight } from './shared/Types';

export interface IBrowser {
isNode: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/SelectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 1 addition & 4 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
Expand All @@ -44,7 +44,6 @@ import { BELL_SOUND } 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.
Expand Down Expand Up @@ -591,8 +590,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.document = this.parent.ownerDocument;
this.body = <HTMLBodyElement>this.document.body;

initializeCharAtlas(this.document);

// Create main element container
this.element = this.document.createElement('div');
this.element.classList.add('terminal');
Expand Down
2 changes: 0 additions & 2 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,3 @@ export enum LinkHoverEventTypes {
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
}

export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
146 changes: 23 additions & 123 deletions src/renderer/CharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -65,8 +66,28 @@ 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,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
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, terminal.options.fontWeight, terminal.options.fontWeightBold, colors.background, colors.foreground, colors.ansi),
bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig),
config: newConfig,
ownedBy: [terminal]
};
Expand Down Expand Up @@ -109,124 +130,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, fontWeight: string, fontWeightBold: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
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 = this._getFont(fontWeight, fontSize, 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 = this._getFont(fontWeightBold, fontSize, fontFamily);
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 = this._getFont(fontWeight, fontSize, fontFamily);
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = this._getFont(fontWeightBold, fontSize, fontFamily);
}
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;
}
}
}

private _getFont(fontWeight: string, fontSize: number, fontFamily: string): string {
return `${fontWeight} ${fontSize * window.devicePixelRatio}px ${fontFamily}`;
}
}
140 changes: 140 additions & 0 deletions src/shared/CharAtlasGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { isFirefox } from './utils/Browser';
import { FontWeight } from './Types';

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;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
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<ImageBitmap> {
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 = getFont(request.fontWeight, request);
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 = getFont(request.fontWeightBold, request);
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 = getFont(request.fontWeight, request);
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
ctx.font = getFont(request.fontWeightBold, request);
}
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;
}
}
}

function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string {
return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`;
}
6 changes: 6 additions & 0 deletions src/shared/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/

export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
11 changes: 9 additions & 2 deletions src/utils/Browser.ts → src/shared/utils/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
};
Loading

0 comments on commit 8f1a629

Please sign in to comment.