Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/minimap/minimapCharRendererFactory.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { MinimapCharRenderer } from './minimapCharRenderer.js';
7
import { allCharCodes, Constants } from './minimapCharSheet.js';
8
import { prebakedMiniMaps } from './minimapPreBaked.js';
9
import { toUint8 } from '../../../../base/common/uint.js';
10
11
/**
12
* Creates character renderers. It takes a 'scale' that determines how large
13
* characters should be drawn. Using this, it draws data into a canvas and
14
* then downsamples the characters as necessary for the current display.
15
* This makes rendering more efficient, rather than drawing a full (tiny)
16
* font, or downsampling in real-time.
17
*/
18
export class MinimapCharRendererFactory {
19
private static lastCreated?: MinimapCharRenderer;
20
private static lastFontFamily?: string;
21
22
/**
23
* Creates a new character renderer factory with the given scale.
24
*/
25
public static create(scale: number, fontFamily: string) {
26
// renderers are immutable. By default we'll 'create' a new minimap
27
// character renderer whenever we switch editors, no need to do extra work.
28
if (this.lastCreated && scale === this.lastCreated.scale && fontFamily === this.lastFontFamily) {
29
return this.lastCreated;
30
}
31
32
let factory: MinimapCharRenderer;
33
if (prebakedMiniMaps[scale]) {
34
factory = new MinimapCharRenderer(prebakedMiniMaps[scale](), scale);
35
} else {
36
factory = MinimapCharRendererFactory.createFromSampleData(
37
MinimapCharRendererFactory.createSampleData(fontFamily).data,
38
scale
39
);
40
}
41
42
this.lastFontFamily = fontFamily;
43
this.lastCreated = factory;
44
return factory;
45
}
46
47
/**
48
* Creates the font sample data, writing to a canvas.
49
*/
50
public static createSampleData(fontFamily: string): ImageData {
51
const canvas = document.createElement('canvas');
52
const ctx = canvas.getContext('2d')!;
53
54
canvas.style.height = `${Constants.SAMPLED_CHAR_HEIGHT}px`;
55
canvas.height = Constants.SAMPLED_CHAR_HEIGHT;
56
canvas.width = Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH;
57
canvas.style.width = Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH + 'px';
58
59
ctx.fillStyle = '#ffffff';
60
ctx.font = `bold ${Constants.SAMPLED_CHAR_HEIGHT}px ${fontFamily}`;
61
ctx.textBaseline = 'middle';
62
63
let x = 0;
64
for (const code of allCharCodes) {
65
ctx.fillText(String.fromCharCode(code), x, Constants.SAMPLED_CHAR_HEIGHT / 2);
66
x += Constants.SAMPLED_CHAR_WIDTH;
67
}
68
69
return ctx.getImageData(0, 0, Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH, Constants.SAMPLED_CHAR_HEIGHT);
70
}
71
72
/**
73
* Creates a character renderer from the canvas sample data.
74
*/
75
public static createFromSampleData(source: Uint8ClampedArray, scale: number): MinimapCharRenderer {
76
const expectedLength =
77
Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT;
78
if (source.length !== expectedLength) {
79
throw new Error('Unexpected source in MinimapCharRenderer');
80
}
81
82
const charData = MinimapCharRendererFactory._downsample(source, scale);
83
return new MinimapCharRenderer(charData, scale);
84
}
85
86
private static _downsampleChar(
87
source: Uint8ClampedArray,
88
sourceOffset: number,
89
dest: Uint8ClampedArray,
90
destOffset: number,
91
scale: number
92
): number {
93
const width = Constants.BASE_CHAR_WIDTH * scale;
94
const height = Constants.BASE_CHAR_HEIGHT * scale;
95
96
let targetIndex = destOffset;
97
let brightest = 0;
98
99
// This is essentially an ad-hoc rescaling algorithm. Standard approaches
100
// like bicubic interpolation are awesome for scaling between image sizes,
101
// but don't work so well when scaling to very small pixel values, we end
102
// up with blurry, indistinct forms.
103
//
104
// The approach taken here is simply mapping each source pixel to the target
105
// pixels, and taking the weighted values for all pixels in each, and then
106
// averaging them out. Finally we apply an intensity boost in _downsample,
107
// since when scaling to the smallest pixel sizes there's more black space
108
// which causes characters to be much less distinct.
109
for (let y = 0; y < height; y++) {
110
// 1. For this destination pixel, get the source pixels we're sampling
111
// from (x1, y1) to the next pixel (x2, y2)
112
const sourceY1 = (y / height) * Constants.SAMPLED_CHAR_HEIGHT;
113
const sourceY2 = ((y + 1) / height) * Constants.SAMPLED_CHAR_HEIGHT;
114
115
for (let x = 0; x < width; x++) {
116
const sourceX1 = (x / width) * Constants.SAMPLED_CHAR_WIDTH;
117
const sourceX2 = ((x + 1) / width) * Constants.SAMPLED_CHAR_WIDTH;
118
119
// 2. Sample all of them, summing them up and weighting them. Similar
120
// to bilinear interpolation.
121
let value = 0;
122
let samples = 0;
123
for (let sy = sourceY1; sy < sourceY2; sy++) {
124
const sourceRow = sourceOffset + Math.floor(sy) * Constants.RGBA_SAMPLED_ROW_WIDTH;
125
const yBalance = 1 - (sy - Math.floor(sy));
126
for (let sx = sourceX1; sx < sourceX2; sx++) {
127
const xBalance = 1 - (sx - Math.floor(sx));
128
const sourceIndex = sourceRow + Math.floor(sx) * Constants.RGBA_CHANNELS_CNT;
129
130
const weight = xBalance * yBalance;
131
samples += weight;
132
value += ((source[sourceIndex] * source[sourceIndex + 3]) / 255) * weight;
133
}
134
}
135
136
const final = value / samples;
137
brightest = Math.max(brightest, final);
138
dest[targetIndex++] = toUint8(final);
139
}
140
}
141
142
return brightest;
143
}
144
145
private static _downsample(data: Uint8ClampedArray, scale: number): Uint8ClampedArray {
146
const pixelsPerCharacter = Constants.BASE_CHAR_HEIGHT * scale * Constants.BASE_CHAR_WIDTH * scale;
147
const resultLen = pixelsPerCharacter * Constants.CHAR_COUNT;
148
const result = new Uint8ClampedArray(resultLen);
149
150
let resultOffset = 0;
151
let sourceOffset = 0;
152
let brightest = 0;
153
for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) {
154
brightest = Math.max(brightest, this._downsampleChar(data, sourceOffset, result, resultOffset, scale));
155
resultOffset += pixelsPerCharacter;
156
sourceOffset += Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT;
157
}
158
159
if (brightest > 0) {
160
const adjust = 255 / brightest;
161
for (let i = 0; i < resultLen; i++) {
162
result[i] *= adjust;
163
}
164
}
165
166
return result;
167
}
168
}
169
170