@xterm/addon-webgl 0.20.0-beta.3 → 0.20.0-beta.300

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.
@@ -0,0 +1,768 @@
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 type { ILogService } from 'common/services/Services';
8
+ import { customGlyphDefinitions } from './CustomGlyphDefinitions';
9
+ import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
10
+
11
+ type PatternCanvas = HTMLCanvasElement | OffscreenCanvas;
12
+ type PatternCanvasFactory = (width: number, height: number) => PatternCanvas;
13
+
14
+ const createOffscreenPatternCanvas: PatternCanvasFactory | undefined = typeof OffscreenCanvas === 'undefined'
15
+ ? undefined
16
+ : (width, height) => new OffscreenCanvas(width, height);
17
+
18
+ const createDomPatternCanvas: PatternCanvasFactory = (width, height) => {
19
+ const canvas = document.createElement('canvas');
20
+ canvas.width = width;
21
+ canvas.height = height;
22
+ return canvas;
23
+ };
24
+
25
+ export function createPatternCanvas(
26
+ width: number,
27
+ height: number,
28
+ offscreenCanvasFactory: PatternCanvasFactory | undefined = createOffscreenPatternCanvas,
29
+ domCanvasFactory: PatternCanvasFactory = createDomPatternCanvas
30
+ ): PatternCanvas {
31
+ return offscreenCanvasFactory?.(width, height) ?? domCanvasFactory(width, height);
32
+ }
33
+
34
+ /**
35
+ * Try drawing a custom block element or box drawing character, returning whether it was
36
+ * successfully drawn.
37
+ */
38
+ export function tryDrawCustomGlyph(
39
+ ctx: CanvasRenderingContext2D,
40
+ c: string,
41
+ xOffset: number,
42
+ yOffset: number,
43
+ deviceCellWidth: number,
44
+ deviceCellHeight: number,
45
+ deviceCharWidth: number,
46
+ deviceCharHeight: number,
47
+ fontSize: number,
48
+ devicePixelRatio: number,
49
+ logService: ILogService,
50
+ backgroundColor?: string,
51
+ variantOffset: number = 0
52
+ ): boolean {
53
+ const unifiedCharDefinition = customGlyphDefinitions[c];
54
+ if (unifiedCharDefinition) {
55
+ // Normalize to array for uniform handling
56
+ const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
57
+ for (const part of parts) {
58
+ drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, logService, backgroundColor, variantOffset);
59
+ }
60
+ return true;
61
+ }
62
+
63
+ return false;
64
+ }
65
+
66
+ function drawDefinitionPart(
67
+ ctx: CanvasRenderingContext2D,
68
+ part: CustomGlyphDefinitionPart,
69
+ xOffset: number,
70
+ yOffset: number,
71
+ deviceCellWidth: number,
72
+ deviceCellHeight: number,
73
+ deviceCharWidth: number,
74
+ deviceCharHeight: number,
75
+ fontSize: number,
76
+ devicePixelRatio: number,
77
+ logService: ILogService,
78
+ backgroundColor?: string,
79
+ variantOffset: number = 0
80
+ ): void {
81
+ // Handle scaleType - adjust dimensions and offset when scaling to character area
82
+ let drawWidth = deviceCellWidth;
83
+ let drawHeight = deviceCellHeight;
84
+ let drawXOffset = xOffset;
85
+ let drawYOffset = yOffset;
86
+ if (part.scaleType === CustomGlyphScaleType.CHAR) {
87
+ drawWidth = deviceCharWidth;
88
+ drawHeight = deviceCharHeight;
89
+ // Center the character within the cell
90
+ drawXOffset = xOffset + (deviceCellWidth - deviceCharWidth) / 2;
91
+ drawYOffset = yOffset + (deviceCellHeight - deviceCharHeight) / 2;
92
+ }
93
+
94
+ // Handle clipPath generically for any definition type
95
+ if (part.clipPath) {
96
+ ctx.save();
97
+ applyClipPath(ctx, part.clipPath, drawXOffset, drawYOffset, drawWidth, drawHeight);
98
+ }
99
+
100
+ switch (part.type) {
101
+ case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
102
+ drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
103
+ break;
104
+ case CustomGlyphDefinitionType.BLOCK_PATTERN:
105
+ drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
106
+ break;
107
+ case CustomGlyphDefinitionType.PATH_FUNCTION:
108
+ drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, logService, part.strokeWidth);
109
+ break;
110
+ case CustomGlyphDefinitionType.PATH:
111
+ drawPathDefinitionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
112
+ break;
113
+ case CustomGlyphDefinitionType.PATH_NEGATIVE:
114
+ drawPathNegativeDefinitionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, backgroundColor);
115
+ break;
116
+ case CustomGlyphDefinitionType.VECTOR_SHAPE:
117
+ drawVectorShape(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, fontSize, devicePixelRatio, logService);
118
+ break;
119
+ case CustomGlyphDefinitionType.BRAILLE:
120
+ drawBrailleCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
121
+ break;
122
+ }
123
+
124
+ if (part.clipPath) {
125
+ ctx.restore();
126
+ }
127
+ }
128
+
129
+ function drawBlockVectorChar(
130
+ ctx: CanvasRenderingContext2D,
131
+ charDefinition: ICustomGlyphSolidOctantBlockVector[],
132
+ xOffset: number,
133
+ yOffset: number,
134
+ deviceCellWidth: number,
135
+ deviceCellHeight: number
136
+ ): void {
137
+ for (let i = 0; i < charDefinition.length; i++) {
138
+ const box = charDefinition[i];
139
+ const xEighth = deviceCellWidth / 8;
140
+ const yEighth = deviceCellHeight / 8;
141
+ ctx.fillRect(
142
+ xOffset + box.x * xEighth,
143
+ yOffset + box.y * yEighth,
144
+ box.w * xEighth,
145
+ box.h * yEighth
146
+ );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Braille dot positions in octant coordinates (x, y for center of each dot area)
152
+ * Columns: left=1-2, right=5-6 (leaving 0 and 7 as margins, 3-4 as gap)
153
+ * Rows: 0-1, 2-3, 4-5, 6-7 for the 4 rows
154
+ */
155
+ const brailleDotPositions = new Uint8Array([
156
+ 1, 0, // dot 1 - bit 0
157
+ 1, 2, // dot 2 - bit 1
158
+ 1, 4, // dot 3 - bit 2
159
+ 5, 0, // dot 4 - bit 3
160
+ 5, 2, // dot 5 - bit 4
161
+ 5, 4, // dot 6 - bit 5
162
+ 1, 6, // dot 7 - bit 6
163
+ 5, 6, // dot 8 - bit 7
164
+ ]);
165
+
166
+ /**
167
+ * Draws a braille pattern
168
+ */
169
+ function drawBrailleCharacter(
170
+ ctx: CanvasRenderingContext2D,
171
+ pattern: number,
172
+ xOffset: number,
173
+ yOffset: number,
174
+ deviceCellWidth: number,
175
+ deviceCellHeight: number
176
+ ): void {
177
+ const xEighth = deviceCellWidth / 8;
178
+ const paddingY = deviceCellHeight * 0.1;
179
+ const usableHeight = deviceCellHeight * 0.8;
180
+ const yEighth = usableHeight / 8;
181
+ const radius = Math.min(xEighth, yEighth);
182
+
183
+ for (let bit = 0; bit < 8; bit++) {
184
+ if (pattern & (1 << bit)) {
185
+ const x = brailleDotPositions[bit * 2];
186
+ const y = brailleDotPositions[bit * 2 + 1];
187
+ const cx = xOffset + (x + 1) * xEighth;
188
+ const cy = yOffset + paddingY + (y + 1) * yEighth;
189
+ ctx.beginPath();
190
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
191
+ ctx.fill();
192
+ }
193
+ }
194
+ }
195
+
196
+ function drawPathDefinitionCharacter(
197
+ ctx: CanvasRenderingContext2D,
198
+ charDefinition: CustomGlyphPathDrawFunctionDefinition | string,
199
+ xOffset: number,
200
+ yOffset: number,
201
+ deviceCellWidth: number,
202
+ deviceCellHeight: number,
203
+ devicePixelRatio: number,
204
+ strokeWidth?: number
205
+ ): void {
206
+ const instructions = typeof charDefinition === 'string' ? charDefinition : charDefinition(0, 0);
207
+ ctx.beginPath();
208
+ let currentX = 0;
209
+ let currentY = 0;
210
+ let lastControlX = 0;
211
+ let lastControlY = 0;
212
+ let lastCommand = '';
213
+ for (const instruction of instructions.split(' ')) {
214
+ const type = instruction[0];
215
+ const args: string[] = instruction.substring(1).split(',');
216
+ if (type === 'Z') {
217
+ ctx.closePath();
218
+ lastCommand = type;
219
+ continue;
220
+ }
221
+ if (type === 'V') {
222
+ const y = yOffset + parseFloat(args[0]) * deviceCellHeight;
223
+ ctx.lineTo(currentX, y);
224
+ currentY = y;
225
+ lastControlX = currentX;
226
+ lastControlY = currentY;
227
+ lastCommand = type;
228
+ continue;
229
+ }
230
+ if (type === 'H') {
231
+ const x = xOffset + parseFloat(args[0]) * deviceCellWidth;
232
+ ctx.lineTo(x, currentY);
233
+ currentX = x;
234
+ lastControlX = currentX;
235
+ lastControlY = currentY;
236
+ lastCommand = type;
237
+ continue;
238
+ }
239
+ if (!args[0] || !args[1]) {
240
+ continue;
241
+ }
242
+ if (type === 'A') {
243
+ // SVG arc: A rx,ry,xAxisRotation,largeArcFlag,sweepFlag,x,y
244
+ const rx = parseFloat(args[0]) * deviceCellWidth;
245
+ const ry = parseFloat(args[1]) * deviceCellHeight;
246
+ const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
247
+ const largeArcFlag = parseInt(args[3], 10);
248
+ const sweepFlag = parseInt(args[4], 10);
249
+ const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
250
+ const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
251
+ drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
252
+ currentX = x;
253
+ currentY = y;
254
+ continue;
255
+ }
256
+ const translatedArgs = args.map((e, i) => {
257
+ const val = parseFloat(e);
258
+ return i % 2 === 0
259
+ ? xOffset + val * deviceCellWidth
260
+ : yOffset + val * deviceCellHeight;
261
+ });
262
+ if (type === 'M') {
263
+ ctx.moveTo(translatedArgs[0], translatedArgs[1]);
264
+ currentX = translatedArgs[0];
265
+ currentY = translatedArgs[1];
266
+ lastControlX = currentX;
267
+ lastControlY = currentY;
268
+ } else if (type === 'L') {
269
+ ctx.lineTo(translatedArgs[0], translatedArgs[1]);
270
+ currentX = translatedArgs[0];
271
+ currentY = translatedArgs[1];
272
+ lastControlX = currentX;
273
+ lastControlY = currentY;
274
+ } else if (type === 'Q') {
275
+ ctx.quadraticCurveTo(translatedArgs[0], translatedArgs[1], translatedArgs[2], translatedArgs[3]);
276
+ lastControlX = translatedArgs[0];
277
+ lastControlY = translatedArgs[1];
278
+ currentX = translatedArgs[2];
279
+ currentY = translatedArgs[3];
280
+ } else if (type === 'T') {
281
+ // T uses reflection of last control point if previous command was Q or T
282
+ let cpX: number;
283
+ let cpY: number;
284
+ if (lastCommand === 'Q' || lastCommand === 'T') {
285
+ cpX = 2 * currentX - lastControlX;
286
+ cpY = 2 * currentY - lastControlY;
287
+ } else {
288
+ cpX = currentX;
289
+ cpY = currentY;
290
+ }
291
+ ctx.quadraticCurveTo(cpX, cpY, translatedArgs[0], translatedArgs[1]);
292
+ lastControlX = cpX;
293
+ lastControlY = cpY;
294
+ currentX = translatedArgs[0];
295
+ currentY = translatedArgs[1];
296
+ } else if (type === 'C') {
297
+ ctx.bezierCurveTo(translatedArgs[0], translatedArgs[1], translatedArgs[2], translatedArgs[3], translatedArgs[4], translatedArgs[5]);
298
+ lastControlX = translatedArgs[2];
299
+ lastControlY = translatedArgs[3];
300
+ currentX = translatedArgs[4];
301
+ currentY = translatedArgs[5];
302
+ }
303
+ lastCommand = type;
304
+ }
305
+ if (strokeWidth !== undefined) {
306
+ ctx.strokeStyle = ctx.fillStyle;
307
+ ctx.lineWidth = devicePixelRatio * strokeWidth;
308
+ ctx.stroke();
309
+ } else {
310
+ ctx.fill();
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Converts SVG arc parameters to canvas arc/ellipse calls.
316
+ * Based on the SVG spec's endpoint to center parameterization conversion.
317
+ */
318
+ function drawSvgArc(
319
+ ctx: CanvasRenderingContext2D,
320
+ x1: number, y1: number,
321
+ rx: number, ry: number,
322
+ phi: number,
323
+ largeArcFlag: number,
324
+ sweepFlag: number,
325
+ x2: number, y2: number
326
+ ): void {
327
+ // Handle degenerate cases
328
+ if (rx === 0 || ry === 0) {
329
+ ctx.lineTo(x2, y2);
330
+ return;
331
+ }
332
+
333
+ rx = Math.abs(rx);
334
+ ry = Math.abs(ry);
335
+
336
+ const cosPhi = Math.cos(phi);
337
+ const sinPhi = Math.sin(phi);
338
+
339
+ // Step 1: Compute (x1', y1')
340
+ const dx = (x1 - x2) / 2;
341
+ const dy = (y1 - y2) / 2;
342
+ const x1p = cosPhi * dx + sinPhi * dy;
343
+ const y1p = -sinPhi * dx + cosPhi * dy;
344
+
345
+ // Step 2: Compute (cx', cy')
346
+ let rxSq = rx * rx;
347
+ let rySq = ry * ry;
348
+ const x1pSq = x1p * x1p;
349
+ const y1pSq = y1p * y1p;
350
+
351
+ // Correct radii if necessary
352
+ const lambda = x1pSq / rxSq + y1pSq / rySq;
353
+ if (lambda > 1) {
354
+ const lambdaSqrt = Math.sqrt(lambda);
355
+ rx *= lambdaSqrt;
356
+ ry *= lambdaSqrt;
357
+ rxSq = rx * rx;
358
+ rySq = ry * ry;
359
+ }
360
+
361
+ let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq);
362
+ if (sq < 0) sq = 0;
363
+ const coef = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(sq);
364
+ const cxp = coef * (rx * y1p / ry);
365
+ const cyp = coef * -(ry * x1p / rx);
366
+
367
+ // Step 3: Compute (cx, cy) from (cx', cy')
368
+ const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
369
+ const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
370
+
371
+ // Step 4: Compute angles
372
+ const ux = (x1p - cxp) / rx;
373
+ const uy = (y1p - cyp) / ry;
374
+ const vx = (-x1p - cxp) / rx;
375
+ const vy = (-y1p - cyp) / ry;
376
+
377
+ const startAngle = Math.atan2(uy, ux);
378
+ let dTheta = Math.atan2(vy, vx) - startAngle;
379
+
380
+ if (sweepFlag === 0 && dTheta > 0) {
381
+ dTheta -= 2 * Math.PI;
382
+ } else if (sweepFlag === 1 && dTheta < 0) {
383
+ dTheta += 2 * Math.PI;
384
+ }
385
+
386
+ const endAngle = startAngle + dTheta;
387
+
388
+ ctx.ellipse(cx, cy, rx, ry, phi, startAngle, endAngle, sweepFlag === 0);
389
+ }
390
+
391
+ /**
392
+ * Draws a "negative" path where the background color is used to draw the shape on top of a
393
+ * foreground-filled cell. This creates the appearance of a cutout without using actual
394
+ * transparency, which allows SPAA (subpixel anti-aliasing) to work correctly.
395
+ *
396
+ * @param ctx The canvas rendering context (fillStyle should be set to foreground color)
397
+ * @param charDefinition The vector shape definition for the negative shape
398
+ * @param xOffset The x offset to draw at
399
+ * @param yOffset The y offset to draw at
400
+ * @param deviceCellWidth The width of the cell in device pixels
401
+ * @param deviceCellHeight The height of the cell in device pixels
402
+ * @param devicePixelRatio The device pixel ratio
403
+ * @param backgroundColor The background color to use for the "cutout" portion
404
+ */
405
+ function drawPathNegativeDefinitionCharacter(
406
+ ctx: CanvasRenderingContext2D,
407
+ charDefinition: ICustomGlyphVectorShape,
408
+ xOffset: number,
409
+ yOffset: number,
410
+ deviceCellWidth: number,
411
+ deviceCellHeight: number,
412
+ devicePixelRatio: number,
413
+ backgroundColor?: string
414
+ ): void {
415
+ ctx.save();
416
+
417
+ // First, fill the entire cell with foreground color
418
+ ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
419
+
420
+ // Then draw the "negative" shape with the background color
421
+ if (backgroundColor) {
422
+ ctx.fillStyle = backgroundColor;
423
+ ctx.strokeStyle = backgroundColor;
424
+ }
425
+
426
+ ctx.lineWidth = devicePixelRatio;
427
+ ctx.lineCap = 'square';
428
+ ctx.beginPath();
429
+ for (const instruction of charDefinition.d.split(' ')) {
430
+ const type = instruction[0];
431
+ const args: string[] = instruction.substring(1).split(',');
432
+ if (!args[0] || !args[1]) {
433
+ if (type === 'Z') {
434
+ ctx.closePath();
435
+ }
436
+ continue;
437
+ }
438
+ const translatedArgs = args.map((e, i) => {
439
+ const val = parseFloat(e);
440
+ return i % 2 === 0
441
+ ? xOffset + val * deviceCellWidth
442
+ : yOffset + val * deviceCellHeight;
443
+ });
444
+ if (type === 'M') {
445
+ ctx.moveTo(translatedArgs[0], translatedArgs[1]);
446
+ } else if (type === 'L') {
447
+ ctx.lineTo(translatedArgs[0], translatedArgs[1]);
448
+ }
449
+ }
450
+
451
+ if (charDefinition.type === CustomGlyphVectorType.STROKE) {
452
+ ctx.stroke();
453
+ } else {
454
+ ctx.fill();
455
+ }
456
+
457
+ ctx.restore();
458
+ }
459
+
460
+ const cachedPatterns: Map<CustomGlyphPatternDefinition, Map</* fillStyle */string, CanvasPattern>> = new Map();
461
+
462
+ function drawPatternChar(
463
+ ctx: CanvasRenderingContext2D,
464
+ charDefinition: number[][],
465
+ xOffset: number,
466
+ yOffset: number,
467
+ deviceCellWidth: number,
468
+ deviceCellHeight: number,
469
+ variantOffset: number = 0
470
+ ): void {
471
+ let patternSet = cachedPatterns.get(charDefinition);
472
+ if (!patternSet) {
473
+ patternSet = new Map();
474
+ cachedPatterns.set(charDefinition, patternSet);
475
+ }
476
+ const fillStyle = ctx.fillStyle;
477
+ if (typeof fillStyle !== 'string') {
478
+ throw new Error(`Unexpected fillStyle type "${fillStyle}"`);
479
+ }
480
+ let pattern = patternSet.get(fillStyle);
481
+ if (!pattern) {
482
+ const width = charDefinition[0].length;
483
+ const height = charDefinition.length;
484
+ // The atlas canvas can be adopted into another document, so temporary resources must not use
485
+ // its mutable ownerDocument.
486
+ const tmpCanvas = createPatternCanvas(width, height);
487
+ const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
488
+ const imageData = new ImageData(width, height);
489
+
490
+ // Extract rgba from fillStyle
491
+ let r: number;
492
+ let g: number;
493
+ let b: number;
494
+ let a: number;
495
+ if (fillStyle.startsWith('#')) {
496
+ r = parseInt(fillStyle.slice(1, 3), 16);
497
+ g = parseInt(fillStyle.slice(3, 5), 16);
498
+ b = parseInt(fillStyle.slice(5, 7), 16);
499
+ a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1;
500
+ } else if (fillStyle.startsWith('rgba')) {
501
+ ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e)));
502
+ } else {
503
+ throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`);
504
+ }
505
+
506
+ for (let y = 0; y < height; y++) {
507
+ for (let x = 0; x < width; x++) {
508
+ imageData.data[(y * width + x) * 4 ] = r;
509
+ imageData.data[(y * width + x) * 4 + 1] = g;
510
+ imageData.data[(y * width + x) * 4 + 2] = b;
511
+ imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255);
512
+ }
513
+ }
514
+ tmpCtx.putImageData(imageData, 0, 0);
515
+ pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
516
+ patternSet.set(fillStyle, pattern);
517
+ }
518
+ // Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
519
+ // variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
520
+ const dx = (variantOffset >> 1) & 1;
521
+ const dy = variantOffset & 1;
522
+ if (dx !== 0 || dy !== 0) {
523
+ pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy));
524
+ } else {
525
+ pattern.setTransform(new DOMMatrix());
526
+ }
527
+ ctx.fillStyle = pattern;
528
+ ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
529
+ }
530
+
531
+ function drawPathFunctionCharacter(
532
+ ctx: CanvasRenderingContext2D,
533
+ charDefinition: string | ((xp: number, yp: number) => string),
534
+ xOffset: number,
535
+ yOffset: number,
536
+ deviceCellWidth: number,
537
+ deviceCellHeight: number,
538
+ devicePixelRatio: number,
539
+ logService: ILogService,
540
+ strokeWidth?: number
541
+ ): void {
542
+ ctx.save();
543
+ ctx.beginPath();
544
+ ctx.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
545
+ ctx.clip();
546
+
547
+ ctx.beginPath();
548
+ let actualInstructions: string;
549
+ if (typeof charDefinition === 'function') {
550
+ const xp = .15;
551
+ const yp = .15 / deviceCellHeight * deviceCellWidth;
552
+ actualInstructions = charDefinition(xp, yp);
553
+ } else {
554
+ actualInstructions = charDefinition;
555
+ }
556
+ const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
557
+ for (const instruction of actualInstructions.split(' ')) {
558
+ const type = instruction[0];
559
+ if (type === 'Z') {
560
+ ctx.closePath();
561
+ state.lastCommand = type;
562
+ continue;
563
+ }
564
+ const f = svgToCanvasInstructionMap[type];
565
+ if (!f) {
566
+ logService.error(`Could not find drawing instructions for "${type}"`);
567
+ continue;
568
+ }
569
+ const args: string[] = instruction.substring(1).split(',');
570
+ if (!args[0] || !args[1]) {
571
+ continue;
572
+ }
573
+ f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio, 0, 0, false), state);
574
+ state.lastCommand = type;
575
+ }
576
+ if (strokeWidth !== undefined) {
577
+ ctx.strokeStyle = ctx.fillStyle;
578
+ ctx.lineWidth = devicePixelRatio * strokeWidth;
579
+ ctx.stroke();
580
+ } else {
581
+ ctx.fill();
582
+ }
583
+ ctx.closePath();
584
+ ctx.restore();
585
+ }
586
+
587
+ /**
588
+ * Applies a clip path to the canvas context from SVG-like path instructions.
589
+ */
590
+ function applyClipPath(
591
+ ctx: CanvasRenderingContext2D,
592
+ clipPath: string,
593
+ xOffset: number,
594
+ yOffset: number,
595
+ deviceCellWidth: number,
596
+ deviceCellHeight: number
597
+ ): void {
598
+ ctx.beginPath();
599
+ for (const instruction of clipPath.split(' ')) {
600
+ const type = instruction[0];
601
+ if (type === 'Z') {
602
+ ctx.closePath();
603
+ continue;
604
+ }
605
+ const args: string[] = instruction.substring(1).split(',');
606
+ if (!args[0] || !args[1]) {
607
+ continue;
608
+ }
609
+ const x = xOffset + parseFloat(args[0]) * deviceCellWidth;
610
+ const y = yOffset + parseFloat(args[1]) * deviceCellHeight;
611
+ if (type === 'M') {
612
+ ctx.moveTo(x, y);
613
+ } else if (type === 'L') {
614
+ ctx.lineTo(x, y);
615
+ }
616
+ }
617
+ ctx.clip();
618
+ }
619
+
620
+ function drawVectorShape(
621
+ ctx: CanvasRenderingContext2D,
622
+ charDefinition: ICustomGlyphVectorShape,
623
+ xOffset: number,
624
+ yOffset: number,
625
+ deviceCellWidth: number,
626
+ deviceCellHeight: number,
627
+ fontSize: number,
628
+ devicePixelRatio: number,
629
+ logService: ILogService
630
+ ): void {
631
+ // Clip the cell to make sure drawing doesn't occur beyond bounds
632
+ const clipRegion = new Path2D();
633
+ clipRegion.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
634
+ ctx.clip(clipRegion);
635
+
636
+ ctx.beginPath();
637
+ // Scale the stroke with DPR and font size
638
+ const cssLineWidth = fontSize / 12;
639
+ ctx.lineWidth = devicePixelRatio * cssLineWidth;
640
+ const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
641
+ for (const instruction of charDefinition.d.split(' ')) {
642
+ const type = instruction[0];
643
+ if (type === 'Z') {
644
+ ctx.closePath();
645
+ state.lastCommand = type;
646
+ continue;
647
+ }
648
+ const f = svgToCanvasInstructionMap[type];
649
+ if (!f) {
650
+ logService.error(`Could not find drawing instructions for "${type}"`);
651
+ continue;
652
+ }
653
+ const args: string[] = instruction.substring(1).split(',');
654
+ if (!args[0] || !args[1]) {
655
+ continue;
656
+ }
657
+ f(ctx, translateArgs(
658
+ args,
659
+ deviceCellWidth,
660
+ deviceCellHeight,
661
+ xOffset,
662
+ yOffset,
663
+ false,
664
+ devicePixelRatio,
665
+ (charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
666
+ (charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
667
+ ), state);
668
+ state.lastCommand = type;
669
+ }
670
+ if (charDefinition.type === CustomGlyphVectorType.STROKE) {
671
+ ctx.strokeStyle = ctx.fillStyle;
672
+ ctx.stroke();
673
+ } else {
674
+ ctx.fill();
675
+ }
676
+ ctx.closePath();
677
+ }
678
+
679
+ function clamp(value: number, max: number, min: number = 0): number {
680
+ return Math.max(Math.min(value, max), min);
681
+ }
682
+
683
+ interface ISvgPathState {
684
+ currentX: number;
685
+ currentY: number;
686
+ lastControlX: number;
687
+ lastControlY: number;
688
+ lastCommand: string;
689
+ }
690
+
691
+ const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext2D, args: number[], state: ISvgPathState) => void } = {
692
+ 'C': (ctx, args, state) => {
693
+ ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]);
694
+ state.lastControlX = args[2];
695
+ state.lastControlY = args[3];
696
+ state.currentX = args[4];
697
+ state.currentY = args[5];
698
+ },
699
+ 'L': (ctx, args, state) => {
700
+ ctx.lineTo(args[0], args[1]);
701
+ state.lastControlX = state.currentX = args[0];
702
+ state.lastControlY = state.currentY = args[1];
703
+ },
704
+ 'M': (ctx, args, state) => {
705
+ ctx.moveTo(args[0], args[1]);
706
+ state.lastControlX = state.currentX = args[0];
707
+ state.lastControlY = state.currentY = args[1];
708
+ },
709
+ 'Q': (ctx, args, state) => {
710
+ ctx.quadraticCurveTo(args[0], args[1], args[2], args[3]);
711
+ state.lastControlX = args[0];
712
+ state.lastControlY = args[1];
713
+ state.currentX = args[2];
714
+ state.currentY = args[3];
715
+ },
716
+ 'T': (ctx, args, state) => {
717
+ let cpX: number;
718
+ let cpY: number;
719
+ if (state.lastCommand === 'Q' || state.lastCommand === 'T') {
720
+ cpX = 2 * state.currentX - state.lastControlX;
721
+ cpY = 2 * state.currentY - state.lastControlY;
722
+ } else {
723
+ cpX = state.currentX;
724
+ cpY = state.currentY;
725
+ }
726
+ ctx.quadraticCurveTo(cpX, cpY, args[0], args[1]);
727
+ state.lastControlX = cpX;
728
+ state.lastControlY = cpY;
729
+ state.currentX = args[0];
730
+ state.currentY = args[1];
731
+ }
732
+ };
733
+
734
+ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0, clampToCell: boolean = true): number[] {
735
+ const result = args.map(e => parseFloat(e) || parseInt(e));
736
+
737
+ if (result.length < 2) {
738
+ throw new Error('Too few arguments for instruction');
739
+ }
740
+
741
+ for (let x = 0; x < result.length; x += 2) {
742
+ // Translate from 0-1 to 0-cellWidth
743
+ result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
744
+ // Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
745
+ // clamp to the cell bounds.
746
+ if (doClamp && result[x] !== 0) {
747
+ const rounded = Math.round(result[x] + 0.5) - 0.5;
748
+ result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
749
+ }
750
+ // Apply the cell's offset (ie. x*cellWidth)
751
+ result[x] += xOffset + (leftPadding * devicePixelRatio);
752
+ }
753
+
754
+ for (let y = 1; y < result.length; y += 2) {
755
+ // Translate from 0-1 to 0-cellHeight
756
+ result[y] *= cellHeight;
757
+ // Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
758
+ // clamp to the cell bounds.
759
+ if (doClamp && result[y] !== 0) {
760
+ const rounded = Math.round(result[y] + 0.5) - 0.5;
761
+ result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
762
+ }
763
+ // Apply the cell's offset (ie. x*cellHeight)
764
+ result[y] += yOffset;
765
+ }
766
+
767
+ return result;
768
+ }