diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 8992fa8a..4b0d9f52 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -15,6 +15,11 @@ const TEXTURE_HEIGHT = 1024; type GlyphCacheKey = string; +interface IGlyphCacheValue { + index: number; + isEmpty: boolean; +} + /** * Removes and returns the oldest element in a map. */ @@ -35,7 +40,7 @@ function getGlyphCacheKey(glyph: IGlyphIdentifier): GlyphCacheKey { export default class DynamicCharAtlas extends BaseCharAtlas { // An ordered map that we're using to keep track of where each glyph is in the atlas texture. // It's ordered so that we can determine when to remove the old entries. - private _cacheMap: Map = new Map(); + private _cacheMap: Map = new Map(); // The texture that the atlas is drawn to private _cacheCanvas: HTMLCanvasElement; @@ -78,23 +83,23 @@ export default class DynamicCharAtlas extends BaseCharAtlas { y: number, ): boolean { const glyphKey = getGlyphCacheKey(glyph); - const index = this._cacheMap.get(glyphKey); - if (index != null) { + const cacheValue = this._cacheMap.get(glyphKey); + if (cacheValue != null) { // move to end of insertion order, so this can behave like an LRU cache this._cacheMap.delete(glyphKey); - this._cacheMap.set(glyphKey, index); - this._drawFromCache(ctx, index, x, y); + this._cacheMap.set(glyphKey, cacheValue); + this._drawFromCache(ctx, cacheValue, x, y); return true; } else if (this._canCache(glyph)) { let index; if (this._cacheMap.size < this._capacity) { index = this._cacheMap.size; } else { - index = mapShift(this._cacheMap)[1]; + index = mapShift(this._cacheMap)[1].index; } - this._drawToCache(glyph, index); - this._cacheMap.set(glyphKey, index); - this._drawFromCache(ctx, index, x, y); + const cacheValue = this._drawToCache(glyph, index); + this._cacheMap.set(glyphKey, cacheValue); + this._drawFromCache(ctx, cacheValue, x, y); return true; } else { return false; @@ -121,11 +126,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas { private _drawFromCache( ctx: CanvasRenderingContext2D, - index: number, + cacheValue: IGlyphCacheValue, x: number, y: number ): void { - const [cacheX, cacheY] = this._toCoordinates(index); + // We don't actually need to do anything if this is whitespace. + if (cacheValue.isEmpty) { + return; + } + const [cacheX, cacheY] = this._toCoordinates(cacheValue.index); ctx.drawImage( this._cacheCanvas, cacheX, @@ -141,7 +150,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { // TODO: We do this (or something similar) in multiple places. We should split this off // into a shared function. - private _drawToCache(glyph: IGlyphIdentifier, index: number): void { + private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue { this._tmpCtx.save(); // no need to clear _tmpCtx, since we're going to draw a fully opaque background @@ -185,11 +194,16 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const imageData = this._tmpCtx.getImageData( 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight, ); - clearColor(imageData, backgroundColor); + const isEmpty = clearColor(imageData, backgroundColor); // copy the data from _tmpCanvas to _cacheCanvas const [x, y] = this._toCoordinates(index); // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us this._cacheCtx.putImageData(imageData, x, y); + + return { + index, + isEmpty, + }; } } diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index 874020c3..3e62c39d 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -108,8 +108,10 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( /** * Makes a partiicular rgb color in an ImageData completely transparent. + * @returns True if the result is "empty", meaning all pixels are fully transparent. */ -export function clearColor(imageData: ImageData, color: IColor): void { +export function clearColor(imageData: ImageData, color: IColor): boolean { + let isEmpty = true; const r = color.rgba >>> 24; const g = color.rgba >>> 16 & 0xFF; const b = color.rgba >>> 8 & 0xFF; @@ -118,8 +120,11 @@ export function clearColor(imageData: ImageData, color: IColor): void { imageData.data[offset + 1] === g && imageData.data[offset + 2] === b) { imageData.data[offset + 3] = 0; + } else { + isEmpty = false; } } + return isEmpty; } function getFont(fontWeight: FontWeight, config: ICharAtlasConfig): string {