From 2cb6593b6b8070bd68f46e162ebf8f54cb9ef5f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 21:44:41 -0700 Subject: [PATCH] Move fg rendering into IRenderLayer --- src/RendererCanvas.ts | 286 +++++++------------------- src/renderer/BackgroundRenderLayer.ts | 29 +-- src/renderer/Color.ts | 23 +++ src/renderer/ForegroundRenderLayer.ts | 180 ++++++++++++++++ src/renderer/Interfaces.ts | 2 +- src/renderer/Types.ts | 10 + src/xterm.css | 4 +- 7 files changed, 300 insertions(+), 234 deletions(-) create mode 100644 src/renderer/Color.ts create mode 100644 src/renderer/ForegroundRenderLayer.ts create mode 100644 src/renderer/Types.ts diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 93466221..bf952b6c 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -8,6 +8,7 @@ import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; import { createBackgroundFillData } from './renderer/Canvas'; import { IRenderLayer } from './renderer/Interfaces'; import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; +import { ForegroundRenderLayer } from './renderer/ForegroundRenderLayer'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -16,79 +17,42 @@ import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; */ const MAX_REFRESH_FRAME_SKIP = 5; -/** - * Flags used to render terminal text properly. - */ -enum FLAGS { - BOLD = 1, - UNDERLINE = 2, - BLINK = 4, - INVERSE = 8, - INVISIBLE = 16 -}; - -let brokenBold: boolean = null; - export class Renderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; - private _textCanvasElement: HTMLCanvasElement; - private _textCanvasContext: CanvasRenderingContext2D; - private _offscreenCanvas: HTMLCanvasElement; - private _offscreenContext: CanvasRenderingContext2D; + // private _textCanvasElement: HTMLCanvasElement; + // private _textCanvasContext: CanvasRenderingContext2D; + // private _offscreenCanvas: HTMLCanvasElement; + // private _offscreenContext: CanvasRenderingContext2D; private _renderLayers: IRenderLayer[]; - private _charAtlasBitmap;//: ImageBitmap; + // private _charAtlasBitmap;//: ImageBitmap; // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects - private _imageDataCache = {}; - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - private _imageData: ImageData; + // private _imageDataCache = {}; + // private _imageData: ImageData; constructor(private _terminal: ITerminal) { // Figure out whether boldness affects // the character width of monospace fonts. - if (brokenBold === null) { - brokenBold = checkBoldBroken(this._terminal.element); - } + // if (brokenBold === null) { + // brokenBold = checkBoldBroken(this._terminal.element); + // } - this._textCanvasElement = document.createElement('canvas'); - this._textCanvasElement.classList.add('xterm-text-canvas'); - this._textCanvasContext = this._textCanvasElement.getContext('2d'); - this._textCanvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); - - this._offscreenCanvas = document.createElement('canvas'); - this._offscreenContext = this._offscreenCanvas.getContext('2d'); - this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); + // this._offscreenCanvas = document.createElement('canvas'); + // this._offscreenContext = this._offscreenCanvas.getContext('2d'); + // this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); this._renderLayers = [ - new BackgroundRenderLayer(this._terminal.element) + new BackgroundRenderLayer(this._terminal.element), + new ForegroundRenderLayer(this._terminal.element) ]; - this._terminal.element.appendChild(this._textCanvasElement); + // this._terminal.element.appendChild(this._textCanvasElement); // TODO: Pull more DOM interactions into Renderer.constructor, element for // example should be owned by Renderer (and also exposed by Terminal due to @@ -96,56 +60,63 @@ export class Renderer { } public onResize(cols: number, rows: number): void { - // TODO: Could be triggered immediately after onCharSizeChanged + // TODO: This could be triggered immediately after onCharSizeChanged? + const charWidth = this._terminal.charMeasure.width; + const charHeight = this._terminal.charMeasure.height; + const width = Math.ceil(charWidth) * this._terminal.cols; + const height = Math.ceil(charHeight) * this._terminal.rows; + for (let i = 0; i < this._renderLayers.length; i++) { + this._renderLayers[i].resize(width, height, charWidth, charHeight, false); + } } public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; - this._textCanvasElement.width = width * window.devicePixelRatio; - this._textCanvasElement.height = height * window.devicePixelRatio; - this._textCanvasElement.style.width = `${width}px`; - this._textCanvasElement.style.height = `${height}px`; - this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; - this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; - this._refreshCharImageDataAtlas(); + // this._textCanvasElement.width = width * window.devicePixelRatio; + // this._textCanvasElement.height = height * window.devicePixelRatio; + // this._textCanvasElement.style.width = `${width}px`; + // this._textCanvasElement.style.height = `${height}px`; + // this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; + // this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; + // this._refreshCharImageDataAtlas(); for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(width, height, charWidth, charHeight); + this._renderLayers[i].resize(width, height, charWidth, charHeight, true); } } - private _refreshCharImageDataAtlas(): void { - const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + // private _refreshCharImageDataAtlas(): void { + // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - this._offscreenContext.save(); - this._offscreenContext.fillStyle = '#ffffff'; - this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; - this._offscreenContext.textBaseline = 'top'; - // Default color - for (let i = 0; i < 256; i++) { - this._offscreenContext.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._offscreenContext.font = `bold ${this._offscreenContext.font}`; - } - for (let i = 0; i < 256; i++) { - this._offscreenContext.fillStyle = this._colors[colorIndex]; - this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); - } - } - this._offscreenContext.restore(); + // this._offscreenContext.save(); + // this._offscreenContext.fillStyle = '#ffffff'; + // this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; + // this._offscreenContext.textBaseline = 'top'; + // // Default color + // for (let i = 0; i < 256; i++) { + // this._offscreenContext.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._offscreenContext.font = `bold ${this._offscreenContext.font}`; + // } + // for (let i = 0; i < 256; i++) { + // this._offscreenContext.fillStyle = this._colors[colorIndex]; + // this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + // } + // } + // this._offscreenContext.restore(); - const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - (window).createImageBitmap(charAtlasImageData).then(bitmap => { - this._charAtlasBitmap = bitmap; - }); + // const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + // (window).createImageBitmap(charAtlasImageData).then(bitmap => { + // this._charAtlasBitmap = bitmap; + // }); - this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - } + // this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + // } /** * Queues a refresh between two rows (inclusive), to be done on next animation @@ -197,7 +168,7 @@ export class Renderer { } this._refreshRowsQueue = []; this._refreshAnimationFrame = null; - this._refresh(start, end); + // this._refresh(start, end); for (let i = 0; i < this._renderLayers.length; i++) { this._renderLayers[i].render(this._terminal, start, end); } @@ -225,107 +196,10 @@ export class Renderer { * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) */ - private _refresh(start: number, end: number): void { - const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - const textCtx = this._textCanvasContext; - - // TODO: Needs to react to terminal resize - // Initialize image data - if (!this._imageData) { - this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); - this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); - } - - // Don't bother rendering until the atlas bitmap is ready - if (!this._charAtlasBitmap) { - return; - } - - // console.log('fill', start, end); - // console.log('fill', start * charHeight, (end - start + 1) * charHeight); - // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); - textCtx.fillStyle = '#ffffff'; - textCtx.textBaseline = 'top'; - textCtx.font = `${16 * window.devicePixelRatio}px courier`; - - // Clear out the old data - textCtx.clearRect(0, start * scaledCharHeight, scaledCharWidth * this._terminal.cols, (end - start + 1) * scaledCharHeight); - - for (let y = start; y <= end; y++) { - let row = y + this._terminal.buffer.ydisp; - let line = this._terminal.buffer.lines.get(row); - for (let x = 0; x < this._terminal.cols; x++) { - textCtx.save(); - - let data: number = line[x][0]; - // const ch = line[x][CHAR_DATA_CHAR_INDEX]; - const code: number = line[x][3]; - - // if (ch === ' ') { - // continue; - // } - - // let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; - - if (flags & FLAGS.BOLD) { - textCtx.font = `bold ${textCtx.font}`; - // Convert the FG color to the bold variant - if (fg < 8) { - fg += 8; - } - } - - // if (fg < 16) { - // ctx.fillStyle = this._colors[fg]; - // } else if (fg < 256) { - // // TODO: Support colors 16-255 - // } - - let colorIndex = 0; - if (fg < 16) { - colorIndex = fg + 1; - } - - // Simulate cache - // let imageData; - // let key = ch + data; - // if (key in this._imageDataCache) { - // imageData = this._imageDataCache[key]; - // } else { - // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); - // if (flags & FLAGS.UNDERLINE) { - // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); - // } - // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - // this._imageDataCache[key] = imageData; - // } - // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); - - // TODO: Try to get atlas working - // This seems too slow :( - // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - - // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - - // ImageBitmap's draw about twice as fast as from a canvas - textCtx.drawImage(this._charAtlasBitmap, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - - // Always write text - // ctx.fillText(ch, x * charWidth, y * charHeight); - textCtx.restore(); - } - - - // TODO: Try to get atlas working - // ctx.putImageData(this._charImageDataAtlas, y * scaledCharWidth, y * scaledCharHeight, 65 * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - } - - // This draws the atlas (for debugging purposes) - // ctx.putImageData(this._charImageDataAtlas, 0, 0); - } + // private _refresh(start: number, end: number): void { + // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + // } /** * Refreshes the selection in the DOM. @@ -390,16 +264,16 @@ export class Renderer { // If bold is broken, we can't use it in the terminal. -function checkBoldBroken(terminalElement: HTMLElement): boolean { - const document = terminalElement.ownerDocument; - const el = document.createElement('span'); - el.innerHTML = 'hello world'; - terminalElement.appendChild(el); - const w1 = el.offsetWidth; - const h1 = el.offsetHeight; - el.style.fontWeight = 'bold'; - const w2 = el.offsetWidth; - const h2 = el.offsetHeight; - terminalElement.removeChild(el); - return w1 !== w2 || h1 !== h2; -} +// function checkBoldBroken(terminalElement: HTMLElement): boolean { +// const document = terminalElement.ownerDocument; +// const el = document.createElement('span'); +// el.innerHTML = 'hello world'; +// terminalElement.appendChild(el); +// const w1 = el.offsetWidth; +// const h1 = el.offsetHeight; +// el.style.fontWeight = 'bold'; +// const w2 = el.offsetWidth; +// const h2 = el.offsetHeight; +// terminalElement.removeChild(el); +// return w1 !== w2 || h1 !== h2; +// } diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index a624ef2e..4f8e9ea7 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,42 +1,21 @@ import { IRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { TANGO_COLORS } from './Color'; export class BackgroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; - // TODO: Pull colors into some other class - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - constructor(container: HTMLElement) { this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-bg-canvas'); + this._canvas.classList.add('xterm-bg-layer'); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); } - public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void { + public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; @@ -58,7 +37,7 @@ export class BackgroundRenderLayer implements IRenderLayer { // TODO: Draw background if (bg < 16) { this._ctx.save(); - this._ctx.fillStyle = this._colors[bg]; + this._ctx.fillStyle = TANGO_COLORS[bg]; this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); } else { diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts new file mode 100644 index 00000000..250c5c50 --- /dev/null +++ b/src/renderer/Color.ts @@ -0,0 +1,23 @@ +// TODO: Ideally colors would be exposed through some theme manager since colors +// are moving to JS. + +export const TANGO_COLORS = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' +]; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts new file mode 100644 index 00000000..adf6c10a --- /dev/null +++ b/src/renderer/ForegroundRenderLayer.ts @@ -0,0 +1,180 @@ +import { IRenderLayer } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { TANGO_COLORS } from './Color'; +import { FLAGS } from './Types'; + +export class ForegroundRenderLayer implements IRenderLayer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _charAtlas: ImageBitmap; + + private _charAtlasGenerator: CharAtlasGenerator; + + constructor(container: HTMLElement) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-fg-layer'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + container.appendChild(this._canvas); + this._charAtlasGenerator = new CharAtlasGenerator(); + } + + public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + if (charSizeChanged) { + this._charAtlas = null; + this._charAtlasGenerator.generate(charWidth, charHeight).then(bitmap => { + this._charAtlas = bitmap; + }); + } + } + + public render(terminal: ITerminal, startRow: number, endRow: number): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + + // TODO: Needs to react to terminal resize + // Initialize image data + // if (!this._imageData) { + // this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); + // this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); + // } + + // TODO: Ensure that the render is eventually performed + // Don't bother render until the atlas bitmap is ready + if (!this._charAtlas) { + return; + } + + // console.log('fill', start, end); + // console.log('fill', start * charHeight, (end - start + 1) * charHeight); + // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); + this._ctx.fillStyle = '#ffffff'; + this._ctx.textBaseline = 'top'; + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + + // Clear out the old data + // TODO: This should be optimised, we don't want to rewrite every character + this._ctx.clearRect(0, startRow * scaledCharHeight, scaledCharWidth * terminal.cols, (endRow - startRow + 1) * scaledCharHeight); + + for (let y = startRow; y <= endRow; y++) { + let row = y + terminal.buffer.ydisp; + let line = terminal.buffer.lines.get(row); + for (let x = 0; x < terminal.cols; x++) { + console.log('fg', x, y); + this._ctx.save(); + + let data: number = line[x][0]; + // const ch = line[x][CHAR_DATA_CHAR_INDEX]; + const code: number = line[x][3]; + + // if (ch === ' ') { + // continue; + // } + + // let bg = data & 0x1ff; + let fg = (data >> 9) & 0x1ff; + let flags = data >> 18; + + if (flags & FLAGS.BOLD) { + this._ctx.font = `bold ${this._ctx.font}`; + // Convert the FG color to the bold variant + if (fg < 8) { + fg += 8; + } + } + + // if (fg < 16) { + // ctx.fillStyle = this._colors[fg]; + // } else if (fg < 256) { + // // TODO: Support colors 16-255 + // } + + let colorIndex = 0; + if (fg < 16) { + colorIndex = fg + 1; + } + + // Simulate cache + // let imageData; + // let key = ch + data; + // if (key in this._imageDataCache) { + // imageData = this._imageDataCache[key]; + // } else { + // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); + // if (flags & FLAGS.UNDERLINE) { + // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); + // } + // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + // this._imageDataCache[key] = imageData; + // } + // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); + + // TODO: Try to get atlas working + // This seems too slow :( + // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); + + // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + + // ImageBitmap's draw about twice as fast as from a canvas + this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.restore(); + } + } + + // This draws the atlas (for debugging purposes) + this._ctx.drawImage(this._charAtlas, 0, 0); + } +} + +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(charWidth: number, charHeight: number): Promise { + const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; + + this._canvas.width = 255 * scaledCharWidth; + this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; + + this._ctx.save(); + this._ctx.fillStyle = '#ffffff'; + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + 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}`; + } + for (let i = 0; i < 256; i++) { + this._ctx.fillStyle = TANGO_COLORS[colorIndex]; + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + } + } + 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; + } +} diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 6e0c3933..f9572525 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,6 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { - resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void; + resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void; render(terminal: ITerminal, startRow: number, endRow: number): void; } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts new file mode 100644 index 00000000..09e74439 --- /dev/null +++ b/src/renderer/Types.ts @@ -0,0 +1,10 @@ +/** + * Flags used to render terminal text properly. + */ +export enum FLAGS { + BOLD = 1, + UNDERLINE = 2, + BLINK = 4, + INVERSE = 8, + INVISIBLE = 16 +}; diff --git a/src/xterm.css b/src/xterm.css index 321cf6d6..2ee98e54 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,11 +164,11 @@ top: 0; } -.terminal .xterm-bg-canvas { +.terminal .xterm-bg-layer { z-index: 0; } -.terminal .xterm-text-canvas { +.terminal .xterm-fg-layer { z-index: 1; }