@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
package/README.md
CHANGED
|
@@ -1,413 +1,450 @@
|
|
|
1
1
|
# @emasoft/svg-matrix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
High-precision matrix, vector, and SVG transformation library for JavaScript. Built on decimal.js for 80-digit precision arithmetic.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- **SVG transform flattening**: parse transform attributes, build CTMs, flatten nested hierarchies
|
|
11
|
-
- **Linear algebra**: LU/QR decomposition, determinant, inverse, solve, matrix exponential
|
|
12
|
-
- **10^77 times better precision** than JavaScript floats for round-trip transforms
|
|
13
|
-
- Works in **Node.js** and **browsers** (via CDN)
|
|
7
|
+
**Linear Algebra**
|
|
8
|
+
- Matrix and Vector classes with full linear algebra operations
|
|
9
|
+
- LU/QR decomposition, determinant, inverse, solve, matrix exponential
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
**Affine Transforms**
|
|
12
|
+
- 2D (3x3 homogeneous): translation, rotation, scale, skew, reflection
|
|
13
|
+
- 3D (4x4 homogeneous): translation, rotation (X/Y/Z/arbitrary axis), scale
|
|
14
|
+
|
|
15
|
+
**SVG Processing**
|
|
16
|
+
- Transform attribute parsing and CTM (Current Transform Matrix) building
|
|
17
|
+
- viewBox, preserveAspectRatio, nested viewports, unit resolution
|
|
18
|
+
- Shape-to-path conversion (circle, ellipse, rect, line, polygon, polyline)
|
|
19
|
+
- Path parsing, normalization (absolute, cubics), transformation
|
|
20
|
+
|
|
21
|
+
**SVG Element Resolution**
|
|
22
|
+
- ClipPath flattening to polygons
|
|
23
|
+
- Mask resolution (luminance and alpha)
|
|
24
|
+
- Pattern tiling expansion
|
|
25
|
+
- Use/symbol inlining with proper transforms
|
|
26
|
+
- Marker positioning and orientation
|
|
27
|
+
|
|
28
|
+
**Advanced**
|
|
29
|
+
- Polygon boolean operations (intersection, union, difference, convex hull)
|
|
30
|
+
- SVG 2.0 mesh gradient parsing and rasterization
|
|
31
|
+
- Text-to-path conversion with font support
|
|
32
|
+
- Browser verification against Chrome's native W3C SVG2 implementation
|
|
33
|
+
|
|
34
|
+
## Precision
|
|
35
|
+
|
|
36
|
+
| Scenario | Float Error | This Library | Improvement |
|
|
37
|
+
|----------|-------------|--------------|-------------|
|
|
38
|
+
| GIS/CAD coordinates (1e6+ scale) | 1.69e-7 | 0 | 10^93x |
|
|
39
|
+
| 6-level SVG hierarchy | 1.14e-13 | 1e-77 | 10^64x |
|
|
40
|
+
| 1000 round-trip transforms | 5.41e-14 | 0 | 10^86x |
|
|
16
41
|
|
|
17
|
-
|
|
42
|
+
**When precision matters:** GIS, CAD, scientific visualization, deep transform hierarchies, accumulated operations.
|
|
43
|
+
|
|
44
|
+
**When floats suffice:** Simple transforms, small coordinates, visual applications where sub-pixel errors are imperceptible.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node test/benchmark-precision.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
18
51
|
|
|
19
52
|
```bash
|
|
20
53
|
npm install @emasoft/svg-matrix
|
|
21
54
|
```
|
|
22
55
|
|
|
23
56
|
```js
|
|
24
|
-
import {
|
|
57
|
+
import { Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from '@emasoft/svg-matrix';
|
|
25
58
|
```
|
|
26
59
|
|
|
27
|
-
### CDN
|
|
28
|
-
|
|
29
|
-
Using esm.sh (recommended - auto-resolves dependencies):
|
|
60
|
+
### CDN
|
|
30
61
|
|
|
31
62
|
```html
|
|
32
63
|
<script type="module">
|
|
33
|
-
import {
|
|
64
|
+
import { Matrix, Vector, Transforms2D } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
34
67
|
|
|
35
|
-
|
|
68
|
+
## Quick Start
|
|
36
69
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.mul(Transforms2D.rotate(Math.PI / 4))
|
|
40
|
-
.mul(Transforms2D.scale(1.5));
|
|
70
|
+
```js
|
|
71
|
+
import { Decimal, Matrix, Vector, Transforms2D, SVGFlatten } from '@emasoft/svg-matrix';
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
const [x, y] = Transforms2D.applyTransform(M, 1, 0);
|
|
44
|
-
console.log('Transformed:', x.toString(), y.toString());
|
|
45
|
-
</script>
|
|
46
|
-
```
|
|
73
|
+
Decimal.set({ precision: 80 });
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
// Compose transforms (right-to-left: scale first, then rotate, then translate)
|
|
76
|
+
const M = Transforms2D.translation(10, 20)
|
|
77
|
+
.mul(Transforms2D.rotate(Math.PI / 4))
|
|
78
|
+
.mul(Transforms2D.scale(2));
|
|
49
79
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
// Apply to point
|
|
81
|
+
const [x, y] = Transforms2D.applyTransform(M, 1, 0);
|
|
82
|
+
|
|
83
|
+
// Round-trip with inverse
|
|
84
|
+
const [xBack, yBack] = Transforms2D.applyTransform(M.inverse(), x, y);
|
|
54
85
|
```
|
|
55
86
|
|
|
56
|
-
|
|
87
|
+
---
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
<script type="importmap">
|
|
60
|
-
{
|
|
61
|
-
"imports": {
|
|
62
|
-
"@emasoft/svg-matrix": "https://esm.sh/@emasoft/svg-matrix"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
</script>
|
|
66
|
-
<script type="module">
|
|
67
|
-
import { Matrix, Vector, Transforms2D } from '@emasoft/svg-matrix';
|
|
68
|
-
</script>
|
|
69
|
-
```
|
|
89
|
+
## API Reference
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
### Linear Algebra
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
#### Vector
|
|
74
94
|
|
|
75
95
|
```js
|
|
76
|
-
import { Vector
|
|
77
|
-
|
|
78
|
-
Decimal.set({ precision: 80 });
|
|
96
|
+
import { Vector } from '@emasoft/svg-matrix';
|
|
79
97
|
|
|
80
98
|
const v = Vector.from([1, 2, 3]);
|
|
81
99
|
const w = Vector.from([4, 5, 6]);
|
|
82
100
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
const proj = v.projectOnto(w);
|
|
96
|
-
console.log('Projection:', proj.toNumberArray());
|
|
101
|
+
v.add(w) // Element-wise addition
|
|
102
|
+
v.sub(w) // Element-wise subtraction
|
|
103
|
+
v.scale(2) // Scalar multiplication
|
|
104
|
+
v.dot(w) // Dot product → Decimal
|
|
105
|
+
v.cross(w) // Cross product (3D)
|
|
106
|
+
v.norm() // Euclidean length
|
|
107
|
+
v.normalize() // Unit vector
|
|
108
|
+
v.angleBetween(w) // Angle in radians
|
|
109
|
+
v.projectOnto(w) // Vector projection
|
|
110
|
+
v.orthogonal() // Perpendicular vector
|
|
111
|
+
v.distance(w) // Euclidean distance
|
|
112
|
+
v.toNumberArray() // [1, 2, 3]
|
|
97
113
|
```
|
|
98
114
|
|
|
99
|
-
|
|
115
|
+
#### Matrix
|
|
100
116
|
|
|
101
117
|
```js
|
|
102
118
|
import { Matrix } from '@emasoft/svg-matrix';
|
|
103
119
|
|
|
104
120
|
const A = Matrix.from([[1, 2], [3, 4]]);
|
|
105
|
-
const
|
|
121
|
+
const I = Matrix.identity(3);
|
|
122
|
+
const Z = Matrix.zeros(2, 3);
|
|
123
|
+
|
|
124
|
+
A.add(B) // Element-wise addition
|
|
125
|
+
A.sub(B) // Element-wise subtraction
|
|
126
|
+
A.mul(B) // Matrix multiplication
|
|
127
|
+
A.transpose() // Transpose
|
|
128
|
+
A.trace() // Sum of diagonal
|
|
129
|
+
A.determinant() // Determinant
|
|
130
|
+
A.inverse() // Matrix inverse
|
|
131
|
+
A.solve([1, 1]) // Solve Ax = b
|
|
132
|
+
A.lu() // { L, U, P } decomposition
|
|
133
|
+
A.qr() // { Q, R } decomposition
|
|
134
|
+
A.exp() // Matrix exponential
|
|
135
|
+
A.applyToVector(v) // Matrix-vector product
|
|
136
|
+
```
|
|
106
137
|
|
|
107
|
-
|
|
108
|
-
console.log('Multiply:', A.mul(B).toNumberArray());
|
|
109
|
-
console.log('Transpose:', A.transpose().toNumberArray());
|
|
110
|
-
console.log('Determinant:', A.determinant().toString()); // -2
|
|
111
|
-
console.log('Trace:', A.trace().toString()); // 5
|
|
138
|
+
### Transforms
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
const inv = A.inverse();
|
|
115
|
-
console.log('Inverse:', inv.toNumberArray());
|
|
140
|
+
#### 2D (3x3 matrices)
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log('Solution:', x.toNumberArray());
|
|
142
|
+
```js
|
|
143
|
+
import { Transforms2D } from '@emasoft/svg-matrix';
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
Transforms2D.translation(tx, ty)
|
|
146
|
+
Transforms2D.scale(sx, sy) // sy defaults to sx
|
|
147
|
+
Transforms2D.rotate(theta) // radians
|
|
148
|
+
Transforms2D.rotateAroundPoint(theta, px, py)
|
|
149
|
+
Transforms2D.skew(ax, ay)
|
|
150
|
+
Transforms2D.stretchAlongAxis(ux, uy, k)
|
|
151
|
+
Transforms2D.reflectX() // flip across X axis
|
|
152
|
+
Transforms2D.reflectY() // flip across Y axis
|
|
153
|
+
Transforms2D.reflectOrigin()
|
|
124
154
|
|
|
125
|
-
//
|
|
126
|
-
const
|
|
155
|
+
// Apply to point
|
|
156
|
+
const [x, y] = Transforms2D.applyTransform(matrix, px, py);
|
|
127
157
|
```
|
|
128
158
|
|
|
129
|
-
|
|
159
|
+
#### 3D (4x4 matrices)
|
|
130
160
|
|
|
131
161
|
```js
|
|
132
|
-
import {
|
|
162
|
+
import { Transforms3D } from '@emasoft/svg-matrix';
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
Transforms3D.translation(tx, ty, tz)
|
|
165
|
+
Transforms3D.scale(sx, sy, sz)
|
|
166
|
+
Transforms3D.rotateX(theta)
|
|
167
|
+
Transforms3D.rotateY(theta)
|
|
168
|
+
Transforms3D.rotateZ(theta)
|
|
169
|
+
Transforms3D.rotateAroundAxis(ux, uy, uz, theta)
|
|
170
|
+
Transforms3D.rotateAroundPoint(ux, uy, uz, theta, px, py, pz)
|
|
171
|
+
Transforms3D.reflectXY() // flip Z
|
|
172
|
+
Transforms3D.reflectXZ() // flip Y
|
|
173
|
+
Transforms3D.reflectYZ() // flip X
|
|
174
|
+
|
|
175
|
+
const [x, y, z] = Transforms3D.applyTransform(matrix, px, py, pz);
|
|
176
|
+
```
|
|
139
177
|
|
|
140
|
-
|
|
141
|
-
const Rp = Transforms2D.rotateAroundPoint(Math.PI / 2, 5, 5);
|
|
178
|
+
### SVG Processing
|
|
142
179
|
|
|
143
|
-
|
|
144
|
-
const Sk = Transforms2D.skew(0.5, 0);
|
|
145
|
-
const St = Transforms2D.stretchAlongAxis(1, 0, 2); // stretch 2x along X
|
|
180
|
+
#### SVGFlatten - Transform Parsing & CTM
|
|
146
181
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
```js
|
|
183
|
+
import { SVGFlatten } from '@emasoft/svg-matrix';
|
|
184
|
+
|
|
185
|
+
// Parse transform attributes
|
|
186
|
+
const m = SVGFlatten.parseTransformAttribute('translate(50,50) rotate(45) scale(2)');
|
|
151
187
|
|
|
152
|
-
//
|
|
153
|
-
const
|
|
188
|
+
// Build CTM from transform stack
|
|
189
|
+
const ctm = SVGFlatten.buildCTM([
|
|
190
|
+
'scale(1.5)',
|
|
191
|
+
'translate(-13.6, -10.2)',
|
|
192
|
+
'rotate(15)',
|
|
193
|
+
'matrix(0.716, 0, 0, 1.397, 0, 0)'
|
|
194
|
+
]);
|
|
154
195
|
|
|
155
196
|
// Apply to point
|
|
156
|
-
const
|
|
197
|
+
const { x, y } = SVGFlatten.applyToPoint(ctm, 10, 10);
|
|
198
|
+
|
|
199
|
+
// Transform path data
|
|
200
|
+
const transformed = SVGFlatten.transformPathData('M 100 100 L 200 200', ctm);
|
|
201
|
+
|
|
202
|
+
// viewBox handling
|
|
203
|
+
const viewBox = SVGFlatten.parseViewBox('0 0 100 100');
|
|
204
|
+
const par = SVGFlatten.parsePreserveAspectRatio('xMidYMid meet');
|
|
205
|
+
const vbTransform = SVGFlatten.computeViewBoxTransform(viewBox, 800, 600, par);
|
|
206
|
+
|
|
207
|
+
// Full CTM with viewBox + nested transforms
|
|
208
|
+
const fullCtm = SVGFlatten.buildFullCTM([
|
|
209
|
+
{ type: 'svg', width: 800, height: 600, viewBox: '0 0 400 300' },
|
|
210
|
+
{ type: 'g', transform: 'translate(50, 50)' },
|
|
211
|
+
{ type: 'g', transform: 'rotate(45)' },
|
|
212
|
+
{ type: 'element', transform: 'scale(2)' }
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
// Unit resolution (px, %, em, pt, in, cm, mm, pc)
|
|
216
|
+
SVGFlatten.resolveLength('50%', 800); // → 400
|
|
217
|
+
SVGFlatten.resolveLength('1in', 800); // → 96
|
|
218
|
+
|
|
219
|
+
// objectBoundingBox transform
|
|
220
|
+
const bboxTransform = SVGFlatten.objectBoundingBoxTransform(100, 50, 200, 100);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### GeometryToPath - Shape Conversion
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
225
|
+
```js
|
|
226
|
+
import { GeometryToPath } from '@emasoft/svg-matrix';
|
|
227
|
+
|
|
228
|
+
// Shape to path
|
|
229
|
+
GeometryToPath.circleToPathData(cx, cy, r, precision)
|
|
230
|
+
GeometryToPath.ellipseToPathData(cx, cy, rx, ry, precision)
|
|
231
|
+
GeometryToPath.rectToPathData(x, y, w, h, rx, ry, useArcs, precision)
|
|
232
|
+
GeometryToPath.lineToPathData(x1, y1, x2, y2, precision)
|
|
233
|
+
GeometryToPath.polylineToPathData(points, precision)
|
|
234
|
+
GeometryToPath.polygonToPathData(points, precision)
|
|
235
|
+
GeometryToPath.convertElementToPath(element, precision)
|
|
236
|
+
|
|
237
|
+
// Path manipulation
|
|
238
|
+
GeometryToPath.parsePathData(pathData) // → [{command, args}]
|
|
239
|
+
GeometryToPath.pathArrayToString(commands) // → path string
|
|
240
|
+
GeometryToPath.pathToAbsolute(pathData) // relative → absolute
|
|
241
|
+
GeometryToPath.pathToCubics(pathData) // all → cubic Beziers
|
|
242
|
+
GeometryToPath.transformPathData(pathData, matrix, precision)
|
|
243
|
+
|
|
244
|
+
// Bezier kappa constant: 4*(sqrt(2)-1)/3
|
|
245
|
+
GeometryToPath.getKappa()
|
|
161
246
|
```
|
|
162
247
|
|
|
163
|
-
|
|
248
|
+
#### BrowserVerify - Chrome Verification
|
|
164
249
|
|
|
165
250
|
```js
|
|
166
|
-
import {
|
|
251
|
+
import { BrowserVerify } from '@emasoft/svg-matrix';
|
|
252
|
+
|
|
253
|
+
// One-off verification
|
|
254
|
+
await BrowserVerify.verifyViewBox(800, 600, '0 0 400 300', 'xMidYMid meet');
|
|
255
|
+
await BrowserVerify.verifyTransform('rotate(45) translate(100, 50) scale(2)');
|
|
256
|
+
|
|
257
|
+
// Session-based verification
|
|
258
|
+
const verifier = new BrowserVerify.BrowserVerifier();
|
|
259
|
+
await verifier.init({ headless: true });
|
|
260
|
+
await verifier.verifyViewBoxTransform(800, 600, '0 0 100 100');
|
|
261
|
+
await verifier.verifyMatrix(ctm, { width: 100, height: 100, transform: '...' });
|
|
262
|
+
await verifier.verifyPointTransform(ctm, 10, 20, config);
|
|
263
|
+
await verifier.close();
|
|
264
|
+
|
|
265
|
+
// Standard test suite (28 tests including W3C issue #215 cases)
|
|
266
|
+
await BrowserVerify.runStandardTests({ verbose: true });
|
|
267
|
+
```
|
|
167
268
|
|
|
168
|
-
|
|
169
|
-
const T = Transforms3D.translation(1, 2, 3);
|
|
170
|
-
const S = Transforms3D.scale(2, 2, 2);
|
|
269
|
+
### Polygon Operations
|
|
171
270
|
|
|
172
|
-
|
|
173
|
-
const Rx = Transforms3D.rotateX(Math.PI / 2);
|
|
174
|
-
const Ry = Transforms3D.rotateY(Math.PI / 4);
|
|
175
|
-
const Rz = Transforms3D.rotateZ(Math.PI / 6);
|
|
271
|
+
#### PolygonClip
|
|
176
272
|
|
|
177
|
-
|
|
178
|
-
|
|
273
|
+
```js
|
|
274
|
+
import { PolygonClip } from '@emasoft/svg-matrix';
|
|
179
275
|
|
|
180
|
-
|
|
181
|
-
|
|
276
|
+
const square = [
|
|
277
|
+
PolygonClip.point(0, 0),
|
|
278
|
+
PolygonClip.point(2, 0),
|
|
279
|
+
PolygonClip.point(2, 2),
|
|
280
|
+
PolygonClip.point(0, 2)
|
|
281
|
+
];
|
|
182
282
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const RfO = Transforms3D.reflectOrigin(); // flip all
|
|
283
|
+
// Boolean operations
|
|
284
|
+
PolygonClip.polygonIntersection(poly1, poly2)
|
|
285
|
+
PolygonClip.polygonUnion(poly1, poly2)
|
|
286
|
+
PolygonClip.polygonDifference(poly1, poly2)
|
|
188
287
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
288
|
+
// Properties
|
|
289
|
+
PolygonClip.polygonArea(polygon)
|
|
290
|
+
PolygonClip.isCounterClockwise(polygon)
|
|
291
|
+
PolygonClip.isConvex(polygon)
|
|
292
|
+
PolygonClip.pointInPolygon(point, polygon) // 1=inside, 0=boundary, -1=outside
|
|
293
|
+
|
|
294
|
+
// Convex hull
|
|
295
|
+
PolygonClip.convexHull(points)
|
|
296
|
+
|
|
297
|
+
// Bounding box
|
|
298
|
+
PolygonClip.boundingBox(polygon) // {minX, minY, maxX, maxY}
|
|
299
|
+
PolygonClip.bboxIntersects(bbox1, bbox2)
|
|
192
300
|
```
|
|
193
301
|
|
|
194
|
-
|
|
302
|
+
#### ClipPathResolver
|
|
195
303
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| Method | Description |
|
|
199
|
-
|--------|-------------|
|
|
200
|
-
| `Vector.from(arr)` | Create vector from array |
|
|
201
|
-
| `add(v)` | Element-wise addition |
|
|
202
|
-
| `sub(v)` | Element-wise subtraction |
|
|
203
|
-
| `scale(s)` | Scalar multiplication |
|
|
204
|
-
| `negate()` | Negate all components |
|
|
205
|
-
| `dot(v)` | Dot product |
|
|
206
|
-
| `cross(v)` | Cross product (3D only) |
|
|
207
|
-
| `outer(v)` | Outer product (returns 2D array) |
|
|
208
|
-
| `norm()` | Euclidean length |
|
|
209
|
-
| `normalize()` | Unit vector |
|
|
210
|
-
| `angleBetween(v)` | Angle in radians |
|
|
211
|
-
| `projectOnto(v)` | Vector projection |
|
|
212
|
-
| `orthogonal()` | Perpendicular vector |
|
|
213
|
-
| `distance(v)` | Euclidean distance |
|
|
214
|
-
| `equals(v, tol?)` | Equality check with optional tolerance |
|
|
215
|
-
| `toArray()` | Get Decimal array |
|
|
216
|
-
| `toNumberArray()` | Get number array |
|
|
217
|
-
| `toStringArray()` | Get string array |
|
|
218
|
-
|
|
219
|
-
### Matrix
|
|
220
|
-
|
|
221
|
-
| Method | Description |
|
|
222
|
-
|--------|-------------|
|
|
223
|
-
| `Matrix.from(arr)` | Create from 2D array |
|
|
224
|
-
| `Matrix.zeros(r, c)` | Zero matrix |
|
|
225
|
-
| `Matrix.identity(n)` | Identity matrix |
|
|
226
|
-
| `add(M)` | Element-wise addition |
|
|
227
|
-
| `sub(M)` | Element-wise subtraction |
|
|
228
|
-
| `mul(M)` | Matrix multiplication |
|
|
229
|
-
| `div(s)` | Scalar division |
|
|
230
|
-
| `negate()` | Negate all elements |
|
|
231
|
-
| `transpose()` | Transpose |
|
|
232
|
-
| `trace()` | Sum of diagonal |
|
|
233
|
-
| `determinant()` | Determinant |
|
|
234
|
-
| `inverse()` | Matrix inverse |
|
|
235
|
-
| `solve(b)` | Solve Ax = b |
|
|
236
|
-
| `lu()` | LU decomposition |
|
|
237
|
-
| `qr()` | QR decomposition |
|
|
238
|
-
| `exp()` | Matrix exponential |
|
|
239
|
-
| `applyToVector(v)` | Matrix-vector product |
|
|
240
|
-
| `equals(M, tol?)` | Equality check |
|
|
241
|
-
| `isSquare()` | Check if square |
|
|
242
|
-
| `toNumberArray()` | Get number 2D array |
|
|
243
|
-
| `toArrayOfStrings()` | Get string 2D array |
|
|
244
|
-
|
|
245
|
-
### Transforms2D
|
|
246
|
-
|
|
247
|
-
| Function | Description |
|
|
248
|
-
|----------|-------------|
|
|
249
|
-
| `translation(tx, ty)` | Translation matrix |
|
|
250
|
-
| `scale(sx, sy?)` | Scale matrix (uniform if sy omitted) |
|
|
251
|
-
| `rotate(theta)` | Rotation matrix (radians) |
|
|
252
|
-
| `rotateAroundPoint(theta, px, py)` | Rotation around point |
|
|
253
|
-
| `skew(ax, ay)` | Skew/shear matrix |
|
|
254
|
-
| `stretchAlongAxis(ux, uy, k)` | Stretch along direction |
|
|
255
|
-
| `reflectX()` | Reflect across X axis |
|
|
256
|
-
| `reflectY()` | Reflect across Y axis |
|
|
257
|
-
| `reflectOrigin()` | Reflect through origin |
|
|
258
|
-
| `applyTransform(M, x, y)` | Apply matrix to point |
|
|
259
|
-
|
|
260
|
-
### Transforms3D
|
|
261
|
-
|
|
262
|
-
| Function | Description |
|
|
263
|
-
|----------|-------------|
|
|
264
|
-
| `translation(tx, ty, tz)` | Translation matrix |
|
|
265
|
-
| `scale(sx, sy?, sz?)` | Scale matrix |
|
|
266
|
-
| `rotateX(theta)` | Rotation around X axis |
|
|
267
|
-
| `rotateY(theta)` | Rotation around Y axis |
|
|
268
|
-
| `rotateZ(theta)` | Rotation around Z axis |
|
|
269
|
-
| `rotateAroundAxis(ux, uy, uz, theta)` | Rotation around arbitrary axis |
|
|
270
|
-
| `rotateAroundPoint(ux, uy, uz, theta, px, py, pz)` | Rotation around point |
|
|
271
|
-
| `reflectXY()` | Reflect across XY plane |
|
|
272
|
-
| `reflectXZ()` | Reflect across XZ plane |
|
|
273
|
-
| `reflectYZ()` | Reflect across YZ plane |
|
|
274
|
-
| `reflectOrigin()` | Reflect through origin |
|
|
275
|
-
| `applyTransform(M, x, y, z)` | Apply matrix to point |
|
|
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:
|
|
304
|
+
```js
|
|
305
|
+
import { ClipPathResolver } from '@emasoft/svg-matrix';
|
|
297
306
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```
|
|
307
|
+
// Parse and resolve clipPath
|
|
308
|
+
const clipData = ClipPathResolver.parseClipPathElement(element);
|
|
309
|
+
const clipPolygon = ClipPathResolver.resolveClipPath(clipData, targetBBox);
|
|
302
310
|
|
|
303
|
-
|
|
311
|
+
// Shape to polygon
|
|
312
|
+
ClipPathResolver.shapeToPolygon({ type: 'circle', cx: 100, cy: 100, r: 50 }, { samples: 32 })
|
|
313
|
+
ClipPathResolver.pathToPolygon(pathData, { samples: 20 })
|
|
314
|
+
ClipPathResolver.polygonToPathData(polygon)
|
|
304
315
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
After round-trip: (10.00000000000000000000000000000000000000,
|
|
308
|
-
9.999999999999999999999999999999999999999999999999999999999999999999999999999999998)
|
|
309
|
-
Round-trip error: X=0, Y=2e-79
|
|
316
|
+
// Apply clipPath to element
|
|
317
|
+
ClipPathResolver.applyClipPath(elementData, clipPathData, targetBBox)
|
|
310
318
|
```
|
|
311
319
|
|
|
312
|
-
|
|
320
|
+
### SVG Element Resolution
|
|
313
321
|
|
|
314
|
-
|
|
322
|
+
#### UseSymbolResolver
|
|
315
323
|
|
|
316
324
|
```js
|
|
317
|
-
import {
|
|
325
|
+
import { UseSymbolResolver } from '@emasoft/svg-matrix';
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
|
|
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)');
|
|
327
|
+
const useData = UseSymbolResolver.parseUseElement(useElement);
|
|
328
|
+
const symbolData = UseSymbolResolver.parseSymbolElement(symbolElement);
|
|
329
|
+
const resolved = UseSymbolResolver.resolveUse(useData, svgDocument);
|
|
330
|
+
const flattened = UseSymbolResolver.flattenResolvedUse(resolved);
|
|
331
|
+
UseSymbolResolver.resolveAllUses(svgDocument)
|
|
328
332
|
```
|
|
329
333
|
|
|
330
|
-
|
|
334
|
+
#### MarkerResolver
|
|
331
335
|
|
|
332
336
|
```js
|
|
333
|
-
import {
|
|
337
|
+
import { MarkerResolver } from '@emasoft/svg-matrix';
|
|
334
338
|
|
|
335
|
-
|
|
339
|
+
const markerData = MarkerResolver.parseMarkerElement(markerElement);
|
|
340
|
+
const vertices = MarkerResolver.getPathVertices(pathData);
|
|
341
|
+
const transform = MarkerResolver.getMarkerTransform(markerData, vertex, angle, strokeWidth);
|
|
342
|
+
const instances = MarkerResolver.resolveMarkers(pathD, { 'marker-start': m1, 'marker-end': m2, strokeWidth: 2 });
|
|
343
|
+
MarkerResolver.markersToPathData(instances)
|
|
344
|
+
```
|
|
336
345
|
|
|
337
|
-
|
|
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
|
-
];
|
|
346
|
+
#### PatternResolver
|
|
353
347
|
|
|
354
|
-
|
|
355
|
-
|
|
348
|
+
```js
|
|
349
|
+
import { PatternResolver } from '@emasoft/svg-matrix';
|
|
350
|
+
|
|
351
|
+
const patternData = PatternResolver.parsePatternElement(patternElement);
|
|
352
|
+
const tiles = PatternResolver.resolvePattern(patternData, targetBBox);
|
|
353
|
+
PatternResolver.applyPattern(elementData, patternData, targetBBox)
|
|
354
|
+
PatternResolver.patternToClipPath(patternData, targetBBox)
|
|
355
|
+
PatternResolver.patternToPathData(patternData, targetBBox)
|
|
356
|
+
```
|
|
356
357
|
|
|
357
|
-
|
|
358
|
-
const local = { x: new Decimal('10'), y: new Decimal('10') };
|
|
359
|
-
const viewport = SVGFlatten.applyToPoint(ctm, local.x, local.y);
|
|
358
|
+
#### MaskResolver
|
|
360
359
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const recovered = SVGFlatten.applyToPoint(inverseCTM, viewport.x, viewport.y);
|
|
360
|
+
```js
|
|
361
|
+
import { MaskResolver } from '@emasoft/svg-matrix';
|
|
364
362
|
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
console.log('Round-trip error Y:', errorY.toString()); // ~2e-79
|
|
363
|
+
const maskData = MaskResolver.parseMaskElement(maskElement);
|
|
364
|
+
const maskPolygon = MaskResolver.resolveMask(maskData, targetBBox);
|
|
365
|
+
MaskResolver.applyMask(elementPolygon, maskData, targetBBox)
|
|
366
|
+
MaskResolver.colorToLuminance({ r, g, b }) // sRGB luminance
|
|
370
367
|
```
|
|
371
368
|
|
|
372
|
-
###
|
|
369
|
+
### Advanced Features
|
|
370
|
+
|
|
371
|
+
#### MeshGradient (SVG 2.0)
|
|
373
372
|
|
|
374
373
|
```js
|
|
375
|
-
import {
|
|
374
|
+
import { MeshGradient } from '@emasoft/svg-matrix';
|
|
375
|
+
|
|
376
|
+
// Parse mesh gradient
|
|
377
|
+
const meshDef = MeshGradient.parseMeshGradientElement(element);
|
|
378
|
+
const meshData = MeshGradient.parseMeshGradient(meshDef);
|
|
379
|
+
|
|
380
|
+
// Coons patch evaluation
|
|
381
|
+
const patch = new MeshGradient.CoonsPatch(topEdge, rightEdge, bottomEdge, leftEdge, cornerColors);
|
|
382
|
+
const { point, color } = patch.evaluate(u, v);
|
|
376
383
|
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const transformed = SVGFlatten.transformPathData(pathD, ctm);
|
|
384
|
+
// Rasterize to ImageData
|
|
385
|
+
const imageData = MeshGradient.rasterizeMeshGradient(meshData, width, height);
|
|
380
386
|
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
// Convert to polygons for vector export
|
|
388
|
+
const polygons = MeshGradient.meshGradientToPolygons(meshData, { subdivisions: 16 });
|
|
389
|
+
|
|
390
|
+
// Clip and export
|
|
391
|
+
const clipped = MeshGradient.clipMeshGradient(meshData, clipPolygon, { subdivisions: 32 });
|
|
392
|
+
const svgPaths = MeshGradient.clippedMeshToSVG(clipped);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### TextToPath
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
import { TextToPath } from '@emasoft/svg-matrix';
|
|
399
|
+
import opentype from 'opentype.js';
|
|
400
|
+
|
|
401
|
+
const font = await opentype.load('font.ttf');
|
|
402
|
+
|
|
403
|
+
// Convert text to path
|
|
404
|
+
const pathData = TextToPath.textToPath("Hello", {
|
|
405
|
+
x: 100, y: 100,
|
|
406
|
+
fontSize: 24,
|
|
407
|
+
font: font,
|
|
408
|
+
textAnchor: TextToPath.TextAnchor.MIDDLE,
|
|
409
|
+
dominantBaseline: TextToPath.DominantBaseline.MIDDLE
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Parse text element
|
|
413
|
+
const textData = TextToPath.parseTextElement(textElement);
|
|
414
|
+
const result = TextToPath.textElementToPath(textData, { font });
|
|
415
|
+
|
|
416
|
+
// Measure text
|
|
417
|
+
const metrics = TextToPath.measureText("Hello", { fontSize: "20px" }, font);
|
|
418
|
+
const bbox = TextToPath.getTextBBox(textData);
|
|
383
419
|
```
|
|
384
420
|
|
|
385
|
-
###
|
|
421
|
+
### Convenience Functions
|
|
386
422
|
|
|
387
|
-
|
|
423
|
+
Direct exports for common operations:
|
|
388
424
|
|
|
389
425
|
```js
|
|
390
|
-
import {
|
|
426
|
+
import {
|
|
427
|
+
// Transforms
|
|
428
|
+
translate2D, rotate2D, scale2D, transform2D,
|
|
429
|
+
translate3D, scale3D, transform3D,
|
|
391
430
|
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// 4. Remove transform attribute, update path d attribute
|
|
396
|
-
// 5. Convert CTM back to SVG: SVGFlatten.toSVGMatrix(ctm, 6)
|
|
397
|
-
```
|
|
431
|
+
// Shape conversion
|
|
432
|
+
circleToPath, ellipseToPath, rectToPath, lineToPath,
|
|
433
|
+
polygonToPath, polylineToPath,
|
|
398
434
|
|
|
399
|
-
|
|
435
|
+
// Path manipulation
|
|
436
|
+
parsePath, pathToString, pathToAbsolute, pathToCubics, transformPath,
|
|
437
|
+
elementToPath,
|
|
400
438
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
import { Decimal, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
439
|
+
// Matrix/Vector creation
|
|
440
|
+
identity, zeros, vec, mat,
|
|
404
441
|
|
|
405
|
-
|
|
442
|
+
// Precision control
|
|
443
|
+
setPrecision, getPrecision,
|
|
406
444
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
</script>
|
|
445
|
+
// Constants
|
|
446
|
+
getKappa
|
|
447
|
+
} from '@emasoft/svg-matrix';
|
|
411
448
|
```
|
|
412
449
|
|
|
413
450
|
## License
|