@emasoft/svg-matrix 1.0.5 → 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 +317 -396
- 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 +1264 -105
- 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/preserveAspectRatio_SVG.svg +0 -63
- package/samples/test.svg +0 -39
package/README.md
CHANGED
|
@@ -1,529 +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
|
|
16
14
|
|
|
17
|
-
|
|
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
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
## Precision
|
|
28
35
|
|
|
29
|
-
|
|
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 |
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
<script type="module">
|
|
33
|
-
import { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
34
|
-
|
|
35
|
-
Decimal.set({ precision: 80 });
|
|
42
|
+
**When precision matters:** GIS, CAD, scientific visualization, deep transform hierarchies, accumulated operations.
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
const M = Transforms2D.translation(2, 3)
|
|
39
|
-
.mul(Transforms2D.rotate(Math.PI / 4))
|
|
40
|
-
.mul(Transforms2D.scale(1.5));
|
|
44
|
+
**When floats suffice:** Simple transforms, small coordinates, visual applications where sub-pixel errors are imperceptible.
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log('Transformed:', x.toString(), y.toString());
|
|
45
|
-
</script>
|
|
46
|
+
```bash
|
|
47
|
+
node test/benchmark-precision.js
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
## Installation
|
|
49
51
|
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
```bash
|
|
53
|
+
npm install @emasoft/svg-matrix
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from '@emasoft/svg-matrix';
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
### CDN
|
|
57
61
|
|
|
58
62
|
```html
|
|
59
|
-
<script type="importmap">
|
|
60
|
-
{
|
|
61
|
-
"imports": {
|
|
62
|
-
"@emasoft/svg-matrix": "https://esm.sh/@emasoft/svg-matrix"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
</script>
|
|
66
63
|
<script type="module">
|
|
67
|
-
import { Matrix, Vector, Transforms2D } from '
|
|
64
|
+
import { Matrix, Vector, Transforms2D } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
68
65
|
</script>
|
|
69
66
|
```
|
|
70
67
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
### Vector Operations
|
|
68
|
+
## Quick Start
|
|
74
69
|
|
|
75
70
|
```js
|
|
76
|
-
import { Vector,
|
|
71
|
+
import { Decimal, Matrix, Vector, Transforms2D, SVGFlatten } from '@emasoft/svg-matrix';
|
|
77
72
|
|
|
78
73
|
Decimal.set({ precision: 80 });
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.log('Add:', v.add(w).toNumberArray()); // [5, 7, 9]
|
|
85
|
-
console.log('Scale:', v.scale(2).toNumberArray()); // [2, 4, 6]
|
|
86
|
-
console.log('Dot:', v.dot(w).toString()); // 32
|
|
87
|
-
console.log('Cross:', v.cross(w).toNumberArray()); // [-3, 6, -3]
|
|
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));
|
|
88
79
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
console.log('Normalized:', v.normalize().toNumberArray());
|
|
92
|
-
console.log('Angle:', v.angleBetween(w).toString()); // radians
|
|
80
|
+
// Apply to point
|
|
81
|
+
const [x, y] = Transforms2D.applyTransform(M, 1, 0);
|
|
93
82
|
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
console.log('Projection:', proj.toNumberArray());
|
|
83
|
+
// Round-trip with inverse
|
|
84
|
+
const [xBack, yBack] = Transforms2D.applyTransform(M.inverse(), x, y);
|
|
97
85
|
```
|
|
98
86
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
import { Matrix } from '@emasoft/svg-matrix';
|
|
87
|
+
---
|
|
103
88
|
|
|
104
|
-
|
|
105
|
-
const B = Matrix.from([[5, 6], [7, 8]]);
|
|
89
|
+
## API Reference
|
|
106
90
|
|
|
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
|
|
91
|
+
### Linear Algebra
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
const inv = A.inverse();
|
|
115
|
-
console.log('Inverse:', inv.toNumberArray());
|
|
93
|
+
#### Vector
|
|
116
94
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log('Solution:', x.toNumberArray());
|
|
95
|
+
```js
|
|
96
|
+
import { Vector } from '@emasoft/svg-matrix';
|
|
120
97
|
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const { Q, R } = A.qr();
|
|
98
|
+
const v = Vector.from([1, 2, 3]);
|
|
99
|
+
const w = Vector.from([4, 5, 6]);
|
|
124
100
|
|
|
125
|
-
//
|
|
126
|
-
|
|
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]
|
|
127
113
|
```
|
|
128
114
|
|
|
129
|
-
|
|
115
|
+
#### Matrix
|
|
130
116
|
|
|
131
117
|
```js
|
|
132
|
-
import {
|
|
118
|
+
import { Matrix } from '@emasoft/svg-matrix';
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
const A = Matrix.from([[1, 2], [3, 4]]);
|
|
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
|
+
```
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
const Rp = Transforms2D.rotateAroundPoint(Math.PI / 2, 5, 5);
|
|
138
|
+
### Transforms
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
const Sk = Transforms2D.skew(0.5, 0);
|
|
145
|
-
const St = Transforms2D.stretchAlongAxis(1, 0, 2); // stretch 2x along X
|
|
140
|
+
#### 2D (3x3 matrices)
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const Ry = Transforms2D.reflectY(); // flip X
|
|
150
|
-
const Ro = Transforms2D.reflectOrigin(); // flip both
|
|
142
|
+
```js
|
|
143
|
+
import { Transforms2D } from '@emasoft/svg-matrix';
|
|
151
144
|
|
|
152
|
-
|
|
153
|
-
|
|
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()
|
|
154
154
|
|
|
155
155
|
// Apply to point
|
|
156
|
-
const [x, y] = Transforms2D.applyTransform(
|
|
157
|
-
|
|
158
|
-
// Inverse transform
|
|
159
|
-
const Minv = M.inverse();
|
|
160
|
-
const [xBack, yBack] = Transforms2D.applyTransform(Minv, x, y);
|
|
156
|
+
const [x, y] = Transforms2D.applyTransform(matrix, px, py);
|
|
161
157
|
```
|
|
162
158
|
|
|
163
|
-
|
|
159
|
+
#### 3D (4x4 matrices)
|
|
164
160
|
|
|
165
161
|
```js
|
|
166
162
|
import { Transforms3D } from '@emasoft/svg-matrix';
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
```
|
|
171
177
|
|
|
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);
|
|
178
|
+
### SVG Processing
|
|
176
179
|
|
|
177
|
-
|
|
178
|
-
const Raxis = Transforms3D.rotateAroundAxis(1, 1, 0, Math.PI / 3);
|
|
180
|
+
#### SVGFlatten - Transform Parsing & CTM
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
```js
|
|
183
|
+
import { SVGFlatten } from '@emasoft/svg-matrix';
|
|
182
184
|
|
|
183
|
-
//
|
|
184
|
-
const
|
|
185
|
-
const RfXZ = Transforms3D.reflectXZ(); // flip Y
|
|
186
|
-
const RfYZ = Transforms3D.reflectYZ(); // flip X
|
|
187
|
-
const RfO = Transforms3D.reflectOrigin(); // flip all
|
|
185
|
+
// Parse transform attributes
|
|
186
|
+
const m = SVGFlatten.parseTransformAttribute('translate(50,50) rotate(45) scale(2)');
|
|
188
187
|
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
]);
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
// Apply to point
|
|
197
|
+
const { x, y } = SVGFlatten.applyToPoint(ctm, 10, 10);
|
|
195
198
|
|
|
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
|
-
| **viewBox & Viewport** | |
|
|
282
|
-
| `parseViewBox(str)` | Parse viewBox attribute "minX minY width height" |
|
|
283
|
-
| `parsePreserveAspectRatio(str)` | Parse preserveAspectRatio (align, meet/slice) |
|
|
284
|
-
| `computeViewBoxTransform(vb, vpW, vpH, par)` | Compute viewBox to viewport matrix |
|
|
285
|
-
| `SVGViewport` class | Represents viewport with viewBox + preserveAspectRatio |
|
|
286
|
-
| `buildFullCTM(hierarchy)` | Build CTM from SVG/group/element hierarchy |
|
|
287
|
-
| **Units & Percentages** | |
|
|
288
|
-
| `resolveLength(value, ref, dpi?)` | Resolve px, %, em, pt, in, cm, mm, pc units |
|
|
289
|
-
| `resolvePercentages(x, y, vpW, vpH)` | Resolve x/y percentages to viewport |
|
|
290
|
-
| `normalizedDiagonal(w, h)` | Compute sqrt(w^2+h^2)/sqrt(2) for percentages |
|
|
291
|
-
| **Object Bounding Box** | |
|
|
292
|
-
| `objectBoundingBoxTransform(x, y, w, h)` | Transform for objectBoundingBox units |
|
|
293
|
-
| **Transform Parsing** | |
|
|
294
|
-
| `parseTransformFunction(func, args)` | Parse a single SVG transform function |
|
|
295
|
-
| `parseTransformAttribute(str)` | Parse a full SVG transform attribute string |
|
|
296
|
-
| `buildCTM(transformStack)` | Build CTM from array of transform strings |
|
|
297
|
-
| `applyToPoint(ctm, x, y)` | Apply CTM to a 2D point |
|
|
298
|
-
| `toSVGMatrix(ctm, precision?)` | Convert CTM back to SVG matrix() notation |
|
|
299
|
-
| `isIdentity(m, tolerance?)` | Check if matrix is effectively identity |
|
|
300
|
-
| `transformPathData(pathD, ctm)` | Transform path data coordinates |
|
|
301
|
-
| `PRECISION_INFO` | Object with precision comparison data |
|
|
302
|
-
|
|
303
|
-
## SVG Transform Flattening
|
|
304
|
-
|
|
305
|
-
The `SVGFlatten` module provides tools for parsing SVG transform attributes, building CTMs (Current Transform Matrices), and flattening nested transforms with arbitrary precision.
|
|
306
|
-
|
|
307
|
-
### Why Use SVGFlatten?
|
|
308
|
-
|
|
309
|
-
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:
|
|
199
|
+
// Transform path data
|
|
200
|
+
const transformed = SVGFlatten.transformPathData('M 100 100 L 200 200', ctm);
|
|
310
201
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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);
|
|
315
206
|
|
|
316
|
-
|
|
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
|
+
]);
|
|
317
214
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
9.999999999999999999999999999999999999999999999999999999999999999999999999999999998)
|
|
322
|
-
Round-trip error: X=0, Y=2e-79
|
|
323
|
-
```
|
|
215
|
+
// Unit resolution (px, %, em, pt, in, cm, mm, pc)
|
|
216
|
+
SVGFlatten.resolveLength('50%', 800); // → 400
|
|
217
|
+
SVGFlatten.resolveLength('1in', 800); // → 96
|
|
324
218
|
|
|
325
|
-
|
|
219
|
+
// objectBoundingBox transform
|
|
220
|
+
const bboxTransform = SVGFlatten.objectBoundingBoxTransform(100, 50, 200, 100);
|
|
221
|
+
```
|
|
326
222
|
|
|
327
|
-
|
|
223
|
+
#### GeometryToPath - Shape Conversion
|
|
328
224
|
|
|
329
225
|
```js
|
|
330
|
-
import {
|
|
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()
|
|
246
|
+
```
|
|
331
247
|
|
|
332
|
-
|
|
333
|
-
const m1 = SVGFlatten.parseTransformAttribute('translate(10, 20)');
|
|
334
|
-
const m2 = SVGFlatten.parseTransformAttribute('rotate(45)');
|
|
335
|
-
const m3 = SVGFlatten.parseTransformAttribute('scale(2, 0.5)');
|
|
336
|
-
const m4 = SVGFlatten.parseTransformAttribute('skewX(15)');
|
|
337
|
-
const m5 = SVGFlatten.parseTransformAttribute('matrix(0.866, 0.5, -0.5, 0.866, 0, 0)');
|
|
248
|
+
#### BrowserVerify - Chrome Verification
|
|
338
249
|
|
|
339
|
-
|
|
340
|
-
|
|
250
|
+
```js
|
|
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 });
|
|
341
267
|
```
|
|
342
268
|
|
|
343
|
-
###
|
|
269
|
+
### Polygon Operations
|
|
344
270
|
|
|
345
|
-
|
|
346
|
-
import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
|
|
271
|
+
#### PolygonClip
|
|
347
272
|
|
|
348
|
-
|
|
273
|
+
```js
|
|
274
|
+
import { PolygonClip } from '@emasoft/svg-matrix';
|
|
349
275
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// <g transform="scale(1.2, 0.8)"> <!-- g4 -->
|
|
356
|
-
// <path transform="matrix(...)"/> <!-- element -->
|
|
357
|
-
|
|
358
|
-
const transformStack = [
|
|
359
|
-
'scale(1.5)', // viewBox scaling
|
|
360
|
-
'translate(-13.613145,-10.209854)', // g1
|
|
361
|
-
'translate(-1144.8563,517.64642)', // g2
|
|
362
|
-
'rotate(15)', // g3
|
|
363
|
-
'scale(1.2, 0.8)', // g4
|
|
364
|
-
'matrix(0.71577068,0,0,1.3970955,0,0)' // element
|
|
276
|
+
const square = [
|
|
277
|
+
PolygonClip.point(0, 0),
|
|
278
|
+
PolygonClip.point(2, 0),
|
|
279
|
+
PolygonClip.point(2, 2),
|
|
280
|
+
PolygonClip.point(0, 2)
|
|
365
281
|
];
|
|
366
282
|
|
|
367
|
-
//
|
|
368
|
-
|
|
283
|
+
// Boolean operations
|
|
284
|
+
PolygonClip.polygonIntersection(poly1, poly2)
|
|
285
|
+
PolygonClip.polygonUnion(poly1, poly2)
|
|
286
|
+
PolygonClip.polygonDifference(poly1, poly2)
|
|
369
287
|
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
373
293
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
const recovered = SVGFlatten.applyToPoint(inverseCTM, viewport.x, viewport.y);
|
|
294
|
+
// Convex hull
|
|
295
|
+
PolygonClip.convexHull(points)
|
|
377
296
|
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
console.log('Round-trip error X:', errorX.toString()); // 0
|
|
382
|
-
console.log('Round-trip error Y:', errorY.toString()); // ~2e-79
|
|
297
|
+
// Bounding box
|
|
298
|
+
PolygonClip.boundingBox(polygon) // {minX, minY, maxX, maxY}
|
|
299
|
+
PolygonClip.bboxIntersects(bbox1, bbox2)
|
|
383
300
|
```
|
|
384
301
|
|
|
385
|
-
|
|
302
|
+
#### ClipPathResolver
|
|
386
303
|
|
|
387
304
|
```js
|
|
388
|
-
import {
|
|
305
|
+
import { ClipPathResolver } from '@emasoft/svg-matrix';
|
|
389
306
|
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
const
|
|
307
|
+
// Parse and resolve clipPath
|
|
308
|
+
const clipData = ClipPathResolver.parseClipPathElement(element);
|
|
309
|
+
const clipPolygon = ClipPathResolver.resolveClipPath(clipData, targetBBox);
|
|
393
310
|
|
|
394
|
-
|
|
395
|
-
|
|
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)
|
|
315
|
+
|
|
316
|
+
// Apply clipPath to element
|
|
317
|
+
ClipPathResolver.applyClipPath(elementData, clipPathData, targetBBox)
|
|
396
318
|
```
|
|
397
319
|
|
|
398
|
-
###
|
|
320
|
+
### SVG Element Resolution
|
|
399
321
|
|
|
400
|
-
|
|
322
|
+
#### UseSymbolResolver
|
|
401
323
|
|
|
402
324
|
```js
|
|
403
|
-
import {
|
|
325
|
+
import { UseSymbolResolver } from '@emasoft/svg-matrix';
|
|
404
326
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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)
|
|
410
332
|
```
|
|
411
333
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
Per the [SVG 2 specification](https://www.w3.org/TR/SVG2/coords.html), the viewBox establishes a new coordinate system that maps to the viewport:
|
|
334
|
+
#### MarkerResolver
|
|
415
335
|
|
|
416
336
|
```js
|
|
417
|
-
import {
|
|
337
|
+
import { MarkerResolver } from '@emasoft/svg-matrix';
|
|
418
338
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// Compute the viewBox-to-viewport transform
|
|
426
|
-
const vbTransform = SVGFlatten.computeViewBoxTransform(viewBox, 800, 600, par);
|
|
427
|
-
|
|
428
|
-
// Point (50, 50) in viewBox coords maps to viewport
|
|
429
|
-
const point = SVGFlatten.applyToPoint(vbTransform, 50, 50);
|
|
430
|
-
console.log('Viewport coords:', point.x.toFixed(2), point.y.toFixed(2));
|
|
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)
|
|
431
344
|
```
|
|
432
345
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
Use `buildFullCTM` for complete SVG hierarchies including viewBox transforms:
|
|
346
|
+
#### PatternResolver
|
|
436
347
|
|
|
437
348
|
```js
|
|
438
|
-
import {
|
|
439
|
-
|
|
440
|
-
Decimal.set({ precision: 80 });
|
|
441
|
-
|
|
442
|
-
// Real SVG structure with viewBox and nested transforms
|
|
443
|
-
const hierarchy = [
|
|
444
|
-
{ type: 'svg', width: 800, height: 600, viewBox: '0 0 400 300', preserveAspectRatio: 'xMidYMid meet' },
|
|
445
|
-
{ type: 'g', transform: 'translate(50, 50)' },
|
|
446
|
-
{ type: 'g', transform: 'rotate(45)' },
|
|
447
|
-
{ type: 'element', transform: 'scale(2)' }
|
|
448
|
-
];
|
|
349
|
+
import { PatternResolver } from '@emasoft/svg-matrix';
|
|
449
350
|
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const inverse = ctm.inverse();
|
|
456
|
-
const recovered = SVGFlatten.applyToPoint(inverse, viewport.x, viewport.y);
|
|
457
|
-
// Error: X=2e-78, Y=2e-78
|
|
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)
|
|
458
356
|
```
|
|
459
357
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
Handle deeply nested `<svg>` elements, each with its own viewBox:
|
|
358
|
+
#### MaskResolver
|
|
463
359
|
|
|
464
360
|
```js
|
|
465
|
-
|
|
466
|
-
{ type: 'svg', width: 1000, height: 800, viewBox: '0 0 500 400' },
|
|
467
|
-
{ type: 'g', transform: 'translate(100, 100)' },
|
|
468
|
-
{ type: 'svg', width: 200, height: 150, viewBox: '0 0 100 75' },
|
|
469
|
-
{ type: 'element' }
|
|
470
|
-
];
|
|
361
|
+
import { MaskResolver } from '@emasoft/svg-matrix';
|
|
471
362
|
|
|
472
|
-
const
|
|
473
|
-
|
|
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
|
|
474
367
|
```
|
|
475
368
|
|
|
476
|
-
###
|
|
369
|
+
### Advanced Features
|
|
477
370
|
|
|
478
|
-
|
|
371
|
+
#### MeshGradient (SVG 2.0)
|
|
479
372
|
|
|
480
373
|
```js
|
|
481
|
-
import {
|
|
374
|
+
import { MeshGradient } from '@emasoft/svg-matrix';
|
|
482
375
|
|
|
483
|
-
|
|
376
|
+
// Parse mesh gradient
|
|
377
|
+
const meshDef = MeshGradient.parseMeshGradientElement(element);
|
|
378
|
+
const meshData = MeshGradient.parseMeshGradient(meshDef);
|
|
484
379
|
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
|
|
380
|
+
// Coons patch evaluation
|
|
381
|
+
const patch = new MeshGradient.CoonsPatch(topEdge, rightEdge, bottomEdge, leftEdge, cornerColors);
|
|
382
|
+
const { point, color } = patch.evaluate(u, v);
|
|
488
383
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
384
|
+
// Rasterize to ImageData
|
|
385
|
+
const imageData = MeshGradient.rasterizeMeshGradient(meshData, width, height);
|
|
386
|
+
|
|
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
|
+
```
|
|
494
394
|
|
|
495
|
-
|
|
496
|
-
SVGFlatten.resolveLength('2em', viewportWidth); // -> 32
|
|
395
|
+
#### TextToPath
|
|
497
396
|
|
|
498
|
-
|
|
499
|
-
|
|
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);
|
|
500
419
|
```
|
|
501
420
|
|
|
502
|
-
###
|
|
421
|
+
### Convenience Functions
|
|
503
422
|
|
|
504
|
-
|
|
423
|
+
Direct exports for common operations:
|
|
505
424
|
|
|
506
425
|
```js
|
|
507
|
-
|
|
508
|
-
|
|
426
|
+
import {
|
|
427
|
+
// Transforms
|
|
428
|
+
translate2D, rotate2D, scale2D, transform2D,
|
|
429
|
+
translate3D, scale3D, transform3D,
|
|
509
430
|
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
```
|
|
431
|
+
// Shape conversion
|
|
432
|
+
circleToPath, ellipseToPath, rectToPath, lineToPath,
|
|
433
|
+
polygonToPath, polylineToPath,
|
|
514
434
|
|
|
515
|
-
|
|
435
|
+
// Path manipulation
|
|
436
|
+
parsePath, pathToString, pathToAbsolute, pathToCubics, transformPath,
|
|
437
|
+
elementToPath,
|
|
516
438
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
import { Decimal, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
|
|
439
|
+
// Matrix/Vector creation
|
|
440
|
+
identity, zeros, vec, mat,
|
|
520
441
|
|
|
521
|
-
|
|
442
|
+
// Precision control
|
|
443
|
+
setPrecision, getPrecision,
|
|
522
444
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
</script>
|
|
445
|
+
// Constants
|
|
446
|
+
getKappa
|
|
447
|
+
} from '@emasoft/svg-matrix';
|
|
527
448
|
```
|
|
528
449
|
|
|
529
450
|
## License
|