@emasoft/svg-matrix 1.0.18 → 1.0.20
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +6 -9
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
package/src/geometry-to-path.js
CHANGED
|
@@ -137,9 +137,9 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
137
137
|
const y2 = y3.minus(kappa.mul(ryD).mul(ty3));
|
|
138
138
|
|
|
139
139
|
if (i === 0) {
|
|
140
|
-
commands.push(`M
|
|
140
|
+
commands.push(`M${f(x0)} ${f(y0)}`);
|
|
141
141
|
}
|
|
142
|
-
commands.push(`C
|
|
142
|
+
commands.push(`C${f(x1)} ${f(y1)} ${f(x2)} ${f(y2)} ${f(x3)} ${f(y3)}`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
commands.push('Z');
|
|
@@ -147,8 +147,13 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
function formatNumber(value, precision = 6) {
|
|
150
|
-
//
|
|
151
|
-
|
|
150
|
+
// Format with precision then remove trailing zeros for smaller output
|
|
151
|
+
let str = value.toFixed(precision);
|
|
152
|
+
// Remove trailing zeros after decimal point
|
|
153
|
+
if (str.includes('.')) {
|
|
154
|
+
str = str.replace(/\.?0+$/, '');
|
|
155
|
+
}
|
|
156
|
+
return str;
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
export function circleToPathData(cx, cy, r, precision = 6) {
|
|
@@ -160,7 +165,7 @@ export function circleToPathData(cx, cy, r, precision = 6) {
|
|
|
160
165
|
const c3x1 = x2, c3y1 = cyD.plus(k), c3x2 = cxD.minus(k), c3y2 = cyD.plus(rD), x3 = cxD, y3 = cyD.plus(rD);
|
|
161
166
|
const c4x1 = cxD.plus(k), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(k);
|
|
162
167
|
const f = v => formatNumber(v, precision);
|
|
163
|
-
return `M
|
|
168
|
+
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`;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
|
|
@@ -172,7 +177,7 @@ export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
|
|
|
172
177
|
const c3x1 = x2, c3y1 = cyD.plus(ky), c3x2 = cxD.minus(kx), c3y2 = cyD.plus(ryD), x3 = cxD, y3 = cyD.plus(ryD);
|
|
173
178
|
const c4x1 = cxD.plus(kx), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(ky);
|
|
174
179
|
const f = v => formatNumber(v, precision);
|
|
175
|
-
return `M
|
|
180
|
+
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`;
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
export function rectToPathData(x, y, width, height, rx = 0, ry = null, useArcs = false, precision = 6) {
|
|
@@ -184,21 +189,25 @@ export function rectToPathData(x, y, width, height, rx = 0, ry = null, useArcs =
|
|
|
184
189
|
const f = v => formatNumber(v, precision);
|
|
185
190
|
if (rxD.isZero() || ryD.isZero()) {
|
|
186
191
|
const x1 = xD.plus(wD), y1 = yD.plus(hD);
|
|
187
|
-
|
|
192
|
+
// Use H (horizontal) and V (vertical) commands for smaller output
|
|
193
|
+
return `M${f(xD)} ${f(yD)}H${f(x1)}V${f(y1)}H${f(xD)}Z`;
|
|
188
194
|
}
|
|
189
195
|
const left = xD, right = xD.plus(wD), top = yD, bottom = yD.plus(hD);
|
|
190
196
|
const leftInner = left.plus(rxD), rightInner = right.minus(rxD);
|
|
191
197
|
const topInner = top.plus(ryD), bottomInner = bottom.minus(ryD);
|
|
192
198
|
if (useArcs) {
|
|
193
|
-
return `M
|
|
199
|
+
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`;
|
|
194
200
|
}
|
|
195
201
|
const kappa = getKappa(), kx = kappa.mul(rxD), ky = kappa.mul(ryD);
|
|
196
|
-
|
|
202
|
+
// Each corner has two Bezier control points:
|
|
203
|
+
// First control point: offset from start point along the edge tangent
|
|
204
|
+
// Second control point: offset from end point along the edge tangent
|
|
205
|
+
return `M${f(leftInner)} ${f(top)}L${f(rightInner)} ${f(top)}C${f(rightInner.plus(kx))} ${f(top)} ${f(right)} ${f(topInner.minus(ky))} ${f(right)} ${f(topInner)}L${f(right)} ${f(bottomInner)}C${f(right)} ${f(bottomInner.plus(ky))} ${f(rightInner.plus(kx))} ${f(bottom)} ${f(rightInner)} ${f(bottom)}L${f(leftInner)} ${f(bottom)}C${f(leftInner.minus(kx))} ${f(bottom)} ${f(left)} ${f(bottomInner.plus(ky))} ${f(left)} ${f(bottomInner)}L${f(left)} ${f(topInner)}C${f(left)} ${f(topInner.minus(ky))} ${f(leftInner.minus(kx))} ${f(top)} ${f(leftInner)} ${f(top)}Z`;
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
export function lineToPathData(x1, y1, x2, y2, precision = 6) {
|
|
200
209
|
const f = v => formatNumber(D(v), precision);
|
|
201
|
-
return `M
|
|
210
|
+
return `M${f(x1)} ${f(y1)}L${f(x2)} ${f(y2)}`;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
function parsePoints(points) {
|
|
@@ -216,10 +225,10 @@ export function polylineToPathData(points, precision = 6) {
|
|
|
216
225
|
if (pairs.length === 0) return '';
|
|
217
226
|
const f = v => formatNumber(v, precision);
|
|
218
227
|
const [x0, y0] = pairs[0];
|
|
219
|
-
let path = `M
|
|
228
|
+
let path = `M${f(x0)} ${f(y0)}`;
|
|
220
229
|
for (let i = 1; i < pairs.length; i++) {
|
|
221
230
|
const [x, y] = pairs[i];
|
|
222
|
-
path += `
|
|
231
|
+
path += `L${f(x)} ${f(y)}`;
|
|
223
232
|
}
|
|
224
233
|
return path;
|
|
225
234
|
}
|
|
@@ -229,6 +238,13 @@ export function polygonToPathData(points, precision = 6) {
|
|
|
229
238
|
return path ? path + ' Z' : '';
|
|
230
239
|
}
|
|
231
240
|
|
|
241
|
+
// Parameter count for each SVG path command
|
|
242
|
+
const COMMAND_PARAMS = {
|
|
243
|
+
M: 2, m: 2, L: 2, l: 2, H: 1, h: 1, V: 1, v: 1,
|
|
244
|
+
C: 6, c: 6, S: 4, s: 4, Q: 4, q: 4, T: 2, t: 2,
|
|
245
|
+
A: 7, a: 7, Z: 0, z: 0
|
|
246
|
+
};
|
|
247
|
+
|
|
232
248
|
export function parsePathData(pathData) {
|
|
233
249
|
const commands = [];
|
|
234
250
|
const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])\s*([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
@@ -236,8 +252,29 @@ export function parsePathData(pathData) {
|
|
|
236
252
|
while ((match = commandRegex.exec(pathData)) !== null) {
|
|
237
253
|
const command = match[1];
|
|
238
254
|
const argsStr = match[2].trim();
|
|
239
|
-
const
|
|
240
|
-
|
|
255
|
+
const allArgs = argsStr.length > 0 ? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(s => D(s)) : [];
|
|
256
|
+
|
|
257
|
+
const paramCount = COMMAND_PARAMS[command];
|
|
258
|
+
|
|
259
|
+
if (paramCount === 0 || allArgs.length === 0) {
|
|
260
|
+
// Z/z command or command with no args
|
|
261
|
+
commands.push({ command, args: [] });
|
|
262
|
+
} else {
|
|
263
|
+
// Split args into groups based on parameter count
|
|
264
|
+
// Handle implicit command repetition per SVG spec
|
|
265
|
+
for (let i = 0; i < allArgs.length; i += paramCount) {
|
|
266
|
+
const args = allArgs.slice(i, i + paramCount);
|
|
267
|
+
if (args.length === paramCount) {
|
|
268
|
+
// For M/m, first group is moveto, subsequent groups become implicit lineto (L/l)
|
|
269
|
+
let effectiveCmd = command;
|
|
270
|
+
if (i > 0 && (command === 'M' || command === 'm')) {
|
|
271
|
+
effectiveCmd = command === 'M' ? 'L' : 'l';
|
|
272
|
+
}
|
|
273
|
+
commands.push({ command: effectiveCmd, args });
|
|
274
|
+
}
|
|
275
|
+
// Incomplete arg groups are silently dropped per SVG error handling
|
|
276
|
+
}
|
|
277
|
+
}
|
|
241
278
|
}
|
|
242
279
|
return commands;
|
|
243
280
|
}
|
|
@@ -254,6 +291,9 @@ export function pathToAbsolute(pathData) {
|
|
|
254
291
|
const result = [];
|
|
255
292
|
let currentX = new Decimal(0), currentY = new Decimal(0);
|
|
256
293
|
let subpathStartX = new Decimal(0), subpathStartY = new Decimal(0);
|
|
294
|
+
let lastControlX = new Decimal(0), lastControlY = new Decimal(0);
|
|
295
|
+
let lastCommand = '';
|
|
296
|
+
|
|
257
297
|
for (const { command, args } of commands) {
|
|
258
298
|
const isRelative = command === command.toLowerCase();
|
|
259
299
|
const upperCmd = command.toUpperCase();
|
|
@@ -262,19 +302,23 @@ export function pathToAbsolute(pathData) {
|
|
|
262
302
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
263
303
|
currentX = x; currentY = y; subpathStartX = x; subpathStartY = y;
|
|
264
304
|
result.push({ command: 'M', args: [x, y] });
|
|
305
|
+
lastCommand = 'M';
|
|
265
306
|
} else if (upperCmd === 'L') {
|
|
266
307
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
267
308
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
268
309
|
currentX = x; currentY = y;
|
|
269
310
|
result.push({ command: 'L', args: [x, y] });
|
|
311
|
+
lastCommand = 'L';
|
|
270
312
|
} else if (upperCmd === 'H') {
|
|
271
313
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
272
314
|
currentX = x;
|
|
273
315
|
result.push({ command: 'L', args: [x, currentY] });
|
|
316
|
+
lastCommand = 'H';
|
|
274
317
|
} else if (upperCmd === 'V') {
|
|
275
318
|
const y = isRelative ? currentY.plus(args[0]) : args[0];
|
|
276
319
|
currentY = y;
|
|
277
320
|
result.push({ command: 'L', args: [currentX, y] });
|
|
321
|
+
lastCommand = 'V';
|
|
278
322
|
} else if (upperCmd === 'C') {
|
|
279
323
|
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
280
324
|
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
@@ -282,18 +326,69 @@ export function pathToAbsolute(pathData) {
|
|
|
282
326
|
const y2 = isRelative ? currentY.plus(args[3]) : args[3];
|
|
283
327
|
const x = isRelative ? currentX.plus(args[4]) : args[4];
|
|
284
328
|
const y = isRelative ? currentY.plus(args[5]) : args[5];
|
|
329
|
+
lastControlX = x2; lastControlY = y2;
|
|
330
|
+
currentX = x; currentY = y;
|
|
331
|
+
result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
|
|
332
|
+
lastCommand = 'C';
|
|
333
|
+
} else if (upperCmd === 'S') {
|
|
334
|
+
// Smooth cubic Bezier: 4 args (x2, y2, x, y)
|
|
335
|
+
// First control point is reflection of previous second control point
|
|
336
|
+
let x1, y1;
|
|
337
|
+
if (lastCommand === 'C' || lastCommand === 'S') {
|
|
338
|
+
x1 = currentX.mul(2).minus(lastControlX);
|
|
339
|
+
y1 = currentY.mul(2).minus(lastControlY);
|
|
340
|
+
} else {
|
|
341
|
+
x1 = currentX;
|
|
342
|
+
y1 = currentY;
|
|
343
|
+
}
|
|
344
|
+
const x2 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
345
|
+
const y2 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
346
|
+
const x = isRelative ? currentX.plus(args[2]) : args[2];
|
|
347
|
+
const y = isRelative ? currentY.plus(args[3]) : args[3];
|
|
348
|
+
lastControlX = x2; lastControlY = y2;
|
|
285
349
|
currentX = x; currentY = y;
|
|
286
350
|
result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
|
|
351
|
+
lastCommand = 'S';
|
|
352
|
+
} else if (upperCmd === 'Q') {
|
|
353
|
+
// Quadratic Bezier: 4 args (x1, y1, x, y)
|
|
354
|
+
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
355
|
+
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
356
|
+
const x = isRelative ? currentX.plus(args[2]) : args[2];
|
|
357
|
+
const y = isRelative ? currentY.plus(args[3]) : args[3];
|
|
358
|
+
lastControlX = x1; lastControlY = y1;
|
|
359
|
+
currentX = x; currentY = y;
|
|
360
|
+
result.push({ command: 'Q', args: [x1, y1, x, y] });
|
|
361
|
+
lastCommand = 'Q';
|
|
362
|
+
} else if (upperCmd === 'T') {
|
|
363
|
+
// Smooth quadratic Bezier: 2 args (x, y)
|
|
364
|
+
// Control point is reflection of previous control point
|
|
365
|
+
let x1, y1;
|
|
366
|
+
if (lastCommand === 'Q' || lastCommand === 'T') {
|
|
367
|
+
x1 = currentX.mul(2).minus(lastControlX);
|
|
368
|
+
y1 = currentY.mul(2).minus(lastControlY);
|
|
369
|
+
} else {
|
|
370
|
+
x1 = currentX;
|
|
371
|
+
y1 = currentY;
|
|
372
|
+
}
|
|
373
|
+
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
374
|
+
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
375
|
+
lastControlX = x1; lastControlY = y1;
|
|
376
|
+
currentX = x; currentY = y;
|
|
377
|
+
result.push({ command: 'Q', args: [x1, y1, x, y] });
|
|
378
|
+
lastCommand = 'T';
|
|
287
379
|
} else if (upperCmd === 'A') {
|
|
288
380
|
const x = isRelative ? currentX.plus(args[5]) : args[5];
|
|
289
381
|
const y = isRelative ? currentY.plus(args[6]) : args[6];
|
|
290
382
|
currentX = x; currentY = y;
|
|
291
383
|
result.push({ command: 'A', args: [args[0], args[1], args[2], args[3], args[4], x, y] });
|
|
384
|
+
lastCommand = 'A';
|
|
292
385
|
} else if (upperCmd === 'Z') {
|
|
293
386
|
currentX = subpathStartX; currentY = subpathStartY;
|
|
294
387
|
result.push({ command: 'Z', args: [] });
|
|
388
|
+
lastCommand = 'Z';
|
|
295
389
|
} else {
|
|
296
390
|
result.push({ command, args });
|
|
391
|
+
lastCommand = command;
|
|
297
392
|
}
|
|
298
393
|
}
|
|
299
394
|
return pathArrayToString(result);
|
|
@@ -461,10 +556,25 @@ export function pathToCubics(pathData) {
|
|
|
461
556
|
return pathArrayToString(result);
|
|
462
557
|
}
|
|
463
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Strip CSS units from a value string (e.g., "100px" -> 100, "50%" -> 50, "2em" -> 2)
|
|
561
|
+
* Returns the numeric value or 0 if parsing fails.
|
|
562
|
+
* @param {string|number|Decimal} val - Value to strip units from
|
|
563
|
+
* @returns {number} Numeric value without units
|
|
564
|
+
*/
|
|
565
|
+
function stripUnits(val) {
|
|
566
|
+
if (typeof val === 'string') {
|
|
567
|
+
return parseFloat(val) || 0;
|
|
568
|
+
}
|
|
569
|
+
return val;
|
|
570
|
+
}
|
|
571
|
+
|
|
464
572
|
export function convertElementToPath(element, precision = 6) {
|
|
465
573
|
const getAttr = (name, defaultValue = 0) => {
|
|
466
|
-
|
|
467
|
-
|
|
574
|
+
const rawValue = element.getAttribute ? element.getAttribute(name) : element[name];
|
|
575
|
+
const value = rawValue !== undefined && rawValue !== null ? rawValue : defaultValue;
|
|
576
|
+
// Strip CSS units before returning (handles px, em, %, etc.)
|
|
577
|
+
return stripUnits(value);
|
|
468
578
|
};
|
|
469
579
|
const tagName = (element.tagName || element.type || '').toLowerCase();
|
|
470
580
|
if (tagName === 'circle') {
|