Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor char atlas to enable generation with worker #1198

Merged
merged 3 commits into from
Jan 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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