Add theme support

This commit is contained in:
Daniel Imms
2017-09-01 15:50:18 -07:00
parent 076156b66e
commit 62fea4d3ed
13 changed files with 278 additions and 181 deletions
+22
View File
@@ -294,3 +294,25 @@ export interface IInputHandler {
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
// cursor?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+8 -1
View File
@@ -39,7 +39,7 @@ import * as Mouse from './utils/Mouse';
import { CHARSETS } from './Charsets';
import { getRawByteCoords } from './utils/Mouse';
import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces';
import { BellSound } from './utils/Sounds';
// Declare for RequireJS in loadAddon
@@ -412,6 +412,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea.focus();
}
public setTheme(theme: ITheme): void {
// TODO: Allow setting of theme before renderer is ready
if (this.renderer) {
this.renderer.setTheme(theme);
}
}
/**
* Retrieves an option's value from the terminal.
* @param {string} key The option key.
+4 -5
View File
@@ -1,7 +1,6 @@
import { IDataRenderLayer } from './Interfaces';
import { IDataRenderLayer, IColorSet } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { COLORS } from './Color';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
@@ -9,8 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer';
export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer {
private _state: GridCache<number>;
constructor(container: HTMLElement, zIndex: number) {
super(container, 'bg', zIndex);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'bg', zIndex, colors);
this._state = new GridCache<number>();
}
@@ -47,7 +46,7 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende
if (needsRefresh) {
if (bg < 256) {
this._ctx.save();
this._ctx.fillStyle = COLORS[bg];
this._ctx.fillStyle = this.colors.ansi[bg];
this.fillCells(x, y, 1, 1);
this._ctx.restore();
this._state.cache[x][y] = bg;
+26 -76
View File
@@ -1,6 +1,6 @@
import { IRenderLayer } from './Interfaces';
import { IRenderLayer, IColorSet } from './Interfaces';
import { ITerminal, ITerminalOptions } from '../Interfaces';
import { COLORS } from './Color';
import { acquireCharAtlas } from '../utils/CharAtlas';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -8,28 +8,34 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private scaledCharWidth: number;
private scaledCharHeight: number;
// TODO: This will apply to all terminals, should it be per-terminal?
private static _charAtlas: ImageBitmap;
private static _charAtlasCharWidth: number;
private static _charAtlasCharHeight: number;
private static _charAtlasGenerator: CharAtlasGenerator;
// TODO: This should be shared between terminals, but not for static as some
// terminals may have different styles
private _charAtlas: ImageBitmap;
constructor(container: HTMLElement, id: string, zIndex: number) {
constructor(
container: HTMLElement,
id: string,
zIndex: number,
protected colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
container.appendChild(this._canvas);
if (!BaseRenderLayer._charAtlasGenerator) {
BaseRenderLayer._charAtlasGenerator = new CharAtlasGenerator();
}
}
// TODO: Should this do anything?
public onOptionsChanged(options: ITerminal): void {}
public onCursorMove(options: ITerminal): void {}
// TODO: Should this do anything?
public onOptionsChanged(terminal: ITerminal): void {}
public onCursorMove(terminal: ITerminal): void {}
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
this._charAtlas = null;
acquireCharAtlas(terminal, this.colors).then(bitmap => {
this._charAtlas = bitmap;
});
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio;
@@ -40,17 +46,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._canvas.style.height = `${canvasHeight}px`;
if (charSizeChanged) {
// Only update the char atlas if an update for the right dimensions is not
// already in progress
if (BaseRenderLayer._charAtlasCharWidth !== terminal.charMeasure.width ||
BaseRenderLayer._charAtlasCharHeight !== terminal.charMeasure.height) {
BaseRenderLayer._charAtlas = null;
BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width;
BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height;
BaseRenderLayer._charAtlasGenerator.generate(terminal, this.scaledCharWidth, this.scaledCharHeight).then(bitmap => {
BaseRenderLayer._charAtlas = bitmap;
});
}
acquireCharAtlas(terminal, this.colors).then(bitmap => {
this._charAtlas = bitmap;
});
}
}
@@ -91,7 +89,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
if (code < 256 && (colorIndex > 0 || fg > 255)) {
// ImageBitmap's draw about twice as fast as from a canvas
this._ctx.drawImage(BaseRenderLayer._charAtlas,
this._ctx.drawImage(this._charAtlas,
code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight,
x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight);
} else {
@@ -108,7 +106,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// 256 color support
if (fg < 256) {
this._ctx.fillStyle = COLORS[fg];
this._ctx.fillStyle = this.colors.ansi[fg];
} else {
this._ctx.fillStyle = '#ffffff';
}
@@ -119,51 +117,3 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor() {
this._canvas = document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(terminal: ITerminal, scaledCharWidth: number, scaledCharHeight: number): Promise<ImageBitmap> {
this._canvas.width = 255 * scaledCharWidth;
this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight;
this._ctx.save();
this._ctx.fillStyle = '#ffffff';
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
this._ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0);
}
// Colors 0-15
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 + 1) * scaledCharHeight;
// Clear rectangle as some fonts seem to draw over the bottom boundary
this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight);
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.fillStyle = COLORS[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y);
}
}
this._ctx.restore();
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
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;
}
}
-70
View File
@@ -1,70 +0,0 @@
// TODO: Ideally colors would be exposed through some theme manager since colors
// are moving to JS.
export enum COLOR_CODES {
BLACK = 0,
RED = 1,
GREEN = 2,
YELLOW = 3,
BLUE = 4,
MAGENTA = 5,
CYAN = 6,
WHITE = 7,
BRIGHT_BLACK = 8,
BRIGHT_RED = 9,
BRIGHT_GREEN = 10,
BRIGHT_YELLOW = 11,
BRIGHT_BLUE = 12,
BRIGHT_MAGENTA = 13,
BRIGHT_CYAN = 14,
BRIGHT_WHITE = 15
}
const TANGO_COLORS = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
export const COLORS: string[] = generate256Colors(TANGO_COLORS);
function generate256Colors(first16Colors: string[]): string[] {
let colors = first16Colors.slice();
// Generate colors (16-231)
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = toPaddedHex(v[(i / 36) % 6 | 0]);
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
const b = toPaddedHex(v[i % 6]);
colors.push(`#${r}${g}${b}`);
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = toPaddedHex(8 + i * 10);
colors.push(`#${c}${c}${c}`);
}
return colors;
}
function toPaddedHex(c: number): string {
let s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
+104
View File
@@ -0,0 +1,104 @@
import { IColorSet } from './Interfaces';
import { ITheme } from '../Interfaces';
// TODO: Ideally colors would be exposed through some theme manager since colors
// are moving to JS.
export enum COLOR_CODES {
BLACK = 0,
RED = 1,
GREEN = 2,
YELLOW = 3,
BLUE = 4,
MAGENTA = 5,
CYAN = 6,
WHITE = 7,
BRIGHT_BLACK = 8,
BRIGHT_RED = 9,
BRIGHT_GREEN = 10,
BRIGHT_YELLOW = 11,
BRIGHT_BLUE = 12,
BRIGHT_MAGENTA = 13,
BRIGHT_CYAN = 14,
BRIGHT_WHITE = 15
}
const DEFAULT_ANSI_COLORS = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
function generate256Colors(first16Colors: string[]): string[] {
let colors = first16Colors.slice();
// Generate colors (16-231)
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = toPaddedHex(v[(i / 36) % 6 | 0]);
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
const b = toPaddedHex(v[i % 6]);
colors.push(`#${r}${g}${b}`);
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = toPaddedHex(8 + i * 10);
colors.push(`#${c}${c}${c}`);
}
return colors;
}
function toPaddedHex(c: number): string {
let s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
export class ColorManager {
public colors: IColorSet;
constructor() {
this.colors = {
foreground: '#ffffff',
background: '#000000',
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
};
}
public setTheme(theme: ITheme): void {
if (theme.foreground) this.colors.foreground = theme.foreground;
if (theme.background) this.colors.background = theme.background;
if (theme.black) this.colors.ansi[0] = theme.black;
if (theme.red) this.colors.ansi[1] = theme.red;
if (theme.green) this.colors.ansi[2] = theme.green;
if (theme.yellow) this.colors.ansi[3] = theme.yellow;
if (theme.blue) this.colors.ansi[4] = theme.blue;
if (theme.magenta) this.colors.ansi[5] = theme.magenta;
if (theme.cyan) this.colors.ansi[6] = theme.cyan;
if (theme.white) this.colors.ansi[7] = theme.white;
if (theme.brightBlack) this.colors.ansi[8] = theme.brightBlack;
if (theme.brightRed) this.colors.ansi[9] = theme.brightRed;
if (theme.brightGreen) this.colors.ansi[10] = theme.brightGreen;
if (theme.brightYellow) this.colors.ansi[11] = theme.brightYellow;
if (theme.brightBlue) this.colors.ansi[12] = theme.brightBlue;
if (theme.brightMagenta) this.colors.ansi[13] = theme.brightMagenta;
if (theme.brightCyan) this.colors.ansi[14] = theme.brightCyan;
if (theme.brightWhite) this.colors.ansi[15] = theme.brightWhite;
}
}
+5 -10
View File
@@ -1,11 +1,11 @@
import { IDataRenderLayer } from './Interfaces';
import { IDataRenderLayer, IColorSet } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { COLORS, COLOR_CODES } from './Color';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData } from '../Types';
import { COLOR_CODES } from './ColorManager';
/**
* The time between cursor blinks.
@@ -17,8 +17,8 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
constructor(container: HTMLElement, zIndex: number) {
super(container, 'cursor', zIndex);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'cursor', zIndex, colors);
this._state = null;
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
@@ -98,7 +98,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
this._ctx.save();
this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE];
this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE];
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
this._ctx.restore();
this._state = [terminal.buffer.x, viewportRelativeCursorY];
@@ -157,7 +157,6 @@ class CursorBlinkStateManager {
}
public restartBlinkAnimation(terminal: ITerminal): void {
console.log('restartBlinkAnimation');
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
@@ -176,7 +175,6 @@ class CursorBlinkStateManager {
window.clearInterval(this._blinkInterval);
}
console.log('restartInterval');
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
@@ -191,7 +189,6 @@ class CursorBlinkStateManager {
return;
}
console.log('timeout');
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
@@ -201,14 +198,12 @@ class CursorBlinkStateManager {
// Setup the blink interval
this._blinkInterval = <number><any>setInterval(() => {
console.log('interval');
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = null;
console.log(' restart in ', time);
this._restartInterval(time);
return;
}
+3 -4
View File
@@ -1,7 +1,6 @@
import { IDataRenderLayer } from './Interfaces';
import { IDataRenderLayer, IColorSet } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { COLORS } from './Color';
import { FLAGS } from './Types';
import { GridCache } from './GridCache';
import { CharData } from '../Types';
@@ -10,8 +9,8 @@ import { BaseRenderLayer } from './BaseRenderLayer';
export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer {
private _state: GridCache<CharData>;
constructor(container: HTMLElement, zIndex: number) {
super(container, 'fg', zIndex);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'fg', zIndex, colors);
this._state = new GridCache<CharData>();
}
+10 -2
View File
@@ -1,8 +1,9 @@
import { ITerminal, ITerminalOptions } from '../Interfaces';
export interface IRenderLayer {
onCursorMove(options: ITerminal): void;
onOptionsChanged(options: ITerminal): void;
onCursorMove(terminal: ITerminal): void;
onOptionsChanged(terminal: ITerminal): void;
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Resize the render layer.
@@ -28,3 +29,10 @@ export interface IDataRenderLayer extends IRenderLayer {
export interface ISelectionRenderLayer extends IRenderLayer {
render(terminal: ITerminal, start: [number, number], end: [number, number]): void;
}
export interface IColorSet {
foreground: string;
background: string;
ansi: string[];
}
+27 -5
View File
@@ -2,7 +2,7 @@
* @license MIT
*/
import { ITerminal } from '../Interfaces';
import { ITerminal, ITheme } from '../Interfaces';
import { DomElementObjectPool } from '../utils/DomElementObjectPool';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { createBackgroundFillData } from './Canvas';
@@ -11,6 +11,8 @@ import { BackgroundRenderLayer } from './BackgroundRenderLayer';
import { ForegroundRenderLayer } from './ForegroundRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
export class Renderer {
/** A queue of the rows to be refreshed */
@@ -20,17 +22,37 @@ export class Renderer {
private _dataRenderLayers: IDataRenderLayer[];
private _selectionRenderLayers: ISelectionRenderLayer[];
private _colorManager: ColorManager;
constructor(private _terminal: ITerminal) {
this._colorManager = new ColorManager();
this._dataRenderLayers = [
new BackgroundRenderLayer(this._terminal.element, 0),
new ForegroundRenderLayer(this._terminal.element, 2),
new CursorRenderLayer(this._terminal.element, 3)
new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors),
new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors),
new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors)
];
this._selectionRenderLayers = [
new SelectionRenderLayer(this._terminal.element, 1)
new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors)
];
}
public setTheme(theme: ITheme): void {
console.log('setTheme');
this._colorManager.setTheme(theme);
// Clear layers and force a full render
for (let i = 0; i < this._dataRenderLayers.length; i++) {
this._dataRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors);
this._dataRenderLayers[i].reset(this._terminal);
}
for (let i = 0; i < this._selectionRenderLayers.length; i++) {
this._selectionRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors);
this._selectionRenderLayers[i].reset(this._terminal);
}
// TODO: This is currently done for every single terminal, but it's static so it's wasting time
this._terminal.refresh(0, this._terminal.rows - 1);
}
public onResize(cols: number, rows: number): void {
const width = this._terminal.charMeasure.width * this._terminal.cols;
const height = this._terminal.charMeasure.height * this._terminal.rows;
+3 -3
View File
@@ -1,4 +1,4 @@
import { ISelectionRenderLayer } from './Interfaces';
import { ISelectionRenderLayer, IColorSet } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
@@ -8,8 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionRenderLayer {
private _state: {start: [number, number], end: [number, number]};
constructor(container: HTMLElement, zIndex: number) {
super(container, 'selection', zIndex);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'selection', zIndex, colors);
this._state = {
start: null,
end: null
-5
View File
@@ -8,8 +8,3 @@ export enum FLAGS {
INVERSE = 8,
INVISIBLE = 16
};
export type Point = {
x: number,
y: number
};
+66
View File
@@ -0,0 +1,66 @@
import { ITerminal } from '../Interfaces';
import { IColorSet } from '../renderer/Interfaces';
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promise<ImageBitmap> {
const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio;
const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio;
// TODO: Check to see if the atlas already exists in a cache
return generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi);
}
export function releaseCharAtlas(terminal: ITerminal): void {
// TODO: Release the char atlas if it's no longer needed
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor() {
this._canvas = document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): Promise<ImageBitmap> {
this._canvas.width = 255 * scaledCharWidth;
this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight;
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.fillText(String.fromCharCode(i), i * scaledCharWidth, 0);
}
// Colors 0-15
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 + 1) * scaledCharHeight;
// Clear rectangle as some fonts seem to draw over the bottom boundary
this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight);
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.fillStyle = ansiColors[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y);
}
}
this._ctx.restore();
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
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;
}
}
const generator = new CharAtlasGenerator();