/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { MinimapCharRenderer } from '../../../../../vs/editor/browser/viewParts/minimap/minimapCharRenderer'; import { allCharCodes } from '../../../../../vs/editor/browser/viewParts/minimap/minimapCharSheet'; import { prebakedMiniMaps } from '../../../../../vs/editor/browser/viewParts/minimap/minimapPreBaked'; import { Constants } from './minimapCharSheet'; import { toUint8 } from '../../../../../vs/base/common/uint'; /** * Creates character renderers. It takes a 'scale' that determines how large * characters should be drawn. Using this, it draws data into a canvas and * then downsamples the characters as necessary for the current display. * This makes rendering more efficient, rather than drawing a full (tiny) * font, or downsampling in real-time. */ export class MinimapCharRendererFactory { private static lastCreated?: MinimapCharRenderer; private static lastFontFamily?: string; /** * Creates a new character renderer factory with the given scale. */ public static create(scale: number, fontFamily: string) { // renderers are immutable. By default we'll 'create' a new minimap // character renderer whenever we switch editors, no need to do extra work. if ( this.lastCreated && scale === this.lastCreated.scale && fontFamily === this.lastFontFamily ) { return this.lastCreated; } let factory: MinimapCharRenderer; if (prebakedMiniMaps[scale]) { factory = new MinimapCharRenderer(prebakedMiniMaps[scale](), scale); } else { factory = MinimapCharRendererFactory.createFromSampleData( MinimapCharRendererFactory.createSampleData(fontFamily).data, scale ); } this.lastFontFamily = fontFamily; this.lastCreated = factory; return factory; } /** * Creates the font sample data, writing to a canvas. */ public static createSampleData(fontFamily: string): ImageData { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; canvas.style.height = `${Constants.SAMPLED_CHAR_HEIGHT}px`; canvas.height = Constants.SAMPLED_CHAR_HEIGHT; canvas.width = Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH; canvas.style.width = Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH + 'px'; ctx.fillStyle = '#ffffff'; ctx.font = `bold ${Constants.SAMPLED_CHAR_HEIGHT}px ${fontFamily}`; ctx.textBaseline = 'middle'; let x = 0; for (const code of allCharCodes) { ctx.fillText( String.fromCharCode(code), x, Constants.SAMPLED_CHAR_HEIGHT / 2 ); x += Constants.SAMPLED_CHAR_WIDTH; } return ctx.getImageData( 0, 0, Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH, Constants.SAMPLED_CHAR_HEIGHT ); } /** * Creates a character renderer from the canvas sample data. */ public static createFromSampleData( source: Uint8ClampedArray, scale: number ): MinimapCharRenderer { const expectedLength = Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT; if (source.length !== expectedLength) { throw new Error('Unexpected source in MinimapCharRenderer'); } let charData = MinimapCharRendererFactory._downsample(source, scale); return new MinimapCharRenderer(charData, scale); } private static _downsampleChar( source: Uint8ClampedArray, sourceOffset: number, dest: Uint8ClampedArray, destOffset: number, scale: number ): number { const width = Constants.BASE_CHAR_WIDTH * scale; const height = Constants.BASE_CHAR_HEIGHT * scale; let targetIndex = destOffset; let brightest = 0; // This is essentially an ad-hoc rescaling algorithm. Standard approaches // like bicubic interpolation are awesome for scaling between image sizes, // but don't work so well when scaling to very small pixel values, we end // up with blurry, indistinct forms. // // The approach taken here is simply mapping each source pixel to the target // pixels, and taking the weighted values for all pixels in each, and then // averaging them out. Finally we apply an intensity boost in _downsample, // since when scaling to the smallest pixel sizes there's more black space // which causes characters to be much less distinct. for (let y = 0; y < height; y++) { // 1. For this destination pixel, get the source pixels we're sampling // from (x1, y1) to the next pixel (x2, y2) const sourceY1 = (y / height) * Constants.SAMPLED_CHAR_HEIGHT; const sourceY2 = ((y + 1) / height) * Constants.SAMPLED_CHAR_HEIGHT; for (let x = 0; x < width; x++) { const sourceX1 = (x / width) * Constants.SAMPLED_CHAR_WIDTH; const sourceX2 = ((x + 1) / width) * Constants.SAMPLED_CHAR_WIDTH; // 2. Sample all of them, summing them up and weighting them. Similar // to bilinear interpolation. let value = 0; let samples = 0; for (let sy = sourceY1; sy < sourceY2; sy++) { const sourceRow = sourceOffset + Math.floor(sy) * Constants.RGBA_SAMPLED_ROW_WIDTH; const yBalance = 1 - (sy - Math.floor(sy)); for (let sx = sourceX1; sx < sourceX2; sx++) { const xBalance = 1 - (sx - Math.floor(sx)); const sourceIndex = sourceRow + Math.floor(sx) * Constants.RGBA_CHANNELS_CNT; const weight = xBalance * yBalance; samples += weight; value += ((source[sourceIndex] * source[sourceIndex + 3]) / 255) * weight; } } const final = value / samples; brightest = Math.max(brightest, final); dest[targetIndex++] = toUint8(final); } } return brightest; } private static _downsample( data: Uint8ClampedArray, scale: number ): Uint8ClampedArray { const pixelsPerCharacter = Constants.BASE_CHAR_HEIGHT * scale * Constants.BASE_CHAR_WIDTH * scale; const resultLen = pixelsPerCharacter * Constants.CHAR_COUNT; const result = new Uint8ClampedArray(resultLen); let resultOffset = 0; let sourceOffset = 0; let brightest = 0; for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) { brightest = Math.max( brightest, this._downsampleChar(data, sourceOffset, result, resultOffset, scale) ); resultOffset += pixelsPerCharacter; sourceOffset += Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT; } if (brightest > 0) { const adjust = 255 / brightest; for (let i = 0; i < resultLen; i++) { result[i] *= adjust; } } return result; } }