@emasoft/svg-matrix 1.0.4 → 1.0.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/README.md +341 -304
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1615 -76
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/test.svg +0 -39
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Path Module - Convert SVG text elements to path elements
|
|
3
|
+
*
|
|
4
|
+
* Converts text, tspan, and textPath elements to path elements for
|
|
5
|
+
* precise clipping and transformation operations.
|
|
6
|
+
*
|
|
7
|
+
* Note: Requires opentype.js for font parsing (optional dependency).
|
|
8
|
+
* Falls back to bounding box approximation if fonts unavailable.
|
|
9
|
+
*
|
|
10
|
+
* @module text-to-path
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Decimal from 'decimal.js';
|
|
14
|
+
import { Matrix } from './matrix.js';
|
|
15
|
+
import * as Transforms2D from './transforms2d.js';
|
|
16
|
+
import * as PolygonClip from './polygon-clip.js';
|
|
17
|
+
|
|
18
|
+
Decimal.set({ precision: 80 });
|
|
19
|
+
|
|
20
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
21
|
+
|
|
22
|
+
// Default font metrics when actual font unavailable
|
|
23
|
+
const DEFAULT_FONT_SIZE = 16;
|
|
24
|
+
const DEFAULT_UNITS_PER_EM = 1000;
|
|
25
|
+
const DEFAULT_ASCENDER = 800;
|
|
26
|
+
const DEFAULT_DESCENDER = -200;
|
|
27
|
+
const DEFAULT_CAP_HEIGHT = 700;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Text alignment options for horizontal positioning.
|
|
31
|
+
*
|
|
32
|
+
* Controls where text is anchored relative to its x coordinate:
|
|
33
|
+
* - START: Text begins at x coordinate (default, left-aligned for LTR text)
|
|
34
|
+
* - MIDDLE: Text is centered on x coordinate
|
|
35
|
+
* - END: Text ends at x coordinate (right-aligned for LTR text)
|
|
36
|
+
*
|
|
37
|
+
* @enum {string}
|
|
38
|
+
* @example
|
|
39
|
+
* // Left-align text starting at x=100
|
|
40
|
+
* textToPath("Hello", { x: 100, textAnchor: TextAnchor.START })
|
|
41
|
+
*
|
|
42
|
+
* // Center text on x=100
|
|
43
|
+
* textToPath("Hello", { x: 100, textAnchor: TextAnchor.MIDDLE })
|
|
44
|
+
*
|
|
45
|
+
* // Right-align text ending at x=100
|
|
46
|
+
* textToPath("Hello", { x: 100, textAnchor: TextAnchor.END })
|
|
47
|
+
*/
|
|
48
|
+
export const TextAnchor = {
|
|
49
|
+
START: 'start',
|
|
50
|
+
MIDDLE: 'middle',
|
|
51
|
+
END: 'end'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Baseline alignment options for vertical positioning.
|
|
56
|
+
*
|
|
57
|
+
* Controls where text sits relative to its y coordinate based on font metrics:
|
|
58
|
+
* - AUTO/ALPHABETIC: y is the alphabetic baseline (bottom of most letters, default)
|
|
59
|
+
* - MIDDLE/CENTRAL: y is the vertical center of the text
|
|
60
|
+
* - HANGING: y is the top of hanging scripts (e.g., Devanagari)
|
|
61
|
+
* - TEXT_BEFORE_EDGE: y is the top edge of the em box
|
|
62
|
+
* - TEXT_AFTER_EDGE: y is the bottom edge of the em box
|
|
63
|
+
* - IDEOGRAPHIC: y is the ideographic baseline (bottom for CJK characters)
|
|
64
|
+
* - MATHEMATICAL: y is the mathematical baseline
|
|
65
|
+
*
|
|
66
|
+
* Font metrics terminology:
|
|
67
|
+
* - Ascent: Distance from baseline to top of tallest glyphs (e.g., 'd', 'h')
|
|
68
|
+
* - Descent: Distance from baseline to bottom of lowest glyphs (e.g., 'g', 'p')
|
|
69
|
+
* - Em box: The design space containing all glyphs (ascent + descent)
|
|
70
|
+
*
|
|
71
|
+
* @enum {string}
|
|
72
|
+
* @example
|
|
73
|
+
* // Text sits on baseline at y=100 (default)
|
|
74
|
+
* textToPath("Hello", { y: 100, dominantBaseline: DominantBaseline.ALPHABETIC })
|
|
75
|
+
*
|
|
76
|
+
* // Text is vertically centered at y=100
|
|
77
|
+
* textToPath("Hello", { y: 100, dominantBaseline: DominantBaseline.MIDDLE })
|
|
78
|
+
*
|
|
79
|
+
* // Top of text aligns with y=100
|
|
80
|
+
* textToPath("Hello", { y: 100, dominantBaseline: DominantBaseline.HANGING })
|
|
81
|
+
*/
|
|
82
|
+
export const DominantBaseline = {
|
|
83
|
+
AUTO: 'auto',
|
|
84
|
+
MIDDLE: 'middle',
|
|
85
|
+
HANGING: 'hanging',
|
|
86
|
+
ALPHABETIC: 'alphabetic',
|
|
87
|
+
IDEOGRAPHIC: 'ideographic',
|
|
88
|
+
MATHEMATICAL: 'mathematical',
|
|
89
|
+
CENTRAL: 'central',
|
|
90
|
+
TEXT_AFTER_EDGE: 'text-after-edge',
|
|
91
|
+
TEXT_BEFORE_EDGE: 'text-before-edge'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse CSS font-size value and convert to pixels.
|
|
96
|
+
*
|
|
97
|
+
* Supports common CSS units:
|
|
98
|
+
* - px: Pixels (1:1 ratio)
|
|
99
|
+
* - pt: Points (1pt = 1.333px, based on 96 DPI)
|
|
100
|
+
* - em/rem: Relative to default font size (16px)
|
|
101
|
+
* - %: Percentage of default font size
|
|
102
|
+
*
|
|
103
|
+
* @param {string|number} value - Font size value (e.g., "16px", "1.5em", "12pt", "120%")
|
|
104
|
+
* @returns {number} Font size in pixels
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* parseFontSize("16px") // → 16
|
|
108
|
+
* parseFontSize("12pt") // → 16 (12 * 1.333)
|
|
109
|
+
* parseFontSize("1.5em") // → 24 (1.5 * 16)
|
|
110
|
+
* parseFontSize("150%") // → 24 (1.5 * 16)
|
|
111
|
+
* parseFontSize(20) // → 20 (assumes px)
|
|
112
|
+
* parseFontSize("invalid") // → 16 (default)
|
|
113
|
+
*/
|
|
114
|
+
export function parseFontSize(value) {
|
|
115
|
+
if (!value) return DEFAULT_FONT_SIZE;
|
|
116
|
+
|
|
117
|
+
const match = value.match(/^([\d.]+)(px|pt|em|rem|%)?$/i);
|
|
118
|
+
if (!match) return DEFAULT_FONT_SIZE;
|
|
119
|
+
|
|
120
|
+
const num = parseFloat(match[1]);
|
|
121
|
+
const unit = (match[2] || 'px').toLowerCase();
|
|
122
|
+
|
|
123
|
+
switch (unit) {
|
|
124
|
+
case 'px': return num;
|
|
125
|
+
case 'pt': return num * 1.333; // 1pt = 1.333px
|
|
126
|
+
case 'em':
|
|
127
|
+
case 'rem': return num * DEFAULT_FONT_SIZE;
|
|
128
|
+
case '%': return num / 100 * DEFAULT_FONT_SIZE;
|
|
129
|
+
default: return num;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate text metrics (dimensions and baseline offsets) for a string.
|
|
135
|
+
*
|
|
136
|
+
* Uses actual font metrics when available via opentype.js, otherwise
|
|
137
|
+
* falls back to character-count estimation.
|
|
138
|
+
*
|
|
139
|
+
* Font metrics explained:
|
|
140
|
+
* - Width: Horizontal advance width of the text
|
|
141
|
+
* - Height: Total vertical space (ascent + descent)
|
|
142
|
+
* - Ascent: Distance from baseline to top of tallest glyphs (e.g., 'd', 'h', 'b')
|
|
143
|
+
* - Descent: Distance from baseline to bottom of lowest glyphs (e.g., 'g', 'p', 'y')
|
|
144
|
+
*
|
|
145
|
+
* The baseline is the invisible line that most letters "sit" on. Letters like
|
|
146
|
+
* 'a', 'e', 'n' rest on the baseline, while 'd' extends above (ascent) and 'g'
|
|
147
|
+
* extends below (descent).
|
|
148
|
+
*
|
|
149
|
+
* @param {string} text - Text content to measure
|
|
150
|
+
* @param {Object} [style={}] - Text style options
|
|
151
|
+
* @param {string} [style.fontSize='16px'] - Font size (CSS value)
|
|
152
|
+
* @param {string} [style.fontFamily='sans-serif'] - Font family name
|
|
153
|
+
* @param {Object|null} [font=null] - Opentype.js font object for accurate metrics
|
|
154
|
+
* @returns {Object} Text metrics
|
|
155
|
+
* @returns {Decimal} returns.width - Text width in pixels
|
|
156
|
+
* @returns {Decimal} returns.height - Text height in pixels (ascent + descent)
|
|
157
|
+
* @returns {Decimal} returns.ascent - Distance from baseline to top
|
|
158
|
+
* @returns {Decimal} returns.descent - Distance from baseline to bottom (positive value)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* // With font object (accurate)
|
|
162
|
+
* const font = opentype.loadSync('Arial.ttf');
|
|
163
|
+
* const metrics = measureText("Hello", { fontSize: "20px" }, font);
|
|
164
|
+
* // metrics.width → Decimal(55.2)
|
|
165
|
+
* // metrics.ascent → Decimal(16.0)
|
|
166
|
+
* // metrics.descent → Decimal(4.0)
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Without font (estimated)
|
|
170
|
+
* const metrics = measureText("Hello", { fontSize: "16px" });
|
|
171
|
+
* // metrics.width → Decimal(44.0) // Estimated: 5 chars * 0.55 * 16px
|
|
172
|
+
*/
|
|
173
|
+
export function measureText(text, style = {}, font = null) {
|
|
174
|
+
const fontSize = parseFontSize(style.fontSize);
|
|
175
|
+
const scale = fontSize / DEFAULT_UNITS_PER_EM;
|
|
176
|
+
|
|
177
|
+
if (font && font.getAdvanceWidth) {
|
|
178
|
+
// Use actual font metrics
|
|
179
|
+
const width = font.getAdvanceWidth(text, fontSize);
|
|
180
|
+
const ascent = font.ascender * scale;
|
|
181
|
+
const descent = -font.descender * scale;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
width: D(width),
|
|
185
|
+
height: D(ascent + descent),
|
|
186
|
+
ascent: D(ascent),
|
|
187
|
+
descent: D(descent)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fallback: estimate based on character count
|
|
192
|
+
// Average character width is ~0.5em for proportional fonts
|
|
193
|
+
const avgCharWidth = fontSize * 0.55;
|
|
194
|
+
const width = text.length * avgCharWidth;
|
|
195
|
+
const ascent = DEFAULT_ASCENDER * scale;
|
|
196
|
+
const descent = -DEFAULT_DESCENDER * scale;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
width: D(width),
|
|
200
|
+
height: D(ascent + descent),
|
|
201
|
+
ascent: D(ascent),
|
|
202
|
+
descent: D(descent)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert a single character to SVG path data using font glyph outlines.
|
|
208
|
+
*
|
|
209
|
+
* A glyph is the visual representation of a character in a specific font.
|
|
210
|
+
* This function extracts the glyph's vector outline and converts it to
|
|
211
|
+
* SVG path commands (M, L, C, Z, etc.).
|
|
212
|
+
*
|
|
213
|
+
* When font is unavailable, returns a rectangular approximation based on
|
|
214
|
+
* estimated character dimensions.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} char - Single character to convert to path
|
|
217
|
+
* @param {Object|null} font - Opentype.js font object (if null, uses fallback)
|
|
218
|
+
* @param {number} fontSize - Font size in pixels
|
|
219
|
+
* @param {number} [x=0] - X position of the character baseline
|
|
220
|
+
* @param {number} [y=0] - Y position of the character baseline
|
|
221
|
+
* @returns {string} SVG path data string (e.g., "M10,20 L30,40 Z")
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // With font object (produces actual glyph outline)
|
|
225
|
+
* const font = opentype.loadSync('Arial.ttf');
|
|
226
|
+
* const path = getCharPath('A', font, 24, 100, 50);
|
|
227
|
+
* // → "M100,50 L112,20 L124,50 Z M106,35 L118,35 Z"
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Without font (produces rectangle approximation)
|
|
231
|
+
* const path = getCharPath('A', null, 16, 0, 0);
|
|
232
|
+
* // → "M0,-12.8 L8.8,-12.8 L8.8,3.2 L0,3.2 Z"
|
|
233
|
+
*/
|
|
234
|
+
export function getCharPath(char, font, fontSize, x = 0, y = 0) {
|
|
235
|
+
if (font && font.getPath) {
|
|
236
|
+
const path = font.getPath(char, x, y, fontSize);
|
|
237
|
+
return path.toPathData();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fallback: return a rectangle approximation
|
|
241
|
+
const metrics = measureText(char, { fontSize: fontSize + 'px' });
|
|
242
|
+
const width = Number(metrics.width);
|
|
243
|
+
const ascent = Number(metrics.ascent);
|
|
244
|
+
const descent = Number(metrics.descent);
|
|
245
|
+
|
|
246
|
+
return `M${x},${y - ascent} L${x + width},${y - ascent} L${x + width},${y + descent} L${x},${y + descent} Z`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Convert a text string to SVG path data with full layout support.
|
|
251
|
+
*
|
|
252
|
+
* This is the main text-to-path conversion function. It:
|
|
253
|
+
* 1. Measures the text to determine its bounding box
|
|
254
|
+
* 2. Applies text-anchor alignment (horizontal positioning)
|
|
255
|
+
* 3. Applies dominant-baseline alignment (vertical positioning)
|
|
256
|
+
* 4. Converts each character to a glyph path
|
|
257
|
+
* 5. Positions each glyph with letter-spacing applied
|
|
258
|
+
*
|
|
259
|
+
* Text rendering concepts:
|
|
260
|
+
* - Advance width: The horizontal distance to move after rendering a character
|
|
261
|
+
* - Letter spacing: Additional space added between characters
|
|
262
|
+
* - Kerning: Font-specific spacing adjustments between character pairs (not yet supported)
|
|
263
|
+
*
|
|
264
|
+
* @param {string} text - Text content to convert
|
|
265
|
+
* @param {Object} [options={}] - Layout and styling options
|
|
266
|
+
* @param {number} [options.x=0] - X coordinate of text anchor point
|
|
267
|
+
* @param {number} [options.y=0] - Y coordinate of text baseline
|
|
268
|
+
* @param {number|string} [options.fontSize=16] - Font size (number in px or CSS string)
|
|
269
|
+
* @param {string} [options.fontFamily='sans-serif'] - Font family name
|
|
270
|
+
* @param {string} [options.textAnchor='start'] - Horizontal alignment (start|middle|end)
|
|
271
|
+
* @param {string} [options.dominantBaseline='auto'] - Vertical alignment
|
|
272
|
+
* @param {number|string} [options.letterSpacing=0] - Extra space between characters
|
|
273
|
+
* @param {Object|null} [options.font=null] - Opentype.js font object for accurate rendering
|
|
274
|
+
* @returns {string} Combined SVG path data for all characters (space-separated)
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* // Basic usage with default alignment (left-aligned, sitting on baseline)
|
|
278
|
+
* const path = textToPath("Hello", { x: 100, y: 100, fontSize: 20 });
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* // Center-aligned, vertically centered text
|
|
282
|
+
* const path = textToPath("Hello", {
|
|
283
|
+
* x: 100,
|
|
284
|
+
* y: 100,
|
|
285
|
+
* fontSize: 20,
|
|
286
|
+
* textAnchor: TextAnchor.MIDDLE,
|
|
287
|
+
* dominantBaseline: DominantBaseline.MIDDLE
|
|
288
|
+
* });
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* // With font object and letter spacing
|
|
292
|
+
* const font = opentype.loadSync('Arial.ttf');
|
|
293
|
+
* const path = textToPath("Spaced", {
|
|
294
|
+
* x: 0,
|
|
295
|
+
* y: 50,
|
|
296
|
+
* fontSize: 24,
|
|
297
|
+
* letterSpacing: 5,
|
|
298
|
+
* font: font
|
|
299
|
+
* });
|
|
300
|
+
*/
|
|
301
|
+
export function textToPath(text, options = {}) {
|
|
302
|
+
const {
|
|
303
|
+
x = 0,
|
|
304
|
+
y = 0,
|
|
305
|
+
fontSize = DEFAULT_FONT_SIZE,
|
|
306
|
+
fontFamily = 'sans-serif',
|
|
307
|
+
textAnchor = TextAnchor.START,
|
|
308
|
+
dominantBaseline = DominantBaseline.AUTO,
|
|
309
|
+
letterSpacing = 0,
|
|
310
|
+
font = null
|
|
311
|
+
} = options;
|
|
312
|
+
|
|
313
|
+
if (!text || text.length === 0) return '';
|
|
314
|
+
|
|
315
|
+
const fontSizePx = parseFontSize(fontSize + (typeof fontSize === 'number' ? 'px' : ''));
|
|
316
|
+
const metrics = measureText(text, { fontSize: fontSizePx + 'px' }, font);
|
|
317
|
+
|
|
318
|
+
// Calculate starting position based on text-anchor
|
|
319
|
+
let startX = D(x);
|
|
320
|
+
switch (textAnchor) {
|
|
321
|
+
case TextAnchor.MIDDLE:
|
|
322
|
+
startX = startX.minus(metrics.width.div(2));
|
|
323
|
+
break;
|
|
324
|
+
case TextAnchor.END:
|
|
325
|
+
startX = startX.minus(metrics.width);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Adjust for dominant-baseline
|
|
330
|
+
let baselineY = D(y);
|
|
331
|
+
switch (dominantBaseline) {
|
|
332
|
+
case DominantBaseline.MIDDLE:
|
|
333
|
+
case DominantBaseline.CENTRAL:
|
|
334
|
+
baselineY = baselineY.plus(metrics.ascent.minus(metrics.height.div(2)));
|
|
335
|
+
break;
|
|
336
|
+
case DominantBaseline.HANGING:
|
|
337
|
+
case DominantBaseline.TEXT_BEFORE_EDGE:
|
|
338
|
+
baselineY = baselineY.plus(metrics.ascent);
|
|
339
|
+
break;
|
|
340
|
+
case DominantBaseline.IDEOGRAPHIC:
|
|
341
|
+
case DominantBaseline.TEXT_AFTER_EDGE:
|
|
342
|
+
baselineY = baselineY.minus(metrics.descent);
|
|
343
|
+
break;
|
|
344
|
+
// AUTO and ALPHABETIC use default baseline
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Build path for each character
|
|
348
|
+
const paths = [];
|
|
349
|
+
let currentX = startX;
|
|
350
|
+
const letterSpacingPx = parseFontSize(letterSpacing + (typeof letterSpacing === 'number' ? 'px' : ''));
|
|
351
|
+
|
|
352
|
+
for (const char of text) {
|
|
353
|
+
if (char === ' ') {
|
|
354
|
+
// Space - just advance
|
|
355
|
+
const spaceMetrics = measureText(' ', { fontSize: fontSizePx + 'px' }, font);
|
|
356
|
+
currentX = currentX.plus(spaceMetrics.width).plus(letterSpacingPx);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const charPath = getCharPath(char, font, fontSizePx, Number(currentX), Number(baselineY));
|
|
361
|
+
paths.push(charPath);
|
|
362
|
+
|
|
363
|
+
const charMetrics = measureText(char, { fontSize: fontSizePx + 'px' }, font);
|
|
364
|
+
currentX = currentX.plus(charMetrics.width).plus(letterSpacingPx);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return paths.join(' ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Convert text to a polygon representation of its bounding box.
|
|
372
|
+
*
|
|
373
|
+
* This function is used for clipping operations. Instead of converting
|
|
374
|
+
* text to actual glyph paths (which is computationally expensive), it
|
|
375
|
+
* creates a simple rectangular polygon that approximates the text's area.
|
|
376
|
+
*
|
|
377
|
+
* The polygon is a rectangle with 4 vertices, positioned according to
|
|
378
|
+
* text-anchor and dominant-baseline alignment rules.
|
|
379
|
+
*
|
|
380
|
+
* Use case: When you need to clip text against shapes, this provides
|
|
381
|
+
* a fast approximation. For precise clipping of actual letter shapes,
|
|
382
|
+
* use textToPath() instead.
|
|
383
|
+
*
|
|
384
|
+
* @param {Object} textElement - Text element configuration
|
|
385
|
+
* @param {number} [textElement.x=0] - X coordinate
|
|
386
|
+
* @param {number} [textElement.y=0] - Y coordinate
|
|
387
|
+
* @param {number|string} [textElement.fontSize=16] - Font size
|
|
388
|
+
* @param {string} [textElement.textAnchor='start'] - Horizontal alignment
|
|
389
|
+
* @param {string} [textElement.dominantBaseline='auto'] - Vertical alignment
|
|
390
|
+
* @param {string} textElement.text - Text content
|
|
391
|
+
* @param {Object} [options={}] - Additional options
|
|
392
|
+
* @param {number} [options.samples=10] - Unused (reserved for future path sampling)
|
|
393
|
+
* @param {Object|null} [options.font=null] - Opentype.js font for accurate metrics
|
|
394
|
+
* @returns {Array<Array<Decimal>>} Array of 4 polygon vertices [[x,y], [x,y], [x,y], [x,y]]
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* // Get bounding box polygon for text
|
|
398
|
+
* const polygon = textToPolygon({
|
|
399
|
+
* x: 100,
|
|
400
|
+
* y: 100,
|
|
401
|
+
* fontSize: 20,
|
|
402
|
+
* text: "Hello",
|
|
403
|
+
* textAnchor: TextAnchor.START
|
|
404
|
+
* });
|
|
405
|
+
* // → [[Decimal(100), Decimal(84)], [Decimal(155), Decimal(84)],
|
|
406
|
+
* // [Decimal(155), Decimal(104)], [Decimal(100), Decimal(104)]]
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* // Use for clipping operations
|
|
410
|
+
* const textBox = textToPolygon({ x: 50, y: 50, text: "Clip me" });
|
|
411
|
+
* const circle = createCirclePolygon(50, 50, 30);
|
|
412
|
+
* const clipped = PolygonClip.polygonIntersection(textBox, circle);
|
|
413
|
+
*/
|
|
414
|
+
export function textToPolygon(textElement, options = {}) {
|
|
415
|
+
const {
|
|
416
|
+
x = 0,
|
|
417
|
+
y = 0,
|
|
418
|
+
fontSize = DEFAULT_FONT_SIZE,
|
|
419
|
+
textAnchor = TextAnchor.START,
|
|
420
|
+
dominantBaseline = DominantBaseline.AUTO,
|
|
421
|
+
text = ''
|
|
422
|
+
} = textElement;
|
|
423
|
+
|
|
424
|
+
const { samples = 10, font = null } = options;
|
|
425
|
+
const fontSizePx = parseFontSize(fontSize + (typeof fontSize === 'number' ? 'px' : ''));
|
|
426
|
+
const metrics = measureText(text, { fontSize: fontSizePx + 'px' }, font);
|
|
427
|
+
|
|
428
|
+
// Calculate bounding box
|
|
429
|
+
let startX = D(x);
|
|
430
|
+
switch (textAnchor) {
|
|
431
|
+
case TextAnchor.MIDDLE:
|
|
432
|
+
startX = startX.minus(metrics.width.div(2));
|
|
433
|
+
break;
|
|
434
|
+
case TextAnchor.END:
|
|
435
|
+
startX = startX.minus(metrics.width);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let topY = D(y).minus(metrics.ascent);
|
|
440
|
+
switch (dominantBaseline) {
|
|
441
|
+
case DominantBaseline.MIDDLE:
|
|
442
|
+
case DominantBaseline.CENTRAL:
|
|
443
|
+
topY = D(y).minus(metrics.height.div(2));
|
|
444
|
+
break;
|
|
445
|
+
case DominantBaseline.HANGING:
|
|
446
|
+
case DominantBaseline.TEXT_BEFORE_EDGE:
|
|
447
|
+
topY = D(y);
|
|
448
|
+
break;
|
|
449
|
+
case DominantBaseline.IDEOGRAPHIC:
|
|
450
|
+
case DominantBaseline.TEXT_AFTER_EDGE:
|
|
451
|
+
topY = D(y).minus(metrics.height).plus(metrics.descent);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Return bounding box as polygon
|
|
456
|
+
const endX = startX.plus(metrics.width);
|
|
457
|
+
const bottomY = topY.plus(metrics.height);
|
|
458
|
+
|
|
459
|
+
return [
|
|
460
|
+
PolygonClip.point(startX, topY),
|
|
461
|
+
PolygonClip.point(endX, topY),
|
|
462
|
+
PolygonClip.point(endX, bottomY),
|
|
463
|
+
PolygonClip.point(startX, bottomY)
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Parse an SVG <text> DOM element into a structured data object.
|
|
469
|
+
*
|
|
470
|
+
* Extracts all relevant attributes and child elements (tspan) from an
|
|
471
|
+
* SVG text element, converting them into a format suitable for processing.
|
|
472
|
+
*
|
|
473
|
+
* SVG text structure:
|
|
474
|
+
* - <text>: Main container with position and style
|
|
475
|
+
* - <tspan>: Inline spans within text that can have different positions/styles
|
|
476
|
+
* - Direct text nodes: Text content not wrapped in tspan
|
|
477
|
+
*
|
|
478
|
+
* Position attributes:
|
|
479
|
+
* - x, y: Absolute position
|
|
480
|
+
* - dx, dy: Relative offset from x, y
|
|
481
|
+
*
|
|
482
|
+
* Style can come from:
|
|
483
|
+
* 1. Inline style attribute: style="font-size: 20px; fill: red"
|
|
484
|
+
* 2. Direct attributes: font-size="20" fill="red"
|
|
485
|
+
* 3. CSS classes (not handled by this function)
|
|
486
|
+
*
|
|
487
|
+
* @param {Element} textElement - SVG <text> DOM element to parse
|
|
488
|
+
* @returns {Object} Structured text data
|
|
489
|
+
* @returns {number} returns.x - X coordinate
|
|
490
|
+
* @returns {number} returns.y - Y coordinate
|
|
491
|
+
* @returns {number} returns.dx - X offset
|
|
492
|
+
* @returns {number} returns.dy - Y offset
|
|
493
|
+
* @returns {string} returns.text - Direct text content (excluding tspans)
|
|
494
|
+
* @returns {Object} returns.style - Extracted style properties
|
|
495
|
+
* @returns {string} returns.style.fontSize - Font size (e.g., "16px")
|
|
496
|
+
* @returns {string} returns.style.fontFamily - Font family name
|
|
497
|
+
* @returns {string} returns.style.fontWeight - Font weight (normal|bold|100-900)
|
|
498
|
+
* @returns {string} returns.style.fontStyle - Font style (normal|italic|oblique)
|
|
499
|
+
* @returns {string} returns.style.textAnchor - Horizontal alignment
|
|
500
|
+
* @returns {string} returns.style.dominantBaseline - Vertical alignment
|
|
501
|
+
* @returns {string} returns.style.letterSpacing - Letter spacing
|
|
502
|
+
* @returns {string} returns.style.fill - Fill color
|
|
503
|
+
* @returns {Array<Object>} returns.tspans - Array of tspan elements
|
|
504
|
+
* @returns {string|null} returns.transform - Transform attribute value
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* // Parse a simple text element
|
|
508
|
+
* const svg = document.querySelector('svg');
|
|
509
|
+
* const textEl = svg.querySelector('text');
|
|
510
|
+
* const data = parseTextElement(textEl);
|
|
511
|
+
* // data.text → "Hello"
|
|
512
|
+
* // data.style.fontSize → "20px"
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* // Parse text with tspans
|
|
516
|
+
* // <text x="10" y="20">Hello <tspan dy="5">World</tspan></text>
|
|
517
|
+
* const data = parseTextElement(textEl);
|
|
518
|
+
* // data.text → "Hello"
|
|
519
|
+
* // data.tspans[0].text → "World"
|
|
520
|
+
* // data.tspans[0].dy → 5
|
|
521
|
+
*/
|
|
522
|
+
export function parseTextElement(textElement) {
|
|
523
|
+
const data = {
|
|
524
|
+
x: parseFloat(textElement.getAttribute('x') || '0'),
|
|
525
|
+
y: parseFloat(textElement.getAttribute('y') || '0'),
|
|
526
|
+
dx: parseFloat(textElement.getAttribute('dx') || '0'),
|
|
527
|
+
dy: parseFloat(textElement.getAttribute('dy') || '0'),
|
|
528
|
+
text: '',
|
|
529
|
+
style: {},
|
|
530
|
+
tspans: [],
|
|
531
|
+
transform: textElement.getAttribute('transform') || null
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Extract style attributes
|
|
535
|
+
const style = textElement.getAttribute('style') || '';
|
|
536
|
+
const styleObj = {};
|
|
537
|
+
style.split(';').forEach(prop => {
|
|
538
|
+
const [key, value] = prop.split(':').map(s => s.trim());
|
|
539
|
+
if (key && value) styleObj[key] = value;
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
data.style = {
|
|
543
|
+
fontSize: styleObj['font-size'] || textElement.getAttribute('font-size') || '16px',
|
|
544
|
+
fontFamily: styleObj['font-family'] || textElement.getAttribute('font-family') || 'sans-serif',
|
|
545
|
+
fontWeight: styleObj['font-weight'] || textElement.getAttribute('font-weight') || 'normal',
|
|
546
|
+
fontStyle: styleObj['font-style'] || textElement.getAttribute('font-style') || 'normal',
|
|
547
|
+
textAnchor: styleObj['text-anchor'] || textElement.getAttribute('text-anchor') || 'start',
|
|
548
|
+
dominantBaseline: styleObj['dominant-baseline'] || textElement.getAttribute('dominant-baseline') || 'auto',
|
|
549
|
+
letterSpacing: styleObj['letter-spacing'] || textElement.getAttribute('letter-spacing') || '0',
|
|
550
|
+
fill: styleObj['fill'] || textElement.getAttribute('fill') || 'black'
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// Get direct text content (excluding tspan content)
|
|
554
|
+
const directText = [];
|
|
555
|
+
for (const node of textElement.childNodes) {
|
|
556
|
+
if (node.nodeType === 3) { // Text node
|
|
557
|
+
directText.push(node.textContent);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
data.text = directText.join('').trim();
|
|
561
|
+
|
|
562
|
+
// Parse tspan children
|
|
563
|
+
const tspans = textElement.querySelectorAll('tspan');
|
|
564
|
+
tspans.forEach(tspan => {
|
|
565
|
+
data.tspans.push({
|
|
566
|
+
x: tspan.getAttribute('x') ? parseFloat(tspan.getAttribute('x')) : null,
|
|
567
|
+
y: tspan.getAttribute('y') ? parseFloat(tspan.getAttribute('y')) : null,
|
|
568
|
+
dx: parseFloat(tspan.getAttribute('dx') || '0'),
|
|
569
|
+
dy: parseFloat(tspan.getAttribute('dy') || '0'),
|
|
570
|
+
text: tspan.textContent || ''
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
return data;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Convert parsed text data to SVG path element data with tspan support.
|
|
579
|
+
*
|
|
580
|
+
* This is a high-level function that processes an entire text element
|
|
581
|
+
* including all its tspan children. It:
|
|
582
|
+
* 1. Converts the main text content to path data
|
|
583
|
+
* 2. Converts each tspan to path data at its specific position
|
|
584
|
+
* 3. Combines all paths into a single path data string
|
|
585
|
+
* 4. Preserves fill color and transform attributes
|
|
586
|
+
*
|
|
587
|
+
* tspan positioning rules:
|
|
588
|
+
* - If tspan has x/y: Use those absolute coordinates
|
|
589
|
+
* - If tspan has only dx/dy: Add to previous position
|
|
590
|
+
* - If tspan has both: x/y sets absolute, dx/dy adds offset
|
|
591
|
+
*
|
|
592
|
+
* The function tracks the "current position" which advances after each
|
|
593
|
+
* tspan based on its text width.
|
|
594
|
+
*
|
|
595
|
+
* @param {Object} textData - Parsed text data from parseTextElement()
|
|
596
|
+
* @param {number} textData.x - Text X position
|
|
597
|
+
* @param {number} textData.y - Text Y position
|
|
598
|
+
* @param {number} textData.dx - X offset
|
|
599
|
+
* @param {number} textData.dy - Y offset
|
|
600
|
+
* @param {string} textData.text - Main text content
|
|
601
|
+
* @param {Object} textData.style - Text styling
|
|
602
|
+
* @param {Array<Object>} textData.tspans - Array of tspan data
|
|
603
|
+
* @param {string|null} textData.transform - Transform attribute
|
|
604
|
+
* @param {Object} [options={}] - Conversion options
|
|
605
|
+
* @param {Object|null} [options.font=null] - Opentype.js font object
|
|
606
|
+
* @returns {Object} Path element data ready for SVG rendering
|
|
607
|
+
* @returns {string} returns.d - Combined SVG path data string
|
|
608
|
+
* @returns {string} returns.fill - Fill color
|
|
609
|
+
* @returns {string|null} returns.transform - Transform attribute (preserved)
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* // Convert simple text
|
|
613
|
+
* const textData = parseTextElement(textElement);
|
|
614
|
+
* const pathData = textElementToPath(textData);
|
|
615
|
+
* // pathData.d → "M10,20 L15,20 C20,25..." (path commands)
|
|
616
|
+
* // pathData.fill → "black"
|
|
617
|
+
* // pathData.transform → null
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* // Convert text with tspans and transform
|
|
621
|
+
* // <text x="10" y="20" transform="rotate(45)">
|
|
622
|
+
* // Hello <tspan dy="5">World</tspan>
|
|
623
|
+
* // </text>
|
|
624
|
+
* const pathData = textElementToPath(textData, { font: myFont });
|
|
625
|
+
* // pathData.d → "M10,20 L... M15,25 L..." (main text + tspan paths)
|
|
626
|
+
* // pathData.transform → "rotate(45)"
|
|
627
|
+
*/
|
|
628
|
+
export function textElementToPath(textData, options = {}) {
|
|
629
|
+
const { font = null } = options;
|
|
630
|
+
|
|
631
|
+
const pathOptions = {
|
|
632
|
+
x: textData.x + textData.dx,
|
|
633
|
+
y: textData.y + textData.dy,
|
|
634
|
+
fontSize: textData.style.fontSize,
|
|
635
|
+
fontFamily: textData.style.fontFamily,
|
|
636
|
+
textAnchor: textData.style.textAnchor,
|
|
637
|
+
dominantBaseline: textData.style.dominantBaseline,
|
|
638
|
+
letterSpacing: textData.style.letterSpacing,
|
|
639
|
+
font
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Convert main text
|
|
643
|
+
let pathData = textToPath(textData.text, pathOptions);
|
|
644
|
+
|
|
645
|
+
// Convert tspans
|
|
646
|
+
let currentX = pathOptions.x;
|
|
647
|
+
let currentY = pathOptions.y;
|
|
648
|
+
|
|
649
|
+
for (const tspan of textData.tspans) {
|
|
650
|
+
const tspanX = tspan.x !== null ? tspan.x : currentX + tspan.dx;
|
|
651
|
+
const tspanY = tspan.y !== null ? tspan.y : currentY + tspan.dy;
|
|
652
|
+
|
|
653
|
+
const tspanPath = textToPath(tspan.text, {
|
|
654
|
+
...pathOptions,
|
|
655
|
+
x: tspanX,
|
|
656
|
+
y: tspanY
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (tspanPath) {
|
|
660
|
+
pathData += ' ' + tspanPath;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Update position for next tspan
|
|
664
|
+
const metrics = measureText(tspan.text, { fontSize: pathOptions.fontSize }, font);
|
|
665
|
+
currentX = tspanX + Number(metrics.width);
|
|
666
|
+
currentY = tspanY;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
d: pathData.trim(),
|
|
671
|
+
fill: textData.style.fill,
|
|
672
|
+
transform: textData.transform
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Calculate the axis-aligned bounding box for text.
|
|
678
|
+
*
|
|
679
|
+
* Returns the smallest rectangle that completely contains the text,
|
|
680
|
+
* accounting for text-anchor alignment. This is useful for:
|
|
681
|
+
* - Hit testing (checking if point is inside text area)
|
|
682
|
+
* - Layout calculations (positioning elements around text)
|
|
683
|
+
* - Viewport optimization (determining what text is visible)
|
|
684
|
+
*
|
|
685
|
+
* The bounding box includes all text and tspans combined.
|
|
686
|
+
*
|
|
687
|
+
* Note: This returns the bounding box in the text's local coordinate space,
|
|
688
|
+
* before any transforms are applied. If textData has a transform attribute,
|
|
689
|
+
* you'll need to apply it separately to get screen coordinates.
|
|
690
|
+
*
|
|
691
|
+
* @param {Object} textData - Parsed text data from parseTextElement()
|
|
692
|
+
* @param {number} textData.x - Text X position
|
|
693
|
+
* @param {number} textData.y - Text Y position
|
|
694
|
+
* @param {number} textData.dx - X offset
|
|
695
|
+
* @param {number} textData.dy - Y offset
|
|
696
|
+
* @param {string} textData.text - Text content
|
|
697
|
+
* @param {Object} textData.style - Text styling
|
|
698
|
+
* @param {string} textData.style.fontSize - Font size
|
|
699
|
+
* @param {string} textData.style.textAnchor - Horizontal alignment
|
|
700
|
+
* @param {Array<Object>} textData.tspans - Array of tspan data
|
|
701
|
+
* @param {Object} [options={}] - Options
|
|
702
|
+
* @param {Object|null} [options.font=null] - Opentype.js font object
|
|
703
|
+
* @returns {Object} Axis-aligned bounding box
|
|
704
|
+
* @returns {number} returns.x - Left edge X coordinate
|
|
705
|
+
* @returns {number} returns.y - Top edge Y coordinate
|
|
706
|
+
* @returns {number} returns.width - Box width in pixels
|
|
707
|
+
* @returns {number} returns.height - Box height in pixels
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* // Get bounding box for left-aligned text
|
|
711
|
+
* const textData = { x: 100, y: 50, dx: 0, dy: 0, text: "Hello",
|
|
712
|
+
* style: { fontSize: "20px", textAnchor: "start" },
|
|
713
|
+
* tspans: [] };
|
|
714
|
+
* const bbox = getTextBBox(textData);
|
|
715
|
+
* // bbox → { x: 100, y: 34, width: 55, height: 20 }
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* // Get bounding box for center-aligned text
|
|
719
|
+
* const textData = { x: 100, y: 50, dx: 0, dy: 0, text: "Hello",
|
|
720
|
+
* style: { fontSize: "20px", textAnchor: "middle" },
|
|
721
|
+
* tspans: [] };
|
|
722
|
+
* const bbox = getTextBBox(textData);
|
|
723
|
+
* // bbox → { x: 72.5, y: 34, width: 55, height: 20 }
|
|
724
|
+
*/
|
|
725
|
+
export function getTextBBox(textData, options = {}) {
|
|
726
|
+
const { font = null } = options;
|
|
727
|
+
|
|
728
|
+
const fontSize = parseFontSize(textData.style.fontSize);
|
|
729
|
+
let totalText = textData.text;
|
|
730
|
+
|
|
731
|
+
for (const tspan of textData.tspans) {
|
|
732
|
+
totalText += tspan.text;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const metrics = measureText(totalText, { fontSize: fontSize + 'px' }, font);
|
|
736
|
+
|
|
737
|
+
let x = D(textData.x + textData.dx);
|
|
738
|
+
const textAnchor = textData.style.textAnchor;
|
|
739
|
+
|
|
740
|
+
switch (textAnchor) {
|
|
741
|
+
case TextAnchor.MIDDLE:
|
|
742
|
+
x = x.minus(metrics.width.div(2));
|
|
743
|
+
break;
|
|
744
|
+
case TextAnchor.END:
|
|
745
|
+
x = x.minus(metrics.width);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
x: Number(x),
|
|
751
|
+
y: textData.y + textData.dy - Number(metrics.ascent),
|
|
752
|
+
width: Number(metrics.width),
|
|
753
|
+
height: Number(metrics.height)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Clip text against a polygon using bounding box intersection.
|
|
759
|
+
*
|
|
760
|
+
* This function performs polygon clipping between a text's bounding box
|
|
761
|
+
* and an arbitrary clipping polygon. It's used to determine which parts
|
|
762
|
+
* of the text are visible within a clipping region.
|
|
763
|
+
*
|
|
764
|
+
* Clipping process:
|
|
765
|
+
* 1. Convert text to its bounding box polygon (4 vertices)
|
|
766
|
+
* 2. Compute intersection with the clip polygon
|
|
767
|
+
* 3. Return resulting polygon(s) representing visible area
|
|
768
|
+
*
|
|
769
|
+
* The result is an array of polygons because clipping can produce:
|
|
770
|
+
* - Empty array: Text is completely clipped (not visible)
|
|
771
|
+
* - One polygon: Simple intersection
|
|
772
|
+
* - Multiple polygons: If clip region creates disconnected visible areas
|
|
773
|
+
*
|
|
774
|
+
* Note: This clips the text's bounding box, not the actual glyph shapes.
|
|
775
|
+
* For precise glyph-level clipping, convert text to paths first using
|
|
776
|
+
* textToPath() then clip those paths.
|
|
777
|
+
*
|
|
778
|
+
* @param {Object} textData - Parsed text data from parseTextElement()
|
|
779
|
+
* @param {number} textData.x - Text X position
|
|
780
|
+
* @param {number} textData.y - Text Y position
|
|
781
|
+
* @param {number} textData.dx - X offset
|
|
782
|
+
* @param {number} textData.dy - Y offset
|
|
783
|
+
* @param {string} textData.text - Text content
|
|
784
|
+
* @param {Object} textData.style - Text styling with fontSize, textAnchor, dominantBaseline
|
|
785
|
+
* @param {Array<Array<Decimal>>} clipPolygon - Clipping polygon vertices [[x,y], [x,y], ...]
|
|
786
|
+
* @param {Object} [options={}] - Options
|
|
787
|
+
* @param {Object|null} [options.font=null] - Opentype.js font object
|
|
788
|
+
* @returns {Array<Array<Array<Decimal>>>} Array of clipped polygons (may be empty)
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* // Clip text against a rectangular region
|
|
792
|
+
* const textData = parseTextElement(textElement);
|
|
793
|
+
* const clipRect = [
|
|
794
|
+
* PolygonClip.point(0, 0),
|
|
795
|
+
* PolygonClip.point(100, 0),
|
|
796
|
+
* PolygonClip.point(100, 100),
|
|
797
|
+
* PolygonClip.point(0, 100)
|
|
798
|
+
* ];
|
|
799
|
+
* const clipped = clipText(textData, clipRect);
|
|
800
|
+
* // clipped → [[[x1,y1], [x2,y2], ...]] (visible portion)
|
|
801
|
+
*
|
|
802
|
+
* @example
|
|
803
|
+
* // Check if text is completely clipped
|
|
804
|
+
* const clipped = clipText(textData, smallClipRegion);
|
|
805
|
+
* if (clipped.length === 0) {
|
|
806
|
+
* console.log("Text is not visible");
|
|
807
|
+
* }
|
|
808
|
+
*/
|
|
809
|
+
export function clipText(textData, clipPolygon, options = {}) {
|
|
810
|
+
const textPolygon = textToPolygon({
|
|
811
|
+
x: textData.x + textData.dx,
|
|
812
|
+
y: textData.y + textData.dy,
|
|
813
|
+
fontSize: textData.style.fontSize,
|
|
814
|
+
textAnchor: textData.style.textAnchor,
|
|
815
|
+
dominantBaseline: textData.style.dominantBaseline,
|
|
816
|
+
text: textData.text
|
|
817
|
+
}, options);
|
|
818
|
+
|
|
819
|
+
return PolygonClip.polygonIntersection(textPolygon, clipPolygon);
|
|
820
|
+
}
|