diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 88d929f1d9..94889a65da 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -368,37 +368,43 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _drawChars(cell: ICellData, x: number, y: number): void { const chars = cell.getChars(); this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y); - let glyph: IRasterizedGlyph; + let glyphs: IRasterizedGlyph[]; if (chars && chars.length > 1) { - glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); + glyphs = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } else { - glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); + glyphs = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } - if (!glyph.size.x || !glyph.size.y) { - return; - } - this._ctx.save(); - this._clipRow(y); - // Draw the image, use the bitmap if it's available - if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) { - if (!this._bitmapGenerator[glyph.texturePage]) { - this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + + let glyphsOffsetX = 0; + for (let i = 0; i < glyphs.length; i++) { + const glyph = glyphs[i]; + if (!glyph.size.x || !glyph.size.y) { + continue; } - this._bitmapGenerator[glyph.texturePage]!.refresh(); - this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; + this._ctx.save(); + this._clipRow(y); + // Draw the image, use the bitmap if it's available + if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) { + if (!this._bitmapGenerator[glyph.texturePage]) { + this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + } + this._bitmapGenerator[glyph.texturePage]!.refresh(); + this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; + } + this._ctx.drawImage( + this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, + glyph.texturePosition.x, + glyph.texturePosition.y, + glyph.size.x, + glyph.size.y, + x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x + glyphsOffsetX, + y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, + glyph.size.x, + glyph.size.y + ); + glyphsOffsetX += glyph.size.x - glyph.offset.x; + this._ctx.restore(); } - this._ctx.drawImage( - this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, - glyph.texturePosition.x, - glyph.texturePosition.y, - glyph.size.x, - glyph.size.y, - x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x, - y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, - glyph.size.x, - glyph.size.y - ); - this._ctx.restore(); } /** diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 202d4deba0..04a548b74b 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -84,9 +84,12 @@ const CELL_POSITION_INDICES = 2; // Work variables to avoid garbage collection let $i = 0; +let $j = 0; let $glyph: IRasterizedGlyph | undefined = undefined; +let $glyphs: IRasterizedGlyph[] | undefined = undefined; let $leftCellPadding = 0; let $clippedPixels = 0; +let $offset = 0; export class GlyphRenderer extends Disposable { private readonly _program: WebGLProgram; @@ -236,43 +239,51 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); + $glyphs = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); + $glyphs = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); - if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { - $clippedPixels = $glyph.offset.x - $leftCellPadding; - // a_origin - array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left; - array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; - // a_size - array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; - array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; - // a_texpage - array[$i + 4] = $glyph.texturePage; - // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; - array[$i + 6] = $glyph.texturePositionClipSpace.y; - // a_texsize - array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; - array[$i + 8] = $glyph.sizeClipSpace.y; - } else { - // a_origin - array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; - array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; - // a_size - array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; - array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; - // a_texpage - array[$i + 4] = $glyph.texturePage; - // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x; - array[$i + 6] = $glyph.texturePositionClipSpace.y; - // a_texsize - array[$i + 7] = $glyph.sizeClipSpace.x; - array[$i + 8] = $glyph.sizeClipSpace.y; + $offset = $glyphs[0].offset.x; + + for ($j = 0; $j < $glyphs.length; $j++) { + $i = (y * this._terminal.cols + x + $j) * INDICES_PER_CELL; + $glyph = $glyphs[$j]; + + $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); + if (bg !== lastBg && $offset > $leftCellPadding) { + $clippedPixels = $offset - $leftCellPadding; + // a_origin + array[$i ] = -($offset - $clippedPixels) + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; + // a_size + array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; + // a_texcoord + array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 6] = $glyph.texturePositionClipSpace.y; + // a_texsize + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 8] = $glyph.sizeClipSpace.y; + } else { + // a_origin + array[$i ] = -$offset + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; + // a_size + array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; + // a_texcoord + array[$i + 5] = $glyph.texturePositionClipSpace.x; + array[$i + 6] = $glyph.texturePositionClipSpace.y; + // a_texsize + array[$i + 7] = $glyph.sizeClipSpace.x; + array[$i + 8] = $glyph.sizeClipSpace.y; + } + $offset -= $glyph.size.x - this._dimensions.device.cell.width; } // a_cellpos only changes on resize } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index fcb0dd8096..2f29181ea4 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -366,6 +366,7 @@ export class WebglRenderer extends Disposable implements IRenderer { let i: number; let x: number; let j: number; + let k: number; start = clamp(start, terminal.rows - 1, 0); end = clamp(end, terminal.rows - 1, 0); for (y = start; y <= end; y++) { @@ -434,22 +435,22 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; - this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); - if (isJoined) { // Restore work cell cell = this._workCell; // Null out non-first cells - for (x++; x < lastCharX; x++) { - j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); + for (k = x + 1; k < lastCharX; k++) { + j = ((y * terminal.cols) + k) * RENDER_MODEL_INDICIES_PER_CELL; + this._glyphRenderer!.updateCell(k, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; } } + this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); + x = lastCharX; } } this._rectangleRenderer!.updateBackgrounds(this._model); diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index ce1684653b..16d0bba75c 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -56,8 +56,8 @@ let $glyph = undefined; export class TextureAtlas implements ITextureAtlas { private _didWarmUp: boolean = false; - private _cacheMap: FourKeyMap = new FourKeyMap(); - private _cacheMapCombined: FourKeyMap = new FourKeyMap(); + private _cacheMap: FourKeyMap = new FourKeyMap(); + private _cacheMapCombined: FourKeyMap = new FourKeyMap(); // The texture that the atlas is drawn to private _pages: AtlasPage[] = []; @@ -242,11 +242,11 @@ export class TextureAtlas implements ITextureAtlas { } } - public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph { + public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph[] { return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext); } - public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph { + public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph[] { return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext); } @@ -254,12 +254,12 @@ export class TextureAtlas implements ITextureAtlas { * Gets the glyphs texture coords, drawing the texture if it's not already */ private _getFromCacheMap( - cacheMap: FourKeyMap, + cacheMap: FourKeyMap, key: string | number, bg: number, fg: number, ext: number - ): IRasterizedGlyph { + ): IRasterizedGlyph[] { $glyph = cacheMap.get(key, bg, fg, ext); if (!$glyph) { $glyph = this._drawToCache(key, bg, fg, ext); @@ -420,7 +420,7 @@ export class TextureAtlas implements ITextureAtlas { return color; } - private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { + private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph[] { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; // Uncomment for debugging @@ -432,6 +432,9 @@ export class TextureAtlas implements ITextureAtlas { const allowedWidth = this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2; if (this._tmpCanvas.width < allowedWidth) { this._tmpCanvas.width = allowedWidth; + } else if (this._tmpCanvas.width > allowedWidth * 2) { + // Shrink the canvas if it's too large + this._tmpCanvas.width = allowedWidth; } // Include line height when drawing glyphs const allowedHeight = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4; @@ -446,7 +449,7 @@ export class TextureAtlas implements ITextureAtlas { const invisible = !!this._workAttributeData.isInvisible(); if (invisible) { - return NULL_RASTERIZED_GLYPH; + return [NULL_RASTERIZED_GLYPH]; } const bold = !!this._workAttributeData.isBold(); @@ -640,7 +643,7 @@ export class TextureAtlas implements ITextureAtlas { // Draw the character if (!customGlyph) { - this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight); + this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight, allowedWidth - padding); } // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible @@ -678,157 +681,183 @@ export class TextureAtlas implements ITextureAtlas { // clear the background from the character to avoid issues with drawing over the previous // character if it extends past it's bounds - const imageData = this._tmpCtx.getImageData( - 0, 0, this._tmpCanvas.width, this._tmpCanvas.height - ); - // Clear out the background color and determine if the glyph is empty. - let isEmpty: boolean; - if (!this._config.allowTransparency) { - isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck); + // split the image into multiple images to fit into the texture + const images: ImageData[] = []; + if (this._tmpCanvas.width > this._textureSize) { + const step = this._textureSize - 2 * padding; + + const fullImageData = this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + if (!this._config.allowTransparency) { + clearColor(fullImageData, backgroundColor, foregroundColor, enableClearThresholdCheck); + } + + this._findGlyphBoundingBox(fullImageData, this._workBoundingBox, fullImageData.width, restrictedPowerlineGlyph, customGlyph, padding); + const end = this._workBoundingBox.right; + for (let i = 0; i <= end; i += step) { + const width = Math.min(step, end - i + 1); + const image = this._tmpCtx.getImageData(i, 0, width, this._tmpCanvas.height); + images.push(image); + } } else { - isEmpty = checkCompletelyTransparent(imageData); + images.push(this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height)); } - // Handle empty glyphs - if (isEmpty) { - return NULL_RASTERIZED_GLYPH; - } + const rasterizedGlyphs: IRasterizedGlyph[] = []; + for (let i=0; i= 0; i--) { - for (const row of this._activePages[i].fixedRows) { - if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) { - activePage = this._activePages[i]; - activeRow = row; + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, imageData.width, restrictedPowerlineGlyph, customGlyph, padding); + + // Find the best atlas row to use + let activePage: AtlasPage; + let activeRow: ICharAtlasActiveRow; + while (true) { + // If there are no active pages (the last smallest 4 were merged), create a new one + if (this._activePages.length === 0) { + const newPage = this._createNewPage(); + activePage = newPage; + activeRow = newPage.currentRow; + activeRow.height = rasterizedGlyph.size.y; + break; + } + + // Get the best current row from all active pages + activePage = this._activePages[this._activePages.length - 1]; + activeRow = activePage.currentRow; + for (const p of this._activePages) { + if (rasterizedGlyph.size.y <= p.currentRow.height) { + activePage = p; + activeRow = p.currentRow; + } + } + + // TODO: This algorithm could be simplified: + // - Search for the page with ROW_PIXEL_THRESHOLD in mind + // - Keep track of current/fixed rows in a Map + + // Replace the best current row with a fixed row if there is one at least as good as the + // current row. Search in reverse to prioritize filling in older pages. + for (let i = this._activePages.length - 1; i >= 0; i--) { + for (const row of this._activePages[i].fixedRows) { + if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) { + activePage = this._activePages[i]; + activeRow = row; + } } } - } - // Create a new one if too much vertical space would be wasted or there is not enough room - // left in the page. The previous active row will become fixed in the process as it now has a - // fixed height - if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) { + // Create a new one if too much vertical space would be wasted or there is not enough room + // left in the page. The previous active row will become fixed in the process as it now has a + // fixed height + if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) { // Create the new fixed height row, creating a new page if there isn't enough room on the // current page - let wasNewPageCreated = false; - if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) { + let wasNewPageCreated = false; + if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) { // Find the first page with room to create the new row on - let candidatePage: AtlasPage | undefined; - for (const p of this._activePages) { - if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) { - candidatePage = p; - break; + let candidatePage: AtlasPage | undefined; + for (const p of this._activePages) { + if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) { + candidatePage = p; + break; + } } - } - if (candidatePage) { - activePage = candidatePage; - } else { + if (candidatePage) { + activePage = candidatePage; + } else { // Create a new page if there is no room - const newPage = this._createNewPage(); - activePage = newPage; - activeRow = newPage.currentRow; - activeRow.height = rasterizedGlyph.size.y; - wasNewPageCreated = true; + const newPage = this._createNewPage(); + activePage = newPage; + activeRow = newPage.currentRow; + activeRow.height = rasterizedGlyph.size.y; + wasNewPageCreated = true; + } } - } - if (!wasNewPageCreated) { + if (!wasNewPageCreated) { // Fix the current row as the new row is being added below - if (activePage.currentRow.height > 0) { - activePage.fixedRows.push(activePage.currentRow); + if (activePage.currentRow.height > 0) { + activePage.fixedRows.push(activePage.currentRow); + } + activeRow = { + x: 0, + y: activePage.currentRow.y + activePage.currentRow.height, + height: rasterizedGlyph.size.y + }; + activePage.fixedRows.push(activeRow); + + // Create the new current row below the new fixed height row + activePage.currentRow = { + x: 0, + y: activeRow.y + activeRow.height, + height: 0 + }; } - activeRow = { - x: 0, - y: activePage.currentRow.y + activePage.currentRow.height, - height: rasterizedGlyph.size.y - }; - activePage.fixedRows.push(activeRow); - - // Create the new current row below the new fixed height row - activePage.currentRow = { - x: 0, - y: activeRow.y + activeRow.height, - height: 0 - }; - } // TODO: Remove pages from _activePages when all rows are filled - } + } - // Exit the loop if there is enough room in the row - if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) { - break; - } + // Exit the loop if there is enough room in the row + if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) { + break; + } - // If there is not enough room in the current row, finish it and try again - if (activeRow === activePage.currentRow) { - activeRow.x = 0; - activeRow.y += activeRow.height; - activeRow.height = 0; - } else { - activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1); + // If there is not enough room in the current row, finish it and try again + if (activeRow === activePage.currentRow) { + activeRow.x = 0; + activeRow.y += activeRow.height; + activeRow.height = 0; + } else { + activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1); + } } - } - // Record texture position - rasterizedGlyph.texturePage = this._pages.indexOf(activePage); - rasterizedGlyph.texturePosition.x = activeRow.x; - rasterizedGlyph.texturePosition.y = activeRow.y; - rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width; - rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height; - - // Fix the clipspace position as pages may be of differing size - rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width; - rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height; - - // Update atlas current row, for fixed rows the glyph height will never be larger than the row - // height - activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y); - activeRow.x += rasterizedGlyph.size.x; - - // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us - activePage.ctx.putImageData( - imageData, - rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, - rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, - this._workBoundingBox.left, - this._workBoundingBox.top, - rasterizedGlyph.size.x, - rasterizedGlyph.size.y - ); - activePage.addGlyph(rasterizedGlyph); - activePage.version++; + // Record texture position + rasterizedGlyph.texturePage = this._pages.indexOf(activePage); + rasterizedGlyph.texturePosition.x = activeRow.x; + rasterizedGlyph.texturePosition.y = activeRow.y; + rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width; + rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height; + + // Fix the clipspace position as pages may be of differing size + rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width; + rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height; + + // Update atlas current row, for fixed rows the glyph height will never be larger than the row + // height + activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y); + activeRow.x += rasterizedGlyph.size.x; + + // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us + activePage.ctx.putImageData( + imageData, + rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, + rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, + this._workBoundingBox.left, + this._workBoundingBox.top, + rasterizedGlyph.size.x, + rasterizedGlyph.size.y + ); + activePage.addGlyph(rasterizedGlyph); + activePage.version++; + + rasterizedGlyphs.push(rasterizedGlyph); + } - return rasterizedGlyph; + return rasterizedGlyphs; } /** @@ -845,7 +874,7 @@ export class TextureAtlas implements ITextureAtlas { let found = false; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.top = y; found = true; @@ -858,9 +887,9 @@ export class TextureAtlas implements ITextureAtlas { } boundingBox.left = 0; found = false; - for (let x = 0; x < padding + width; x++) { + for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.left = x; found = true; @@ -873,9 +902,9 @@ export class TextureAtlas implements ITextureAtlas { } boundingBox.right = width; found = false; - for (let x = padding + width - 1; x >= padding; x--) { + for (let x = width - 1; x >= 0; x--) { for (let y = 0; y < height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.right = x; found = true; @@ -890,7 +919,7 @@ export class TextureAtlas implements ITextureAtlas { found = false; for (let y = height - 1; y >= 0; y--) { for (let x = 0; x < width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.bottom = y; found = true; diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index cf4513c26a..24f276dd0a 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -107,8 +107,8 @@ export interface ITextureAtlas extends IDisposable { * Clear all glyphs from the texture atlas. */ clearTexture(): void; - getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph; - getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph; + getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph[]; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph[]; } /**