@emasoft/svg-matrix 1.0.3 → 1.0.4
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 +140 -2
- package/package.json +1 -1
- package/samples/test.svg +39 -0
- package/src/index.js +2 -1
- package/src/svg-flatten.js +335 -0
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ Arbitrary-precision matrix, vector and affine transformation library for JavaScr
|
|
|
7
7
|
- **Decimal-backed** Matrix and Vector classes for high-precision geometry
|
|
8
8
|
- **2D transforms** (3x3 homogeneous matrices): translation, rotation, scale, skew, reflection
|
|
9
9
|
- **3D transforms** (4x4 homogeneous matrices): translation, rotation (X/Y/Z axis), scale, reflection
|
|
10
|
+
- **SVG transform flattening**: parse transform attributes, build CTMs, flatten nested hierarchies
|
|
10
11
|
- **Linear algebra**: LU/QR decomposition, determinant, inverse, solve, matrix exponential
|
|
12
|
+
- **10^77 times better precision** than JavaScript floats for round-trip transforms
|
|
11
13
|
- Works in **Node.js** and **browsers** (via CDN)
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
@@ -19,7 +21,7 @@ npm install @emasoft/svg-matrix
|
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
```js
|
|
22
|
-
import { Decimal, Matrix, Vector, Transforms2D, Transforms3D } from '@emasoft/svg-matrix';
|
|
24
|
+
import { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from '@emasoft/svg-matrix';
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
### CDN (Browser)
|
|
@@ -28,7 +30,7 @@ Using esm.sh (recommended - auto-resolves dependencies):
|
|
|
28
30
|
|
|
29
31
|
```html
|
|
30
32
|
<script type="module">
|
|
31
|
-
import { Decimal, Matrix, Vector, Transforms2D, Transforms3D } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
33
|
+
import { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
32
34
|
|
|
33
35
|
Decimal.set({ precision: 80 });
|
|
34
36
|
|
|
@@ -272,6 +274,142 @@ const [x, y, z] = Transforms3D.applyTransform(M, 1, 0, 0);
|
|
|
272
274
|
| `reflectOrigin()` | Reflect through origin |
|
|
273
275
|
| `applyTransform(M, x, y, z)` | Apply matrix to point |
|
|
274
276
|
|
|
277
|
+
### SVGFlatten
|
|
278
|
+
|
|
279
|
+
| Function | Description |
|
|
280
|
+
|----------|-------------|
|
|
281
|
+
| `parseTransformFunction(func, args)` | Parse a single SVG transform function |
|
|
282
|
+
| `parseTransformAttribute(str)` | Parse a full SVG transform attribute string |
|
|
283
|
+
| `buildCTM(transformStack)` | Build CTM from array of transform strings |
|
|
284
|
+
| `applyToPoint(ctm, x, y)` | Apply CTM to a 2D point |
|
|
285
|
+
| `toSVGMatrix(ctm, precision?)` | Convert CTM back to SVG matrix() notation |
|
|
286
|
+
| `isIdentity(m, tolerance?)` | Check if matrix is effectively identity |
|
|
287
|
+
| `transformPathData(pathD, ctm)` | Transform path data coordinates |
|
|
288
|
+
| `PRECISION_INFO` | Object with precision comparison data |
|
|
289
|
+
|
|
290
|
+
## SVG Transform Flattening
|
|
291
|
+
|
|
292
|
+
The `SVGFlatten` module provides tools for parsing SVG transform attributes, building CTMs (Current Transform Matrices), and flattening nested transforms with arbitrary precision.
|
|
293
|
+
|
|
294
|
+
### Why Use SVGFlatten?
|
|
295
|
+
|
|
296
|
+
SVG elements can have deeply nested transforms through parent groups. When coordinates are transformed from local space to viewport and back using JavaScript's native 64-bit floats, precision is lost:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
Original coordinates: (10, 10)
|
|
300
|
+
After round-trip (float): (9.9857, 9.9857) // Error: 0.0143
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
With `@emasoft/svg-matrix` using 80-digit Decimal precision:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
Original coordinates: (10, 10)
|
|
307
|
+
After round-trip: (10.00000000000000000000000000000000000000,
|
|
308
|
+
9.999999999999999999999999999999999999999999999999999999999999999999999999999999998)
|
|
309
|
+
Round-trip error: X=0, Y=2e-79
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Improvement: 10^77 times better precision than JavaScript floats.**
|
|
313
|
+
|
|
314
|
+
### Parsing SVG Transforms
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
import { SVGFlatten } from '@emasoft/svg-matrix';
|
|
318
|
+
|
|
319
|
+
// Parse individual transforms
|
|
320
|
+
const m1 = SVGFlatten.parseTransformAttribute('translate(10, 20)');
|
|
321
|
+
const m2 = SVGFlatten.parseTransformAttribute('rotate(45)');
|
|
322
|
+
const m3 = SVGFlatten.parseTransformAttribute('scale(2, 0.5)');
|
|
323
|
+
const m4 = SVGFlatten.parseTransformAttribute('skewX(15)');
|
|
324
|
+
const m5 = SVGFlatten.parseTransformAttribute('matrix(0.866, 0.5, -0.5, 0.866, 0, 0)');
|
|
325
|
+
|
|
326
|
+
// Parse chained transforms
|
|
327
|
+
const combined = SVGFlatten.parseTransformAttribute('translate(50,50) rotate(45) scale(2)');
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Building CTM from Nested Elements
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
|
|
334
|
+
|
|
335
|
+
Decimal.set({ precision: 80 });
|
|
336
|
+
|
|
337
|
+
// Simulate a 6-level SVG hierarchy:
|
|
338
|
+
// <svg viewBox="..."> <!-- viewBox scaling -->
|
|
339
|
+
// <g transform="translate(-13.6,-10.2)"> <!-- g1 -->
|
|
340
|
+
// <g transform="translate(-1144.8,517.6)"> <!-- g2 -->
|
|
341
|
+
// <g transform="rotate(15)"> <!-- g3 -->
|
|
342
|
+
// <g transform="scale(1.2, 0.8)"> <!-- g4 -->
|
|
343
|
+
// <path transform="matrix(...)"/> <!-- element -->
|
|
344
|
+
|
|
345
|
+
const transformStack = [
|
|
346
|
+
'scale(1.5)', // viewBox scaling
|
|
347
|
+
'translate(-13.613145,-10.209854)', // g1
|
|
348
|
+
'translate(-1144.8563,517.64642)', // g2
|
|
349
|
+
'rotate(15)', // g3
|
|
350
|
+
'scale(1.2, 0.8)', // g4
|
|
351
|
+
'matrix(0.71577068,0,0,1.3970955,0,0)' // element
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
// Build combined CTM
|
|
355
|
+
const ctm = SVGFlatten.buildCTM(transformStack);
|
|
356
|
+
|
|
357
|
+
// Transform a point from local to viewport coordinates
|
|
358
|
+
const local = { x: new Decimal('10'), y: new Decimal('10') };
|
|
359
|
+
const viewport = SVGFlatten.applyToPoint(ctm, local.x, local.y);
|
|
360
|
+
|
|
361
|
+
// Transform back to local coordinates
|
|
362
|
+
const inverseCTM = ctm.inverse();
|
|
363
|
+
const recovered = SVGFlatten.applyToPoint(inverseCTM, viewport.x, viewport.y);
|
|
364
|
+
|
|
365
|
+
// Verify precision
|
|
366
|
+
const errorX = recovered.x.minus(local.x).abs();
|
|
367
|
+
const errorY = recovered.y.minus(local.y).abs();
|
|
368
|
+
console.log('Round-trip error X:', errorX.toString()); // 0
|
|
369
|
+
console.log('Round-trip error Y:', errorY.toString()); // ~2e-79
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Transforming Path Data
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
import { SVGFlatten } from '@emasoft/svg-matrix';
|
|
376
|
+
|
|
377
|
+
const pathD = 'M 100 100 L 200 100 L 200 200 L 100 200 Z';
|
|
378
|
+
const ctm = SVGFlatten.parseTransformAttribute('translate(50, 50) scale(2)');
|
|
379
|
+
const transformed = SVGFlatten.transformPathData(pathD, ctm);
|
|
380
|
+
|
|
381
|
+
console.log(transformed);
|
|
382
|
+
// M 250.000000 250.000000 L 450.000000 250.000000 L 450.000000 450.000000 L 250.000000 450.000000 Z
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Flattening All Transforms
|
|
386
|
+
|
|
387
|
+
To flatten an SVG (remove all transform attributes and apply them directly to coordinates):
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
import { SVGFlatten } from '@emasoft/svg-matrix';
|
|
391
|
+
|
|
392
|
+
// 1. For each element, collect transforms from root to element
|
|
393
|
+
// 2. Build CTM: const ctm = SVGFlatten.buildCTM(transformStack);
|
|
394
|
+
// 3. Transform path data: const newD = SVGFlatten.transformPathData(d, ctm);
|
|
395
|
+
// 4. Remove transform attribute, update path d attribute
|
|
396
|
+
// 5. Convert CTM back to SVG: SVGFlatten.toSVGMatrix(ctm, 6)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### CDN Usage
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<script type="module">
|
|
403
|
+
import { Decimal, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
404
|
+
|
|
405
|
+
Decimal.set({ precision: 80 });
|
|
406
|
+
|
|
407
|
+
const ctm = SVGFlatten.parseTransformAttribute('rotate(45, 100, 100)');
|
|
408
|
+
const point = SVGFlatten.applyToPoint(ctm, 50, 50);
|
|
409
|
+
console.log('Transformed:', point.x.toFixed(6), point.y.toFixed(6));
|
|
410
|
+
</script>
|
|
411
|
+
```
|
|
412
|
+
|
|
275
413
|
## License
|
|
276
414
|
|
|
277
415
|
MIT
|
package/package.json
CHANGED
package/samples/test.svg
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg id="pure_smill_no_javascript" version="1.1" x="0px" y="0px" width="1037.2269" height="1486.5736" viewBox="0 0 1037.227 2892.792" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
|
3
|
+
<defs id="defs82" />
|
|
4
|
+
<defs id="defs83" />
|
|
5
|
+
<g id="g37" transform="translate(-13.613145,-10.209854)">
|
|
6
|
+
<rect style="fill:#ffffff;stroke-width:16.1377" id="rect46" width="2018.3876" height="2892.792" x="-476.96716" y="10.209854" />
|
|
7
|
+
<g id="g1" transform="translate(-1144.8563,517.64642)">
|
|
8
|
+
<path id="rect1851" style="fill:none;stroke:#fe0000;stroke-width:14.5557;stroke-dasharray:87.3339, 14.5557;stroke-dashoffset:0;stroke-opacity:1" d="m 1318.7684,284.08405 c 307.4737,-77.43295 477.4693,-79.6744 779.5799,0 132.9776,50.26731 130.7718,132.64482 0,168.79753 -297.0165,89.1289 -485.8097,78.65553 -779.5799,0 -143.454,-38.49011 -151.4589,-124.05689 0,-168.79753 z" />
|
|
9
|
+
</g>
|
|
10
|
+
<g id="g6" transform="translate(13.613181,10.209854)">
|
|
11
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:94.4263px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#808000;stroke-width:16.0447" x="535.36902" y="880.21216" id="text2"><tspan id="tspan2" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:94.4263px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#808000;stroke-width:16.0447" x="535.36902" y="880.21216">100% PURE SMIL</tspan></text>
|
|
12
|
+
</g>
|
|
13
|
+
<g id="g7" transform="translate(13.613181,10.209854)">
|
|
14
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:51.2678px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#808000;stroke-width:8.7113" x="550.46289" y="939.87573" id="text3"><tspan id="tspan3" style="font-style:italic;font-variant:normal;font-weight:500;font-stretch:normal;font-size:51.2678px;font-family:Futura;-inkscape-font-specification:'Futura, Medium Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#808000;stroke-width:8.7113" x="550.46289" y="939.87573">No javascript or css needed!</tspan></text>
|
|
15
|
+
</g>
|
|
16
|
+
<text xml:space="preserve" style="font-style:italic;font-weight:600;font-size:85.6631px;font-family:'EB Garamond', serif;-inkscape-font-specification:'EB Garamond, serif, Semi-Bold Italic';writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="388.92661" y="1477.7195" id="text37" text-anchor="middle"><tspan id="tspan1" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:85.6631px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="388.92661" y="1477.7195">100% PURE SMIL</tspan></text>
|
|
17
|
+
<text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'.New York';-inkscape-font-specification:'.New York, Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="363.31311" y="1603.9994" id="text39" text-anchor="middle"><tspan id="tspan38" style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'.New York';-inkscape-font-specification:'.New York, Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="363.31311" y="1603.9994">No javascript or css needed!</tspan></text>
|
|
18
|
+
<text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="678.02161" y="1757.4683" id="text40" transform="scale(0.71577068,1.3970955)" text-anchor="middle"><tspan id="tspan39" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="678.02161" y="1757.4683">No javascript or css needed!</tspan></text>
|
|
19
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="404.79761" y="2284.7117" id="text41" text-anchor="middle"><tspan id="tspan40" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="404.79761" y="2284.7117">100% PURE SMIL</tspan></text>
|
|
20
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:85.6631px;font-family:'American Typewriter';-inkscape-font-specification:'American Typewriter, Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="341.14975" y="1755.0848" id="text42" text-anchor="middle"><tspan id="tspan41" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:85.6631px;font-family:'American Typewriter';-inkscape-font-specification:'American Typewriter, Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="341.14975" y="1755.0848">100% PURE SMIL</tspan></text>
|
|
21
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:85.6631px;font-family:'Comic Sans MS';-inkscape-font-specification:'Comic Sans MS, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="419.90002" y="1326.3029" id="text43" text-anchor="middle"><tspan id="tspan42" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:85.6631px;font-family:'Comic Sans MS';-inkscape-font-specification:'Comic Sans MS, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="419.90002" y="1326.3029">100% PURE SMIL</tspan></text>
|
|
22
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:'.SF Arabic Rounded';-inkscape-font-specification:'.SF Arabic Rounded, @opsz=80,wght=510';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'opsz' 80, 'wght' 510;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:1228.52;display:inline;fill:#000080;stroke-width:26.6984" x="1381.6643" y="1890.0889" id="text44" text-anchor="middle" transform="matrix(1.3203255,0,0,1.3203255,-1375.0262,-560.41607)"><tspan x="1381.6643" y="1890.0889" id="tspan27"><tspan style="text-align:end;text-anchor:end" id="tspan24">منتررف؛</tspan><tspan dx="212.47826" style="text-align:end;text-anchor:end" id="tspan25">٦٥</tspan><tspan dx="714.30957 -42.915234" style="text-align:end;text-anchor:end" id="tspan26">غفبممهت ورعبب</tspan><tspan y="1890.0889" id="tspan29"> </tspan></tspan><tspan x="1381.6643" y="1997.1678" id="tspan45"><tspan style="text-align:end;text-anchor:end" id="tspan30">٣٤٥٨</tspan><tspan dx="538.61511 0 0 0 0 25.01298 0 14.890675 -39.903576" style="text-align:end;text-anchor:end" id="tspan32">حخهـعنٌففڤك</tspan><tspan dx="296.26553" style="text-align:end;text-anchor:end" id="tspan33">٦</tspan><tspan dx="132.55197" style="text-align:end;text-anchor:end" id="tspan34">#</tspan><tspan dx="-47.641735" style="text-align:end;text-anchor:end" id="tspan35">٤</tspan><tspan dx="514.77338" style="text-align:end;text-anchor:end" id="tspan43">وحكهههـا؛جن</tspan></tspan></text>
|
|
23
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:67.9909px;font-family:'GohuFont 14 Nerd Font';-inkscape-font-specification:'GohuFont 14 Nerd Font, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.1905" x="604.13745" y="1147.0764" id="text45" text-anchor="middle"><tspan id="tspan44" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:67.9909px;font-family:'GohuFont 14 Nerd Font';-inkscape-font-specification:'GohuFont 14 Nerd Font, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.1905" x="604.13745" y="1147.0764">100% PURE SMIL تاكهعغفبقڤذطسببى٦٥!§|</tspan></text>
|
|
24
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:Phosphate;-inkscape-font-specification:'Phosphate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="536.83356" y="1861.4491" id="text47" transform="scale(0.71577068,1.3970955)" text-anchor="middle"><tspan id="tspan47" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:Phosphate;-inkscape-font-specification:'Phosphate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="536.83356" y="1861.4491">No javascript or css needed!</tspan></text>
|
|
25
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:'ProFont IIx Nerd Font';-inkscape-font-specification:'ProFont IIx Nerd Font, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="747.38995" y="525.20264" id="text48" transform="matrix(0.71322166,-0.06035344,0.11780243,1.3921201,0,0)" text-anchor="middle"><tspan id="tspan48" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:'ProFont IIx Nerd Font';-inkscape-font-specification:'ProFont IIx Nerd Font, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="747.39001" y="525.20264">No javascript or css needed!</tspan></text>
|
|
26
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Bodoni Ornaments';-inkscape-font-specification:'Bodoni Ornaments, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="472.37082" y="2838.6682" id="text49" text-anchor="middle"><tspan id="tspan49" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Bodoni Ornaments';-inkscape-font-specification:'Bodoni Ornaments, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="472.37082" y="2838.6682">No javascript or css needed!</tspan></text>
|
|
27
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="533.84888" y="530.25562" id="text50" text-anchor="middle"><tspan id="tspan50" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="533.84888" y="530.25562">No javascript or css needed!</tspan></text>
|
|
28
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Zapfino;-inkscape-font-specification:'Zapfino, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="584.37799" y="397.6167" id="text51" text-anchor="middle"><tspan id="tspan51" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Zapfino;-inkscape-font-specification:'Zapfino, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="584.37799" y="397.6167">No javascript or css needed!</tspan></text>
|
|
29
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Webdings;-inkscape-font-specification:'Webdings, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="449.30762" y="120.5267" id="text52" text-anchor="middle"><tspan id="tspan52" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Webdings;-inkscape-font-specification:'Webdings, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="449.30762" y="120.5267">No javascript or css needed!</tspan></text>
|
|
30
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Copperplate;-inkscape-font-specification:'Copperplate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="609.64258" y="246.02931" id="text53" text-anchor="middle"><tspan id="tspan53" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Copperplate;-inkscape-font-specification:'Copperplate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="609.64258" y="246.02931">No javascript or css needed!</tspan></text>
|
|
31
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="450.42053" y="2729.5894" id="text54" text-anchor="middle"><tspan id="tspan54" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="450.42053" y="2729.5894">100% غعخيقفٌع٩ْخنتلو،حجةضصشيذزظطققثًار،منتزقُڤخهـرزلڤ</tspan></text>
|
|
32
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:302.923px;font-family:'.SF Arabic Rounded';-inkscape-font-specification:'.SF Arabic Rounded, @opsz=80,wght=510';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'opsz' 80, 'wght' 510;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#808000;stroke-width:51.4717" x="-101.66634" y="477.18921" id="text4"><tspan id="tspan4" style="stroke-width:51.4717" x="-101.66634" y="477.18921">兛</tspan><tspan style="stroke-width:51.4717" x="-101.66634" y="864.82745" id="tspan5">瓩</tspan><tspan style="stroke-width:51.4717" x="-101.66634" y="1252.4657" id="tspan6">☞</tspan></text>
|
|
33
|
+
<path style="font-variation-settings:'opsz' 80, 'wght' 510;fill:none;stroke:#e40000;stroke-width:17.0854;stroke-dasharray:none;stroke-opacity:0.648148" d="m 1417.5151,346.27451 c -172.195,68.09973 -181.5149,120.68753 -227.2008,324.94298 -10.7474,48.04981 -1.555,122.34751 21.0816,166.07303 l 3.347,6.46516 c 40.3542,77.94932 296.6243,309.72422 275.2567,92.27938" id="path7" />
|
|
34
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;fill:#000080;stroke-width:19.8645" id="text7" text-anchor="middle" transform="matrix(0.89589712,0,0,0.89589712,151.55716,65.666192)"><textPath xlink:href="#path7" startOffset="50%" id="textPath7"><tspan id="tspan7" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645">No javascript or css needed!</tspan></textPath></text>
|
|
35
|
+
<text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:268.424px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:2.43157;stroke-dasharray:none;stroke-opacity:0.648148" x="-50.072258" y="1466.8563" id="text8" transform="scale(0.86535508,1.155595)"><tspan id="tspan8" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:268.424px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:2.43157;stroke-dasharray:none" x="-50.072266" y="1466.8563">Λοπ</tspan></text>
|
|
36
|
+
<text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:300;font-stretch:normal;font-size:92.6956px;font-family:Superclarendon;-inkscape-font-specification:'Superclarendon, Light Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:0.839701;stroke-dasharray:none;stroke-opacity:0.648148" x="-41.03904" y="1797.0054" id="text9"><tspan id="tspan9" style="font-style:italic;font-variant:normal;font-weight:300;font-stretch:normal;font-size:92.6956px;font-family:Superclarendon;-inkscape-font-specification:'Superclarendon, Light Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:0.839701;stroke-dasharray:none" x="-41.039047" y="1797.0054">lkœtrëå</tspan></text>
|
|
37
|
+
<text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-decoration:underline;text-decoration-line:underline;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="1786.3558" y="997.44519" id="text22" transform="scale(0.71577068,1.3970955)" text-anchor="middle" text-decoration="underline"><tspan id="tspan23" x="1785.8699" y="997.44519" style="text-align:end;text-anchor:end"><tspan id="tspan22" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#000080;stroke-width:21.6396" x="1785.8699" y="997.44519">No javascript</tspan></tspan><tspan x="1785.8699" y="997.44519" id="tspan28"><tspan id="tspan36" style="text-align:end;text-anchor:end"><tspan style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#000080;stroke-width:21.6396" x="1785.8699" y="997.44519" id="tspan31"> </tspan></tspan></tspan><tspan x="1785.8699" y="1084.2356" id="tspan37">no css needed!</tspan></text>
|
|
38
|
+
</g>
|
|
39
|
+
</svg>
|
package/src/index.js
CHANGED
|
@@ -27,5 +27,6 @@ import { Matrix } from './matrix.js';
|
|
|
27
27
|
import { Vector } from './vector.js';
|
|
28
28
|
import * as Transforms2D from './transforms2d.js';
|
|
29
29
|
import * as Transforms3D from './transforms3d.js';
|
|
30
|
+
import * as SVGFlatten from './svg-flatten.js';
|
|
30
31
|
|
|
31
|
-
export { Decimal, Matrix, Vector, Transforms2D, Transforms3D };
|
|
32
|
+
export { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Transform Flattening Utility
|
|
3
|
+
*
|
|
4
|
+
* Parses SVG transform attributes, builds CTM (Current Transform Matrix) for each element,
|
|
5
|
+
* and can flatten all transforms by applying them directly to coordinates.
|
|
6
|
+
*
|
|
7
|
+
* @module svg-flatten
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Decimal from 'decimal.js';
|
|
11
|
+
import { Matrix } from './matrix.js';
|
|
12
|
+
import * as Transforms2D from './transforms2d.js';
|
|
13
|
+
|
|
14
|
+
// Set high precision for all calculations
|
|
15
|
+
Decimal.set({ precision: 80 });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a single SVG transform function and return a 3x3 matrix.
|
|
19
|
+
* Supports: translate, scale, rotate, skewX, skewY, matrix
|
|
20
|
+
*
|
|
21
|
+
* @param {string} func - Transform function name
|
|
22
|
+
* @param {number[]} args - Numeric arguments
|
|
23
|
+
* @returns {Matrix} 3x3 transformation matrix
|
|
24
|
+
*/
|
|
25
|
+
export function parseTransformFunction(func, args) {
|
|
26
|
+
const D = x => new Decimal(x);
|
|
27
|
+
|
|
28
|
+
switch (func.toLowerCase()) {
|
|
29
|
+
case 'translate': {
|
|
30
|
+
const tx = args[0] || 0;
|
|
31
|
+
const ty = args[1] || 0;
|
|
32
|
+
return Transforms2D.translation(tx, ty);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case 'scale': {
|
|
36
|
+
const sx = args[0] || 1;
|
|
37
|
+
const sy = args[1] !== undefined ? args[1] : sx;
|
|
38
|
+
return Transforms2D.scale(sx, sy);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'rotate': {
|
|
42
|
+
// SVG rotate is in degrees, can have optional cx, cy
|
|
43
|
+
const angleDeg = args[0] || 0;
|
|
44
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
45
|
+
|
|
46
|
+
if (args.length >= 3) {
|
|
47
|
+
// rotate(angle, cx, cy) - rotation around point
|
|
48
|
+
const cx = args[1];
|
|
49
|
+
const cy = args[2];
|
|
50
|
+
return Transforms2D.rotateAroundPoint(angleRad, cx, cy);
|
|
51
|
+
}
|
|
52
|
+
return Transforms2D.rotate(angleRad);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'skewx': {
|
|
56
|
+
const angleDeg = args[0] || 0;
|
|
57
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
58
|
+
const tanVal = Decimal.tan(angleRad);
|
|
59
|
+
return Transforms2D.skew(tanVal, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'skewy': {
|
|
63
|
+
const angleDeg = args[0] || 0;
|
|
64
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
65
|
+
const tanVal = Decimal.tan(angleRad);
|
|
66
|
+
return Transforms2D.skew(0, tanVal);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'matrix': {
|
|
70
|
+
// matrix(a, b, c, d, e, f) -> | a c e |
|
|
71
|
+
// | b d f |
|
|
72
|
+
// | 0 0 1 |
|
|
73
|
+
const [a, b, c, d, e, f] = args.map(x => D(x || 0));
|
|
74
|
+
return Matrix.from([
|
|
75
|
+
[a, c, e],
|
|
76
|
+
[b, d, f],
|
|
77
|
+
[D(0), D(0), D(1)]
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
console.warn(`Unknown transform function: ${func}`);
|
|
83
|
+
return Matrix.identity(3);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse an SVG transform attribute string into a combined matrix.
|
|
89
|
+
* Handles multiple transforms: "translate(10,20) rotate(45) scale(2)"
|
|
90
|
+
*
|
|
91
|
+
* @param {string} transformStr - SVG transform attribute value
|
|
92
|
+
* @returns {Matrix} Combined 3x3 transformation matrix
|
|
93
|
+
*/
|
|
94
|
+
export function parseTransformAttribute(transformStr) {
|
|
95
|
+
if (!transformStr || transformStr.trim() === '') {
|
|
96
|
+
return Matrix.identity(3);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Regex to match transform functions: name(args)
|
|
100
|
+
const transformRegex = /(\w+)\s*\(([^)]*)\)/g;
|
|
101
|
+
let match;
|
|
102
|
+
let result = Matrix.identity(3);
|
|
103
|
+
|
|
104
|
+
while ((match = transformRegex.exec(transformStr)) !== null) {
|
|
105
|
+
const func = match[1];
|
|
106
|
+
const argsStr = match[2];
|
|
107
|
+
|
|
108
|
+
// Parse arguments (comma or space separated)
|
|
109
|
+
const args = argsStr
|
|
110
|
+
.split(/[\s,]+/)
|
|
111
|
+
.filter(s => s.length > 0)
|
|
112
|
+
.map(s => parseFloat(s));
|
|
113
|
+
|
|
114
|
+
const matrix = parseTransformFunction(func, args);
|
|
115
|
+
// Transforms are applied left-to-right in SVG, so we multiply in order
|
|
116
|
+
result = result.mul(matrix);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build the CTM (Current Transform Matrix) for an element by walking up its ancestry.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object[]} transformStack - Array of transform strings from root to element
|
|
126
|
+
* @returns {Matrix} Combined CTM as 3x3 matrix
|
|
127
|
+
*/
|
|
128
|
+
export function buildCTM(transformStack) {
|
|
129
|
+
let ctm = Matrix.identity(3);
|
|
130
|
+
|
|
131
|
+
for (const transformStr of transformStack) {
|
|
132
|
+
if (transformStr) {
|
|
133
|
+
const matrix = parseTransformAttribute(transformStr);
|
|
134
|
+
ctm = ctm.mul(matrix);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return ctm;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Apply a CTM to a 2D point.
|
|
143
|
+
*
|
|
144
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
145
|
+
* @param {number|string|Decimal} x - X coordinate
|
|
146
|
+
* @param {number|string|Decimal} y - Y coordinate
|
|
147
|
+
* @returns {{x: Decimal, y: Decimal}} Transformed coordinates
|
|
148
|
+
*/
|
|
149
|
+
export function applyToPoint(ctm, x, y) {
|
|
150
|
+
const [tx, ty] = Transforms2D.applyTransform(ctm, x, y);
|
|
151
|
+
return { x: tx, y: ty };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Convert a CTM back to SVG matrix() notation.
|
|
156
|
+
*
|
|
157
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
158
|
+
* @param {number} [precision=6] - Decimal places for output
|
|
159
|
+
* @returns {string} SVG matrix transform string
|
|
160
|
+
*/
|
|
161
|
+
export function toSVGMatrix(ctm, precision = 6) {
|
|
162
|
+
const a = ctm.data[0][0].toFixed(precision);
|
|
163
|
+
const b = ctm.data[1][0].toFixed(precision);
|
|
164
|
+
const c = ctm.data[0][1].toFixed(precision);
|
|
165
|
+
const d = ctm.data[1][1].toFixed(precision);
|
|
166
|
+
const e = ctm.data[0][2].toFixed(precision);
|
|
167
|
+
const f = ctm.data[1][2].toFixed(precision);
|
|
168
|
+
|
|
169
|
+
return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a matrix is effectively the identity matrix.
|
|
174
|
+
*
|
|
175
|
+
* @param {Matrix} m - 3x3 matrix to check
|
|
176
|
+
* @param {string} [tolerance='1e-10'] - Tolerance for comparison
|
|
177
|
+
* @returns {boolean} True if matrix is identity within tolerance
|
|
178
|
+
*/
|
|
179
|
+
export function isIdentity(m, tolerance = '1e-10') {
|
|
180
|
+
const identity = Matrix.identity(3);
|
|
181
|
+
return m.equals(identity, tolerance);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Transform path data coordinates using a CTM.
|
|
186
|
+
* Handles M, L, C, Q, S, T, A, Z commands (absolute only for now).
|
|
187
|
+
*
|
|
188
|
+
* @param {string} pathData - SVG path d attribute
|
|
189
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
190
|
+
* @returns {string} Transformed path data
|
|
191
|
+
*/
|
|
192
|
+
export function transformPathData(pathData, ctm) {
|
|
193
|
+
// Simple regex-based path parser for common commands
|
|
194
|
+
const result = [];
|
|
195
|
+
const commandRegex = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
|
|
196
|
+
let match;
|
|
197
|
+
|
|
198
|
+
while ((match = commandRegex.exec(pathData)) !== null) {
|
|
199
|
+
const cmd = match[1];
|
|
200
|
+
const argsStr = match[2].trim();
|
|
201
|
+
const args = argsStr
|
|
202
|
+
.split(/[\s,]+/)
|
|
203
|
+
.filter(s => s.length > 0)
|
|
204
|
+
.map(s => parseFloat(s));
|
|
205
|
+
|
|
206
|
+
const cmdUpper = cmd.toUpperCase();
|
|
207
|
+
|
|
208
|
+
switch (cmdUpper) {
|
|
209
|
+
case 'M':
|
|
210
|
+
case 'L':
|
|
211
|
+
case 'T': {
|
|
212
|
+
// Pairs of coordinates
|
|
213
|
+
const transformed = [];
|
|
214
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
215
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
216
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
217
|
+
}
|
|
218
|
+
result.push(cmd + ' ' + transformed.join(' '));
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'H': {
|
|
223
|
+
// Horizontal line - becomes L after transform
|
|
224
|
+
const { x, y } = applyToPoint(ctm, args[0], 0);
|
|
225
|
+
result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'V': {
|
|
230
|
+
// Vertical line - becomes L after transform
|
|
231
|
+
const { x, y } = applyToPoint(ctm, 0, args[0]);
|
|
232
|
+
result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'C': {
|
|
237
|
+
// Cubic bezier: 3 pairs of coordinates
|
|
238
|
+
const transformed = [];
|
|
239
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
240
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
241
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
242
|
+
}
|
|
243
|
+
result.push('C ' + transformed.join(' '));
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case 'S': {
|
|
248
|
+
// Smooth cubic: 2 pairs of coordinates
|
|
249
|
+
const transformed = [];
|
|
250
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
251
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
252
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
253
|
+
}
|
|
254
|
+
result.push('S ' + transformed.join(' '));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'Q': {
|
|
259
|
+
// Quadratic bezier: 2 pairs of coordinates
|
|
260
|
+
const transformed = [];
|
|
261
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
262
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
263
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
264
|
+
}
|
|
265
|
+
result.push('Q ' + transformed.join(' '));
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'A': {
|
|
270
|
+
// Arc: rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
|
271
|
+
// Transform end point, scale radii (approximate for non-uniform scale)
|
|
272
|
+
const transformed = [];
|
|
273
|
+
for (let i = 0; i < args.length; i += 7) {
|
|
274
|
+
const rx = args[i];
|
|
275
|
+
const ry = args[i + 1];
|
|
276
|
+
const rotation = args[i + 2];
|
|
277
|
+
const largeArc = args[i + 3];
|
|
278
|
+
const sweep = args[i + 4];
|
|
279
|
+
const x = args[i + 5];
|
|
280
|
+
const y = args[i + 6];
|
|
281
|
+
|
|
282
|
+
const { x: tx, y: ty } = applyToPoint(ctm, x, y);
|
|
283
|
+
|
|
284
|
+
// Scale radii approximately (doesn't handle rotation correctly for skew)
|
|
285
|
+
const scaleX = ctm.data[0][0].abs().plus(ctm.data[0][1].abs()).div(2);
|
|
286
|
+
const scaleY = ctm.data[1][0].abs().plus(ctm.data[1][1].abs()).div(2);
|
|
287
|
+
|
|
288
|
+
transformed.push(
|
|
289
|
+
(rx * scaleX.toNumber()).toFixed(6),
|
|
290
|
+
(ry * scaleY.toNumber()).toFixed(6),
|
|
291
|
+
rotation,
|
|
292
|
+
largeArc,
|
|
293
|
+
sweep,
|
|
294
|
+
tx.toFixed(6),
|
|
295
|
+
ty.toFixed(6)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
result.push('A ' + transformed.join(' '));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'Z': {
|
|
303
|
+
result.push('Z');
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
default:
|
|
308
|
+
// Keep unknown commands as-is
|
|
309
|
+
result.push(cmd + ' ' + argsStr);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result.join(' ');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Information about precision comparison between float and Decimal.
|
|
318
|
+
*/
|
|
319
|
+
export const PRECISION_INFO = {
|
|
320
|
+
floatError: 0.0143, // Typical error: 10 -> 9.9857
|
|
321
|
+
decimalPrecision: 80,
|
|
322
|
+
typicalRoundTripError: '2e-79',
|
|
323
|
+
improvementFactor: '1.43e+77'
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export default {
|
|
327
|
+
parseTransformFunction,
|
|
328
|
+
parseTransformAttribute,
|
|
329
|
+
buildCTM,
|
|
330
|
+
applyToPoint,
|
|
331
|
+
toSVGMatrix,
|
|
332
|
+
isIdentity,
|
|
333
|
+
transformPathData,
|
|
334
|
+
PRECISION_INFO
|
|
335
|
+
};
|