@xterm/addon-webgl 0.20.0-beta.4 → 0.20.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/addon-webgl.js +1 -1
- package/lib/addon-webgl.js.map +1 -1
- package/lib/addon-webgl.mjs +15 -15
- package/lib/addon-webgl.mjs.map +4 -4
- package/package.json +3 -3
- package/src/TextureAtlas.ts +2 -2
- package/src/customGlyphs/CustomGlyphDefinitions.ts +821 -0
- package/src/customGlyphs/CustomGlyphRasterizer.ts +659 -0
- package/src/customGlyphs/Types.ts +61 -0
- package/src/CustomGlyphs.ts +0 -1026
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
|
7
|
+
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
|
8
|
+
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type CustomGlyphRegionDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Try drawing a custom block element or box drawing character, returning whether it was
|
|
12
|
+
* successfully drawn.
|
|
13
|
+
*/
|
|
14
|
+
export function tryDrawCustomGlyph(
|
|
15
|
+
ctx: CanvasRenderingContext2D,
|
|
16
|
+
c: string,
|
|
17
|
+
xOffset: number,
|
|
18
|
+
yOffset: number,
|
|
19
|
+
deviceCellWidth: number,
|
|
20
|
+
deviceCellHeight: number,
|
|
21
|
+
fontSize: number,
|
|
22
|
+
devicePixelRatio: number,
|
|
23
|
+
backgroundColor?: string
|
|
24
|
+
): boolean {
|
|
25
|
+
const unifiedCharDefinition = customGlyphDefinitions[c];
|
|
26
|
+
if (unifiedCharDefinition) {
|
|
27
|
+
switch (unifiedCharDefinition.type) {
|
|
28
|
+
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
|
|
29
|
+
drawBlockVectorChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
30
|
+
return true;
|
|
31
|
+
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
|
32
|
+
drawPatternChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
33
|
+
return true;
|
|
34
|
+
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION:
|
|
35
|
+
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
36
|
+
return true;
|
|
37
|
+
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR:
|
|
38
|
+
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data.pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
39
|
+
drawBlockVectorChar(ctx, unifiedCharDefinition.data.vectors, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
40
|
+
return true;
|
|
41
|
+
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_CLIP_PATH:
|
|
42
|
+
drawBlockPatternWithClipPath(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
43
|
+
return true;
|
|
44
|
+
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
|
45
|
+
case CustomGlyphDefinitionType.PATH:
|
|
46
|
+
drawPathDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
47
|
+
return true;
|
|
48
|
+
case CustomGlyphDefinitionType.PATH_NEGATIVE:
|
|
49
|
+
drawPathNegativeDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, backgroundColor);
|
|
50
|
+
return true;
|
|
51
|
+
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
|
52
|
+
drawVectorShape(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
|
53
|
+
return true;
|
|
54
|
+
case CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT:
|
|
55
|
+
drawPathDefinitionCharacterWithWeight(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function drawBlockVectorChar(
|
|
64
|
+
ctx: CanvasRenderingContext2D,
|
|
65
|
+
charDefinition: ICustomGlyphSolidOctantBlockVector[],
|
|
66
|
+
xOffset: number,
|
|
67
|
+
yOffset: number,
|
|
68
|
+
deviceCellWidth: number,
|
|
69
|
+
deviceCellHeight: number
|
|
70
|
+
): void {
|
|
71
|
+
for (let i = 0; i < charDefinition.length; i++) {
|
|
72
|
+
const box = charDefinition[i];
|
|
73
|
+
const xEighth = deviceCellWidth / 8;
|
|
74
|
+
const yEighth = deviceCellHeight / 8;
|
|
75
|
+
ctx.fillRect(
|
|
76
|
+
xOffset + box.x * xEighth,
|
|
77
|
+
yOffset + box.y * yEighth,
|
|
78
|
+
box.w * xEighth,
|
|
79
|
+
box.h * yEighth
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function drawPathDefinitionCharacter(
|
|
85
|
+
ctx: CanvasRenderingContext2D,
|
|
86
|
+
charDefinition: CustomGlyphPathDrawFunctionDefinition | string,
|
|
87
|
+
xOffset: number,
|
|
88
|
+
yOffset: number,
|
|
89
|
+
deviceCellWidth: number,
|
|
90
|
+
deviceCellHeight: number
|
|
91
|
+
): void {
|
|
92
|
+
const instructions = typeof charDefinition === 'string' ? charDefinition : charDefinition(0, 0);
|
|
93
|
+
ctx.beginPath();
|
|
94
|
+
let currentX = 0;
|
|
95
|
+
let currentY = 0;
|
|
96
|
+
let lastControlX = 0;
|
|
97
|
+
let lastControlY = 0;
|
|
98
|
+
let lastCommand = '';
|
|
99
|
+
for (const instruction of instructions.split(' ')) {
|
|
100
|
+
const type = instruction[0];
|
|
101
|
+
const args: string[] = instruction.substring(1).split(',');
|
|
102
|
+
if (type === 'Z') {
|
|
103
|
+
ctx.closePath();
|
|
104
|
+
lastCommand = type;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (type === 'V') {
|
|
108
|
+
const y = yOffset + parseFloat(args[0]) * deviceCellHeight;
|
|
109
|
+
ctx.lineTo(currentX, y);
|
|
110
|
+
currentY = y;
|
|
111
|
+
lastControlX = currentX;
|
|
112
|
+
lastControlY = currentY;
|
|
113
|
+
lastCommand = type;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (type === 'H') {
|
|
117
|
+
const x = xOffset + parseFloat(args[0]) * deviceCellWidth;
|
|
118
|
+
ctx.lineTo(x, currentY);
|
|
119
|
+
currentX = x;
|
|
120
|
+
lastControlX = currentX;
|
|
121
|
+
lastControlY = currentY;
|
|
122
|
+
lastCommand = type;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!args[0] || !args[1]) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (type === 'A') {
|
|
129
|
+
// SVG arc: A rx,ry,xAxisRotation,largeArcFlag,sweepFlag,x,y
|
|
130
|
+
const rx = parseFloat(args[0]) * deviceCellWidth;
|
|
131
|
+
const ry = parseFloat(args[1]) * deviceCellHeight;
|
|
132
|
+
const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
|
|
133
|
+
const largeArcFlag = parseInt(args[3]);
|
|
134
|
+
const sweepFlag = parseInt(args[4]);
|
|
135
|
+
const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
|
|
136
|
+
const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
|
|
137
|
+
drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
|
|
138
|
+
currentX = x;
|
|
139
|
+
currentY = y;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const translatedArgs = args.map((e, i) => {
|
|
143
|
+
const val = parseFloat(e);
|
|
144
|
+
return i % 2 === 0
|
|
145
|
+
? xOffset + val * deviceCellWidth
|
|
146
|
+
: yOffset + val * deviceCellHeight;
|
|
147
|
+
});
|
|
148
|
+
if (type === 'M') {
|
|
149
|
+
ctx.moveTo(translatedArgs[0], translatedArgs[1]);
|
|
150
|
+
currentX = translatedArgs[0];
|
|
151
|
+
currentY = translatedArgs[1];
|
|
152
|
+
lastControlX = currentX;
|
|
153
|
+
lastControlY = currentY;
|
|
154
|
+
} else if (type === 'L') {
|
|
155
|
+
ctx.lineTo(translatedArgs[0], translatedArgs[1]);
|
|
156
|
+
currentX = translatedArgs[0];
|
|
157
|
+
currentY = translatedArgs[1];
|
|
158
|
+
lastControlX = currentX;
|
|
159
|
+
lastControlY = currentY;
|
|
160
|
+
} else if (type === 'Q') {
|
|
161
|
+
ctx.quadraticCurveTo(translatedArgs[0], translatedArgs[1], translatedArgs[2], translatedArgs[3]);
|
|
162
|
+
lastControlX = translatedArgs[0];
|
|
163
|
+
lastControlY = translatedArgs[1];
|
|
164
|
+
currentX = translatedArgs[2];
|
|
165
|
+
currentY = translatedArgs[3];
|
|
166
|
+
} else if (type === 'T') {
|
|
167
|
+
// T uses reflection of last control point if previous command was Q or T
|
|
168
|
+
let cpX: number;
|
|
169
|
+
let cpY: number;
|
|
170
|
+
if (lastCommand === 'Q' || lastCommand === 'T') {
|
|
171
|
+
cpX = 2 * currentX - lastControlX;
|
|
172
|
+
cpY = 2 * currentY - lastControlY;
|
|
173
|
+
} else {
|
|
174
|
+
cpX = currentX;
|
|
175
|
+
cpY = currentY;
|
|
176
|
+
}
|
|
177
|
+
ctx.quadraticCurveTo(cpX, cpY, translatedArgs[0], translatedArgs[1]);
|
|
178
|
+
lastControlX = cpX;
|
|
179
|
+
lastControlY = cpY;
|
|
180
|
+
currentX = translatedArgs[0];
|
|
181
|
+
currentY = translatedArgs[1];
|
|
182
|
+
} else if (type === 'C') {
|
|
183
|
+
ctx.bezierCurveTo(translatedArgs[0], translatedArgs[1], translatedArgs[2], translatedArgs[3], translatedArgs[4], translatedArgs[5]);
|
|
184
|
+
lastControlX = translatedArgs[2];
|
|
185
|
+
lastControlY = translatedArgs[3];
|
|
186
|
+
currentX = translatedArgs[4];
|
|
187
|
+
currentY = translatedArgs[5];
|
|
188
|
+
}
|
|
189
|
+
lastCommand = type;
|
|
190
|
+
}
|
|
191
|
+
ctx.fill();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Converts SVG arc parameters to canvas arc/ellipse calls.
|
|
196
|
+
* Based on the SVG spec's endpoint to center parameterization conversion.
|
|
197
|
+
*/
|
|
198
|
+
function drawSvgArc(
|
|
199
|
+
ctx: CanvasRenderingContext2D,
|
|
200
|
+
x1: number, y1: number,
|
|
201
|
+
rx: number, ry: number,
|
|
202
|
+
phi: number,
|
|
203
|
+
largeArcFlag: number,
|
|
204
|
+
sweepFlag: number,
|
|
205
|
+
x2: number, y2: number
|
|
206
|
+
): void {
|
|
207
|
+
// Handle degenerate cases
|
|
208
|
+
if (rx === 0 || ry === 0) {
|
|
209
|
+
ctx.lineTo(x2, y2);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
rx = Math.abs(rx);
|
|
214
|
+
ry = Math.abs(ry);
|
|
215
|
+
|
|
216
|
+
const cosPhi = Math.cos(phi);
|
|
217
|
+
const sinPhi = Math.sin(phi);
|
|
218
|
+
|
|
219
|
+
// Step 1: Compute (x1', y1')
|
|
220
|
+
const dx = (x1 - x2) / 2;
|
|
221
|
+
const dy = (y1 - y2) / 2;
|
|
222
|
+
const x1p = cosPhi * dx + sinPhi * dy;
|
|
223
|
+
const y1p = -sinPhi * dx + cosPhi * dy;
|
|
224
|
+
|
|
225
|
+
// Step 2: Compute (cx', cy')
|
|
226
|
+
let rxSq = rx * rx;
|
|
227
|
+
let rySq = ry * ry;
|
|
228
|
+
const x1pSq = x1p * x1p;
|
|
229
|
+
const y1pSq = y1p * y1p;
|
|
230
|
+
|
|
231
|
+
// Correct radii if necessary
|
|
232
|
+
const lambda = x1pSq / rxSq + y1pSq / rySq;
|
|
233
|
+
if (lambda > 1) {
|
|
234
|
+
const lambdaSqrt = Math.sqrt(lambda);
|
|
235
|
+
rx *= lambdaSqrt;
|
|
236
|
+
ry *= lambdaSqrt;
|
|
237
|
+
rxSq = rx * rx;
|
|
238
|
+
rySq = ry * ry;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq);
|
|
242
|
+
if (sq < 0) sq = 0;
|
|
243
|
+
const coef = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(sq);
|
|
244
|
+
const cxp = coef * (rx * y1p / ry);
|
|
245
|
+
const cyp = coef * -(ry * x1p / rx);
|
|
246
|
+
|
|
247
|
+
// Step 3: Compute (cx, cy) from (cx', cy')
|
|
248
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
|
|
249
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
|
|
250
|
+
|
|
251
|
+
// Step 4: Compute angles
|
|
252
|
+
const ux = (x1p - cxp) / rx;
|
|
253
|
+
const uy = (y1p - cyp) / ry;
|
|
254
|
+
const vx = (-x1p - cxp) / rx;
|
|
255
|
+
const vy = (-y1p - cyp) / ry;
|
|
256
|
+
|
|
257
|
+
const startAngle = Math.atan2(uy, ux);
|
|
258
|
+
let dTheta = Math.atan2(vy, vx) - startAngle;
|
|
259
|
+
|
|
260
|
+
if (sweepFlag === 0 && dTheta > 0) {
|
|
261
|
+
dTheta -= 2 * Math.PI;
|
|
262
|
+
} else if (sweepFlag === 1 && dTheta < 0) {
|
|
263
|
+
dTheta += 2 * Math.PI;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const endAngle = startAngle + dTheta;
|
|
267
|
+
|
|
268
|
+
ctx.ellipse(cx, cy, rx, ry, phi, startAngle, endAngle, sweepFlag === 0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Draws a "negative" path where the background color is used to draw the shape on top of a
|
|
273
|
+
* foreground-filled cell. This creates the appearance of a cutout without using actual
|
|
274
|
+
* transparency, which allows SPAA (subpixel anti-aliasing) to work correctly.
|
|
275
|
+
*
|
|
276
|
+
* @param ctx The canvas rendering context (fillStyle should be set to foreground color)
|
|
277
|
+
* @param charDefinition The vector shape definition for the negative shape
|
|
278
|
+
* @param xOffset The x offset to draw at
|
|
279
|
+
* @param yOffset The y offset to draw at
|
|
280
|
+
* @param deviceCellWidth The width of the cell in device pixels
|
|
281
|
+
* @param deviceCellHeight The height of the cell in device pixels
|
|
282
|
+
* @param devicePixelRatio The device pixel ratio
|
|
283
|
+
* @param backgroundColor The background color to use for the "cutout" portion
|
|
284
|
+
*/
|
|
285
|
+
function drawPathNegativeDefinitionCharacter(
|
|
286
|
+
ctx: CanvasRenderingContext2D,
|
|
287
|
+
charDefinition: ICustomGlyphVectorShape,
|
|
288
|
+
xOffset: number,
|
|
289
|
+
yOffset: number,
|
|
290
|
+
deviceCellWidth: number,
|
|
291
|
+
deviceCellHeight: number,
|
|
292
|
+
devicePixelRatio: number,
|
|
293
|
+
backgroundColor?: string
|
|
294
|
+
): void {
|
|
295
|
+
ctx.save();
|
|
296
|
+
|
|
297
|
+
// First, fill the entire cell with foreground color
|
|
298
|
+
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
299
|
+
|
|
300
|
+
// Then draw the "negative" shape with the background color
|
|
301
|
+
if (backgroundColor) {
|
|
302
|
+
ctx.fillStyle = backgroundColor;
|
|
303
|
+
ctx.strokeStyle = backgroundColor;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
ctx.lineWidth = devicePixelRatio;
|
|
307
|
+
ctx.lineCap = 'square';
|
|
308
|
+
ctx.beginPath();
|
|
309
|
+
for (const instruction of charDefinition.d.split(' ')) {
|
|
310
|
+
const type = instruction[0];
|
|
311
|
+
const args: string[] = instruction.substring(1).split(',');
|
|
312
|
+
if (!args[0] || !args[1]) {
|
|
313
|
+
if (type === 'Z') {
|
|
314
|
+
ctx.closePath();
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const translatedArgs = args.map((e, i) => {
|
|
319
|
+
const val = parseFloat(e);
|
|
320
|
+
return i % 2 === 0
|
|
321
|
+
? xOffset + val * deviceCellWidth
|
|
322
|
+
: yOffset + val * deviceCellHeight;
|
|
323
|
+
});
|
|
324
|
+
if (type === 'M') {
|
|
325
|
+
ctx.moveTo(translatedArgs[0], translatedArgs[1]);
|
|
326
|
+
} else if (type === 'L') {
|
|
327
|
+
ctx.lineTo(translatedArgs[0], translatedArgs[1]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (charDefinition.type === CustomGlyphVectorType.STROKE) {
|
|
332
|
+
ctx.stroke();
|
|
333
|
+
} else {
|
|
334
|
+
ctx.fill();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ctx.restore();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const cachedPatterns: Map<CustomGlyphPatternDefinition, Map</* fillStyle */string, CanvasPattern>> = new Map();
|
|
341
|
+
|
|
342
|
+
function drawPatternChar(
|
|
343
|
+
ctx: CanvasRenderingContext2D,
|
|
344
|
+
charDefinition: number[][],
|
|
345
|
+
xOffset: number,
|
|
346
|
+
yOffset: number,
|
|
347
|
+
deviceCellWidth: number,
|
|
348
|
+
deviceCellHeight: number
|
|
349
|
+
): void {
|
|
350
|
+
let patternSet = cachedPatterns.get(charDefinition);
|
|
351
|
+
if (!patternSet) {
|
|
352
|
+
patternSet = new Map();
|
|
353
|
+
cachedPatterns.set(charDefinition, patternSet);
|
|
354
|
+
}
|
|
355
|
+
const fillStyle = ctx.fillStyle;
|
|
356
|
+
if (typeof fillStyle !== 'string') {
|
|
357
|
+
throw new Error(`Unexpected fillStyle type "${fillStyle}"`);
|
|
358
|
+
}
|
|
359
|
+
let pattern = patternSet.get(fillStyle);
|
|
360
|
+
if (!pattern) {
|
|
361
|
+
const width = charDefinition[0].length;
|
|
362
|
+
const height = charDefinition.length;
|
|
363
|
+
const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas');
|
|
364
|
+
tmpCanvas.width = width;
|
|
365
|
+
tmpCanvas.height = height;
|
|
366
|
+
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
|
|
367
|
+
const imageData = new ImageData(width, height);
|
|
368
|
+
|
|
369
|
+
// Extract rgba from fillStyle
|
|
370
|
+
let r: number;
|
|
371
|
+
let g: number;
|
|
372
|
+
let b: number;
|
|
373
|
+
let a: number;
|
|
374
|
+
if (fillStyle.startsWith('#')) {
|
|
375
|
+
r = parseInt(fillStyle.slice(1, 3), 16);
|
|
376
|
+
g = parseInt(fillStyle.slice(3, 5), 16);
|
|
377
|
+
b = parseInt(fillStyle.slice(5, 7), 16);
|
|
378
|
+
a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1;
|
|
379
|
+
} else if (fillStyle.startsWith('rgba')) {
|
|
380
|
+
([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e)));
|
|
381
|
+
} else {
|
|
382
|
+
throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (let y = 0; y < height; y++) {
|
|
386
|
+
for (let x = 0; x < width; x++) {
|
|
387
|
+
imageData.data[(y * width + x) * 4 ] = r;
|
|
388
|
+
imageData.data[(y * width + x) * 4 + 1] = g;
|
|
389
|
+
imageData.data[(y * width + x) * 4 + 2] = b;
|
|
390
|
+
imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
tmpCtx.putImageData(imageData, 0, 0);
|
|
394
|
+
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
|
|
395
|
+
patternSet.set(fillStyle, pattern);
|
|
396
|
+
}
|
|
397
|
+
ctx.fillStyle = pattern;
|
|
398
|
+
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Draws rectangular shade characters - medium shade pattern clipped to a region.
|
|
403
|
+
* Uses a checkerboard pattern that shifts 1px each row (same as medium shade U+2592).
|
|
404
|
+
*/
|
|
405
|
+
function drawBlockPatternWithRegion(
|
|
406
|
+
ctx: CanvasRenderingContext2D,
|
|
407
|
+
definition: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition],
|
|
408
|
+
xOffset: number,
|
|
409
|
+
yOffset: number,
|
|
410
|
+
deviceCellWidth: number,
|
|
411
|
+
deviceCellHeight: number
|
|
412
|
+
): void {
|
|
413
|
+
const [pattern, region] = definition;
|
|
414
|
+
const [rx, ry, rw, rh] = region;
|
|
415
|
+
const regionX = Math.round(xOffset + rx * deviceCellWidth);
|
|
416
|
+
const regionY = Math.round(yOffset + ry * deviceCellHeight);
|
|
417
|
+
const regionW = Math.round(rw * deviceCellWidth);
|
|
418
|
+
const regionH = Math.round(rh * deviceCellHeight);
|
|
419
|
+
|
|
420
|
+
// Save context state
|
|
421
|
+
ctx.save();
|
|
422
|
+
|
|
423
|
+
// Clip to the region
|
|
424
|
+
ctx.beginPath();
|
|
425
|
+
ctx.rect(regionX, regionY, regionW, regionH);
|
|
426
|
+
ctx.clip();
|
|
427
|
+
|
|
428
|
+
// Draw the pattern
|
|
429
|
+
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
430
|
+
|
|
431
|
+
// Restore context state
|
|
432
|
+
ctx.restore();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
|
|
437
|
+
* canvas draw calls.
|
|
438
|
+
*
|
|
439
|
+
* Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐
|
|
440
|
+
* ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤
|
|
441
|
+
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘
|
|
442
|
+
* ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐
|
|
443
|
+
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤
|
|
444
|
+
* └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘
|
|
445
|
+
*
|
|
446
|
+
* Other:
|
|
447
|
+
* ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈
|
|
448
|
+
* │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉
|
|
449
|
+
* ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋
|
|
450
|
+
*
|
|
451
|
+
* All box drawing characters:
|
|
452
|
+
* ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
|
453
|
+
* ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
|
454
|
+
* ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
|
455
|
+
* ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
|
456
|
+
* ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
|
457
|
+
* ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
|
458
|
+
* ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
|
459
|
+
* ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
|
460
|
+
*
|
|
461
|
+
* ---
|
|
462
|
+
*
|
|
463
|
+
* Box drawing alignment tests: █
|
|
464
|
+
* ▉
|
|
465
|
+
* ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
|
|
466
|
+
* ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
|
|
467
|
+
* ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
|
|
468
|
+
* ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
|
|
469
|
+
* ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
|
|
470
|
+
* ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
|
|
471
|
+
* ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
|
|
472
|
+
*
|
|
473
|
+
* Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
|
|
474
|
+
*/
|
|
475
|
+
function drawPathDefinitionCharacterWithWeight(
|
|
476
|
+
ctx: CanvasRenderingContext2D,
|
|
477
|
+
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
|
|
478
|
+
xOffset: number,
|
|
479
|
+
yOffset: number,
|
|
480
|
+
deviceCellWidth: number,
|
|
481
|
+
deviceCellHeight: number,
|
|
482
|
+
devicePixelRatio: number
|
|
483
|
+
): void {
|
|
484
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
485
|
+
for (const [fontWeight, instructions] of Object.entries(charDefinition)) {
|
|
486
|
+
ctx.beginPath();
|
|
487
|
+
ctx.lineWidth = devicePixelRatio * Number.parseInt(fontWeight);
|
|
488
|
+
let actualInstructions: string;
|
|
489
|
+
if (typeof instructions === 'function') {
|
|
490
|
+
const xp = .15;
|
|
491
|
+
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
|
492
|
+
actualInstructions = instructions(xp, yp);
|
|
493
|
+
} else {
|
|
494
|
+
actualInstructions = instructions;
|
|
495
|
+
}
|
|
496
|
+
for (const instruction of actualInstructions.split(' ')) {
|
|
497
|
+
const type = instruction[0];
|
|
498
|
+
if (type === 'Z') {
|
|
499
|
+
ctx.closePath();
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const f = svgToCanvasInstructionMap[type];
|
|
503
|
+
if (!f) {
|
|
504
|
+
console.error(`Could not find drawing instructions for "${type}"`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const args: string[] = instruction.substring(1).split(',');
|
|
508
|
+
if (!args[0] || !args[1]) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio));
|
|
512
|
+
}
|
|
513
|
+
ctx.stroke();
|
|
514
|
+
ctx.closePath();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Draws a pattern clipped to an arbitrary path (for triangular shades, etc.)
|
|
520
|
+
*/
|
|
521
|
+
function drawBlockPatternWithClipPath(
|
|
522
|
+
ctx: CanvasRenderingContext2D,
|
|
523
|
+
definition: [pattern: CustomGlyphPatternDefinition, clipPath: string],
|
|
524
|
+
xOffset: number,
|
|
525
|
+
yOffset: number,
|
|
526
|
+
deviceCellWidth: number,
|
|
527
|
+
deviceCellHeight: number
|
|
528
|
+
): void {
|
|
529
|
+
const [pattern, clipPath] = definition;
|
|
530
|
+
|
|
531
|
+
ctx.save();
|
|
532
|
+
|
|
533
|
+
// Build clip path from SVG-like instructions
|
|
534
|
+
ctx.beginPath();
|
|
535
|
+
for (const instruction of clipPath.split(' ')) {
|
|
536
|
+
const type = instruction[0];
|
|
537
|
+
if (type === 'Z') {
|
|
538
|
+
ctx.closePath();
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const args: string[] = instruction.substring(1).split(',');
|
|
542
|
+
if (!args[0] || !args[1]) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const x = xOffset + parseFloat(args[0]) * deviceCellWidth;
|
|
546
|
+
const y = yOffset + parseFloat(args[1]) * deviceCellHeight;
|
|
547
|
+
if (type === 'M') {
|
|
548
|
+
ctx.moveTo(x, y);
|
|
549
|
+
} else if (type === 'L') {
|
|
550
|
+
ctx.lineTo(x, y);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
ctx.clip();
|
|
554
|
+
|
|
555
|
+
// Draw the pattern
|
|
556
|
+
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
557
|
+
|
|
558
|
+
ctx.restore();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function drawVectorShape(
|
|
562
|
+
ctx: CanvasRenderingContext2D,
|
|
563
|
+
charDefinition: ICustomGlyphVectorShape,
|
|
564
|
+
xOffset: number,
|
|
565
|
+
yOffset: number,
|
|
566
|
+
deviceCellWidth: number,
|
|
567
|
+
deviceCellHeight: number,
|
|
568
|
+
fontSize: number,
|
|
569
|
+
devicePixelRatio: number
|
|
570
|
+
): void {
|
|
571
|
+
// Clip the cell to make sure drawing doesn't occur beyond bounds
|
|
572
|
+
const clipRegion = new Path2D();
|
|
573
|
+
clipRegion.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
574
|
+
ctx.clip(clipRegion);
|
|
575
|
+
|
|
576
|
+
ctx.beginPath();
|
|
577
|
+
// Scale the stroke with DPR and font size
|
|
578
|
+
const cssLineWidth = fontSize / 12;
|
|
579
|
+
ctx.lineWidth = devicePixelRatio * cssLineWidth;
|
|
580
|
+
for (const instruction of charDefinition.d.split(' ')) {
|
|
581
|
+
const type = instruction[0];
|
|
582
|
+
if (type === 'Z') {
|
|
583
|
+
ctx.closePath();
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const f = svgToCanvasInstructionMap[type];
|
|
587
|
+
if (!f) {
|
|
588
|
+
console.error(`Could not find drawing instructions for "${type}"`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const args: string[] = instruction.substring(1).split(',');
|
|
592
|
+
if (!args[0] || !args[1]) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
f(ctx, translateArgs(
|
|
596
|
+
args,
|
|
597
|
+
deviceCellWidth,
|
|
598
|
+
deviceCellHeight,
|
|
599
|
+
xOffset,
|
|
600
|
+
yOffset,
|
|
601
|
+
false,
|
|
602
|
+
devicePixelRatio,
|
|
603
|
+
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
|
604
|
+
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
|
605
|
+
));
|
|
606
|
+
}
|
|
607
|
+
if (charDefinition.type === CustomGlyphVectorType.STROKE) {
|
|
608
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
609
|
+
ctx.stroke();
|
|
610
|
+
} else {
|
|
611
|
+
ctx.fill();
|
|
612
|
+
}
|
|
613
|
+
ctx.closePath();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function clamp(value: number, max: number, min: number = 0): number {
|
|
617
|
+
return Math.max(Math.min(value, max), min);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const svgToCanvasInstructionMap: { [index: string]: any } = {
|
|
621
|
+
'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]),
|
|
622
|
+
'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]),
|
|
623
|
+
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]),
|
|
624
|
+
'Q': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.quadraticCurveTo(args[0], args[1], args[2], args[3])
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
|
628
|
+
const result = args.map(e => parseFloat(e) || parseInt(e));
|
|
629
|
+
|
|
630
|
+
if (result.length < 2) {
|
|
631
|
+
throw new Error('Too few arguments for instruction');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (let x = 0; x < result.length; x += 2) {
|
|
635
|
+
// Translate from 0-1 to 0-cellWidth
|
|
636
|
+
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
|
|
637
|
+
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
|
638
|
+
// line at 100% devicePixelRatio
|
|
639
|
+
if (doClamp && result[x] !== 0) {
|
|
640
|
+
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
|
641
|
+
}
|
|
642
|
+
// Apply the cell's offset (ie. x*cellWidth)
|
|
643
|
+
result[x] += xOffset + (leftPadding * devicePixelRatio);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
for (let y = 1; y < result.length; y += 2) {
|
|
647
|
+
// Translate from 0-1 to 0-cellHeight
|
|
648
|
+
result[y] *= cellHeight;
|
|
649
|
+
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
|
650
|
+
// line at 100% devicePixelRatio
|
|
651
|
+
if (doClamp && result[y] !== 0) {
|
|
652
|
+
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
|
|
653
|
+
}
|
|
654
|
+
// Apply the cell's offset (ie. x*cellHeight)
|
|
655
|
+
result[y] += yOffset;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return result;
|
|
659
|
+
}
|