@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.
@@ -0,0 +1,348 @@
1
+ import Decimal from 'decimal.js';
2
+ import { Matrix } from './matrix.js';
3
+
4
+ const D = x => (x instanceof Decimal ? x : new Decimal(x));
5
+
6
+ export function getKappa() {
7
+ const two = new Decimal(2);
8
+ const three = new Decimal(3);
9
+ const four = new Decimal(4);
10
+ return four.mul(two.sqrt().minus(1)).div(three);
11
+ }
12
+
13
+ function formatNumber(value, precision = 6) {
14
+ // Use toFixed to preserve trailing zeros for consistent output formatting
15
+ return value.toFixed(precision);
16
+ }
17
+
18
+ export function circleToPathData(cx, cy, r, precision = 6) {
19
+ const cxD = D(cx), cyD = D(cy), rD = D(r);
20
+ const k = getKappa().mul(rD);
21
+ const x0 = cxD.plus(rD), y0 = cyD;
22
+ const c1x1 = x0, c1y1 = y0.minus(k), c1x2 = cxD.plus(k), c1y2 = cyD.minus(rD), x1 = cxD, y1 = cyD.minus(rD);
23
+ const c2x1 = cxD.minus(k), c2y1 = y1, c2x2 = cxD.minus(rD), c2y2 = cyD.minus(k), x2 = cxD.minus(rD), y2 = cyD;
24
+ const c3x1 = x2, c3y1 = cyD.plus(k), c3x2 = cxD.minus(k), c3y2 = cyD.plus(rD), x3 = cxD, y3 = cyD.plus(rD);
25
+ const c4x1 = cxD.plus(k), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(k);
26
+ const f = v => formatNumber(v, precision);
27
+ return `M ${f(x0)} ${f(y0)} C ${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)} C ${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)} C ${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)} C ${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)} Z`;
28
+ }
29
+
30
+ export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
31
+ const cxD = D(cx), cyD = D(cy), rxD = D(rx), ryD = D(ry);
32
+ const kappa = getKappa(), kx = kappa.mul(rxD), ky = kappa.mul(ryD);
33
+ const x0 = cxD.plus(rxD), y0 = cyD;
34
+ const c1x1 = x0, c1y1 = y0.minus(ky), c1x2 = cxD.plus(kx), c1y2 = cyD.minus(ryD), x1 = cxD, y1 = cyD.minus(ryD);
35
+ const c2x1 = cxD.minus(kx), c2y1 = y1, c2x2 = cxD.minus(rxD), c2y2 = cyD.minus(ky), x2 = cxD.minus(rxD), y2 = cyD;
36
+ const c3x1 = x2, c3y1 = cyD.plus(ky), c3x2 = cxD.minus(kx), c3y2 = cyD.plus(ryD), x3 = cxD, y3 = cyD.plus(ryD);
37
+ const c4x1 = cxD.plus(kx), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(ky);
38
+ const f = v => formatNumber(v, precision);
39
+ return `M ${f(x0)} ${f(y0)} C ${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)} C ${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)} C ${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)} C ${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)} Z`;
40
+ }
41
+
42
+ export function rectToPathData(x, y, width, height, rx = 0, ry = null, useArcs = false, precision = 6) {
43
+ const xD = D(x), yD = D(y), wD = D(width), hD = D(height);
44
+ let rxD = D(rx || 0), ryD = ry !== null ? D(ry) : rxD;
45
+ const halfW = wD.div(2), halfH = hD.div(2);
46
+ if (rxD.gt(halfW)) rxD = halfW;
47
+ if (ryD.gt(halfH)) ryD = halfH;
48
+ const f = v => formatNumber(v, precision);
49
+ if (rxD.isZero() || ryD.isZero()) {
50
+ const x1 = xD.plus(wD), y1 = yD.plus(hD);
51
+ return `M ${f(xD)} ${f(yD)} L ${f(x1)} ${f(yD)} L ${f(x1)} ${f(y1)} L ${f(xD)} ${f(y1)} Z`;
52
+ }
53
+ const left = xD, right = xD.plus(wD), top = yD, bottom = yD.plus(hD);
54
+ const leftInner = left.plus(rxD), rightInner = right.minus(rxD);
55
+ const topInner = top.plus(ryD), bottomInner = bottom.minus(ryD);
56
+ if (useArcs) {
57
+ return `M ${f(leftInner)} ${f(top)} L ${f(rightInner)} ${f(top)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(right)} ${f(topInner)} L ${f(right)} ${f(bottomInner)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(rightInner)} ${f(bottom)} L ${f(leftInner)} ${f(bottom)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(left)} ${f(bottomInner)} L ${f(left)} ${f(topInner)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(leftInner)} ${f(top)} Z`;
58
+ }
59
+ const kappa = getKappa(), kx = kappa.mul(rxD), ky = kappa.mul(ryD);
60
+ return `M ${f(leftInner)} ${f(top)} L ${f(rightInner)} ${f(top)} C ${f(rightInner)} ${f(top)} ${f(right)} ${f(topInner.minus(ky))} ${f(right)} ${f(topInner)} L ${f(right)} ${f(bottomInner)} C ${f(right)} ${f(bottomInner)} ${f(rightInner.plus(kx))} ${f(bottom)} ${f(rightInner)} ${f(bottom)} L ${f(leftInner)} ${f(bottom)} C ${f(leftInner)} ${f(bottom)} ${f(left)} ${f(bottomInner.plus(ky))} ${f(left)} ${f(bottomInner)} L ${f(left)} ${f(topInner)} C ${f(left)} ${f(topInner)} ${f(leftInner.minus(kx))} ${f(top)} ${f(leftInner)} ${f(top)} Z`;
61
+ }
62
+
63
+ export function lineToPathData(x1, y1, x2, y2, precision = 6) {
64
+ const f = v => formatNumber(D(v), precision);
65
+ return `M ${f(x1)} ${f(y1)} L ${f(x2)} ${f(y2)}`;
66
+ }
67
+
68
+ function parsePoints(points) {
69
+ if (Array.isArray(points)) return points.map(([x, y]) => [D(x), D(y)]);
70
+ const nums = points.split(/[\s,]+/).filter(s => s.length > 0).map(s => D(s));
71
+ const pairs = [];
72
+ for (let i = 0; i < nums.length; i += 2) {
73
+ if (i + 1 < nums.length) pairs.push([nums[i], nums[i + 1]]);
74
+ }
75
+ return pairs;
76
+ }
77
+
78
+ export function polylineToPathData(points, precision = 6) {
79
+ const pairs = parsePoints(points);
80
+ if (pairs.length === 0) return '';
81
+ const f = v => formatNumber(v, precision);
82
+ const [x0, y0] = pairs[0];
83
+ let path = `M ${f(x0)} ${f(y0)}`;
84
+ for (let i = 1; i < pairs.length; i++) {
85
+ const [x, y] = pairs[i];
86
+ path += ` L ${f(x)} ${f(y)}`;
87
+ }
88
+ return path;
89
+ }
90
+
91
+ export function polygonToPathData(points, precision = 6) {
92
+ const path = polylineToPathData(points, precision);
93
+ return path ? path + ' Z' : '';
94
+ }
95
+
96
+ export function parsePathData(pathData) {
97
+ const commands = [];
98
+ const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])\s*([^MmLlHhVvCcSsQqTtAaZz]*)/g;
99
+ let match;
100
+ while ((match = commandRegex.exec(pathData)) !== null) {
101
+ const command = match[1];
102
+ const argsStr = match[2].trim();
103
+ const args = argsStr.length > 0 ? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(s => D(s)) : [];
104
+ commands.push({ command, args });
105
+ }
106
+ return commands;
107
+ }
108
+
109
+ export function pathArrayToString(commands, precision = 6) {
110
+ return commands.map(({ command, args }) => {
111
+ const argsStr = args.map(a => formatNumber(a, precision)).join(' ');
112
+ return argsStr.length > 0 ? `${command} ${argsStr}` : command;
113
+ }).join(' ');
114
+ }
115
+
116
+ export function pathToAbsolute(pathData) {
117
+ const commands = parsePathData(pathData);
118
+ const result = [];
119
+ let currentX = new Decimal(0), currentY = new Decimal(0);
120
+ let subpathStartX = new Decimal(0), subpathStartY = new Decimal(0);
121
+ for (const { command, args } of commands) {
122
+ const isRelative = command === command.toLowerCase();
123
+ const upperCmd = command.toUpperCase();
124
+ if (upperCmd === 'M') {
125
+ const x = isRelative ? currentX.plus(args[0]) : args[0];
126
+ const y = isRelative ? currentY.plus(args[1]) : args[1];
127
+ currentX = x; currentY = y; subpathStartX = x; subpathStartY = y;
128
+ result.push({ command: 'M', args: [x, y] });
129
+ } else if (upperCmd === 'L') {
130
+ const x = isRelative ? currentX.plus(args[0]) : args[0];
131
+ const y = isRelative ? currentY.plus(args[1]) : args[1];
132
+ currentX = x; currentY = y;
133
+ result.push({ command: 'L', args: [x, y] });
134
+ } else if (upperCmd === 'H') {
135
+ const x = isRelative ? currentX.plus(args[0]) : args[0];
136
+ currentX = x;
137
+ result.push({ command: 'L', args: [x, currentY] });
138
+ } else if (upperCmd === 'V') {
139
+ const y = isRelative ? currentY.plus(args[0]) : args[0];
140
+ currentY = y;
141
+ result.push({ command: 'L', args: [currentX, y] });
142
+ } else if (upperCmd === 'C') {
143
+ const x1 = isRelative ? currentX.plus(args[0]) : args[0];
144
+ const y1 = isRelative ? currentY.plus(args[1]) : args[1];
145
+ const x2 = isRelative ? currentX.plus(args[2]) : args[2];
146
+ const y2 = isRelative ? currentY.plus(args[3]) : args[3];
147
+ const x = isRelative ? currentX.plus(args[4]) : args[4];
148
+ const y = isRelative ? currentY.plus(args[5]) : args[5];
149
+ currentX = x; currentY = y;
150
+ result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
151
+ } else if (upperCmd === 'A') {
152
+ const x = isRelative ? currentX.plus(args[5]) : args[5];
153
+ const y = isRelative ? currentY.plus(args[6]) : args[6];
154
+ currentX = x; currentY = y;
155
+ result.push({ command: 'A', args: [args[0], args[1], args[2], args[3], args[4], x, y] });
156
+ } else if (upperCmd === 'Z') {
157
+ currentX = subpathStartX; currentY = subpathStartY;
158
+ result.push({ command: 'Z', args: [] });
159
+ } else {
160
+ result.push({ command, args });
161
+ }
162
+ }
163
+ return pathArrayToString(result);
164
+ }
165
+
166
+ export function transformArcParams(rx, ry, xAxisRotation, largeArc, sweep, endX, endY, matrix) {
167
+ const rxD = D(rx), ryD = D(ry), rotD = D(xAxisRotation);
168
+ const endXD = D(endX), endYD = D(endY);
169
+
170
+ // Transform the endpoint
171
+ const endPoint = Matrix.from([[endXD], [endYD], [new Decimal(1)]]);
172
+ const transformedEnd = matrix.mul(endPoint);
173
+ const newEndX = transformedEnd.data[0][0].div(transformedEnd.data[2][0]);
174
+ const newEndY = transformedEnd.data[1][0].div(transformedEnd.data[2][0]);
175
+
176
+ // Extract the 2x2 linear part of the affine transformation
177
+ const a = matrix.data[0][0], b = matrix.data[0][1];
178
+ const c = matrix.data[1][0], d = matrix.data[1][1];
179
+
180
+ // Calculate determinant to check for reflection (flips sweep direction)
181
+ const det = a.mul(d).minus(b.mul(c));
182
+ const newSweep = det.isNegative() ? (sweep === 1 ? 0 : 1) : sweep;
183
+
184
+ // Transform ellipse radii by applying the 2x2 linear transformation
185
+ // For an ellipse with rotation, we need to compute the transformed ellipse parameters
186
+ // Convert rotation to radians
187
+ const rotRad = rotD.mul(new Decimal(Math.PI)).div(180);
188
+ const cosRot = Decimal.cos(rotRad), sinRot = Decimal.sin(rotRad);
189
+
190
+ // Unit ellipse basis vectors (before xAxisRotation)
191
+ // The ellipse can be parameterized as: [rx*cos(t)*cos(rot) - ry*sin(t)*sin(rot), rx*cos(t)*sin(rot) + ry*sin(t)*cos(rot)]
192
+ // We transform the two principal axis vectors and compute new radii from them
193
+
194
+ // Vector along major axis (rotated rx direction)
195
+ const v1x = rxD.mul(cosRot);
196
+ const v1y = rxD.mul(sinRot);
197
+
198
+ // Vector along minor axis (rotated ry direction, perpendicular to major)
199
+ const v2x = ryD.mul(sinRot).neg();
200
+ const v2y = ryD.mul(cosRot);
201
+
202
+ // Apply linear transformation to these vectors
203
+ const t1x = a.mul(v1x).plus(b.mul(v1y));
204
+ const t1y = c.mul(v1x).plus(d.mul(v1y));
205
+ const t2x = a.mul(v2x).plus(b.mul(v2y));
206
+ const t2y = c.mul(v2x).plus(d.mul(v2y));
207
+
208
+ // Compute the lengths of transformed vectors (approximate new radii)
209
+ // For a general affine transform, this gives the scale applied to each axis
210
+ const newRx = Decimal.sqrt(t1x.mul(t1x).plus(t1y.mul(t1y)));
211
+ const newRy = Decimal.sqrt(t2x.mul(t2x).plus(t2y.mul(t2y)));
212
+
213
+ // Compute new rotation angle from the transformed major axis
214
+ const newRotRad = Decimal.atan2(t1y, t1x);
215
+ const newRot = newRotRad.mul(180).div(new Decimal(Math.PI));
216
+
217
+ return [newRx, newRy, newRot, largeArc, newSweep, newEndX, newEndY];
218
+ }
219
+
220
+ export function transformPathData(pathData, matrix, precision = 6) {
221
+ const absPath = pathToAbsolute(pathData);
222
+ const commands = parsePathData(absPath);
223
+ const result = [];
224
+ for (const { command, args } of commands) {
225
+ if (command === 'M' || command === 'L') {
226
+ const pt = Matrix.from([[args[0]], [args[1]], [new Decimal(1)]]);
227
+ const transformed = matrix.mul(pt);
228
+ const x = transformed.data[0][0].div(transformed.data[2][0]);
229
+ const y = transformed.data[1][0].div(transformed.data[2][0]);
230
+ result.push({ command, args: [x, y] });
231
+ } else if (command === 'C') {
232
+ const transformedArgs = [];
233
+ for (let i = 0; i < 6; i += 2) {
234
+ const pt = Matrix.from([[args[i]], [args[i + 1]], [new Decimal(1)]]);
235
+ const transformed = matrix.mul(pt);
236
+ transformedArgs.push(transformed.data[0][0].div(transformed.data[2][0]));
237
+ transformedArgs.push(transformed.data[1][0].div(transformed.data[2][0]));
238
+ }
239
+ result.push({ command, args: transformedArgs });
240
+ } else if (command === 'A') {
241
+ const [newRx, newRy, newRot, newLarge, newSweep, newEndX, newEndY] =
242
+ transformArcParams(args[0], args[1], args[2], args[3], args[4], args[5], args[6], matrix);
243
+ result.push({ command, args: [newRx, newRy, newRot, newLarge, newSweep, newEndX, newEndY] });
244
+ } else {
245
+ result.push({ command, args });
246
+ }
247
+ }
248
+ return pathArrayToString(result, precision);
249
+ }
250
+
251
+ function quadraticToCubic(x0, y0, x1, y1, x2, y2) {
252
+ const twoThirds = new Decimal(2).div(3);
253
+ const cp1x = x0.plus(twoThirds.mul(x1.minus(x0)));
254
+ const cp1y = y0.plus(twoThirds.mul(y1.minus(y0)));
255
+ const cp2x = x2.plus(twoThirds.mul(x1.minus(x2)));
256
+ const cp2y = y2.plus(twoThirds.mul(y1.minus(y2)));
257
+ return [cp1x, cp1y, cp2x, cp2y, x2, y2];
258
+ }
259
+
260
+ export function pathToCubics(pathData) {
261
+ const absPath = pathToAbsolute(pathData);
262
+ const commands = parsePathData(absPath);
263
+ const result = [];
264
+ let currentX = new Decimal(0), currentY = new Decimal(0);
265
+ let lastControlX = new Decimal(0), lastControlY = new Decimal(0);
266
+ let lastCommand = '';
267
+ for (const { command, args } of commands) {
268
+ if (command === 'M') {
269
+ currentX = args[0]; currentY = args[1];
270
+ result.push({ command: 'M', args: [currentX, currentY] });
271
+ lastCommand = 'M';
272
+ } else if (command === 'L') {
273
+ const x = args[0], y = args[1];
274
+ result.push({ command: 'C', args: [currentX, currentY, x, y, x, y] });
275
+ currentX = x; currentY = y;
276
+ lastCommand = 'L';
277
+ } else if (command === 'C') {
278
+ const [x1, y1, x2, y2, x, y] = args;
279
+ result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
280
+ lastControlX = x2; lastControlY = y2;
281
+ currentX = x; currentY = y;
282
+ lastCommand = 'C';
283
+ } else if (command === 'S') {
284
+ let x1, y1;
285
+ if (lastCommand === 'C' || lastCommand === 'S') {
286
+ x1 = currentX.mul(2).minus(lastControlX);
287
+ y1 = currentY.mul(2).minus(lastControlY);
288
+ } else {
289
+ x1 = currentX; y1 = currentY;
290
+ }
291
+ const [x2, y2, x, y] = args;
292
+ result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
293
+ lastControlX = x2; lastControlY = y2;
294
+ currentX = x; currentY = y;
295
+ lastCommand = 'S';
296
+ } else if (command === 'Q') {
297
+ const [x1, y1, x, y] = args;
298
+ const cubic = quadraticToCubic(currentX, currentY, x1, y1, x, y);
299
+ result.push({ command: 'C', args: cubic });
300
+ lastControlX = x1; lastControlY = y1;
301
+ currentX = x; currentY = y;
302
+ lastCommand = 'Q';
303
+ } else if (command === 'T') {
304
+ let x1, y1;
305
+ if (lastCommand === 'Q' || lastCommand === 'T') {
306
+ x1 = currentX.mul(2).minus(lastControlX);
307
+ y1 = currentY.mul(2).minus(lastControlY);
308
+ } else {
309
+ x1 = currentX; y1 = currentY;
310
+ }
311
+ const [x, y] = args;
312
+ const cubic = quadraticToCubic(currentX, currentY, x1, y1, x, y);
313
+ result.push({ command: 'C', args: cubic });
314
+ lastControlX = x1; lastControlY = y1;
315
+ currentX = x; currentY = y;
316
+ lastCommand = 'T';
317
+ } else if (command === 'Z') {
318
+ result.push({ command: 'Z', args: [] });
319
+ lastCommand = 'Z';
320
+ } else {
321
+ result.push({ command, args });
322
+ lastCommand = command;
323
+ }
324
+ }
325
+ return pathArrayToString(result);
326
+ }
327
+
328
+ export function convertElementToPath(element, precision = 6) {
329
+ const getAttr = (name, defaultValue = 0) => {
330
+ if (element.getAttribute) return element.getAttribute(name) || defaultValue;
331
+ return element[name] !== undefined ? element[name] : defaultValue;
332
+ };
333
+ const tagName = (element.tagName || element.type || '').toLowerCase();
334
+ if (tagName === 'circle') {
335
+ return circleToPathData(getAttr('cx', 0), getAttr('cy', 0), getAttr('r', 0), precision);
336
+ } else if (tagName === 'ellipse') {
337
+ return ellipseToPathData(getAttr('cx', 0), getAttr('cy', 0), getAttr('rx', 0), getAttr('ry', 0), precision);
338
+ } else if (tagName === 'rect') {
339
+ return rectToPathData(getAttr('x', 0), getAttr('y', 0), getAttr('width', 0), getAttr('height', 0), getAttr('rx', 0), getAttr('ry', null), false, precision);
340
+ } else if (tagName === 'line') {
341
+ return lineToPathData(getAttr('x1', 0), getAttr('y1', 0), getAttr('x2', 0), getAttr('y2', 0), precision);
342
+ } else if (tagName === 'polyline') {
343
+ return polylineToPathData(getAttr('points', ''), precision);
344
+ } else if (tagName === 'polygon') {
345
+ return polygonToPathData(getAttr('points', ''), precision);
346
+ }
347
+ return null;
348
+ }