@emasoft/svg-matrix 1.0.28 → 1.0.30

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -4,13 +4,67 @@
4
4
  * Detects SVG 2.0 features (mesh gradients, hatches) and generates inline
5
5
  * JavaScript polyfills for browser compatibility.
6
6
  *
7
- * Uses the existing mesh-gradient.js math for Coons patch evaluation.
7
+ * Uses the Inkscape mesh.js polyfill by Tavmjong Bah for mesh gradient support.
8
+ * The mesh polyfill is licensed under GPLv3 - see src/vendor/inkscape-mesh-polyfill.js
9
+ * Hatch polyfills use a simplified MIT-licensed implementation.
10
+ *
8
11
  * All polyfills are embedded inline in the SVG for self-contained output.
9
12
  *
10
13
  * @module svg2-polyfills
11
14
  */
12
15
 
13
16
  import { SVGElement } from './svg-parser.js';
17
+ import { readFileSync } from 'fs';
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, join } from 'path';
20
+
21
+ // Load Inkscape polyfills at module initialization (minified + full versions)
22
+ let INKSCAPE_MESH_POLYFILL_MIN = '';
23
+ let INKSCAPE_MESH_POLYFILL_FULL = '';
24
+ let INKSCAPE_HATCH_POLYFILL_MIN = '';
25
+ let INKSCAPE_HATCH_POLYFILL_FULL = '';
26
+
27
+ try {
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+
31
+ // Load mesh gradient polyfills (GPLv3 by Tavmjong Bah)
32
+ // Minified version (default, ~16KB)
33
+ INKSCAPE_MESH_POLYFILL_MIN = readFileSync(
34
+ join(__dirname, 'vendor', 'inkscape-mesh-polyfill.min.js'),
35
+ 'utf-8'
36
+ );
37
+ // Full version (~35KB, for debugging or when --no-minify-polyfills is used)
38
+ INKSCAPE_MESH_POLYFILL_FULL = readFileSync(
39
+ join(__dirname, 'vendor', 'inkscape-mesh-polyfill.js'),
40
+ 'utf-8'
41
+ );
42
+
43
+ // Load hatch polyfills (CC0/Public Domain by Valentin Ionita)
44
+ // Minified version (default, ~5KB)
45
+ INKSCAPE_HATCH_POLYFILL_MIN = readFileSync(
46
+ join(__dirname, 'vendor', 'inkscape-hatch-polyfill.min.js'),
47
+ 'utf-8'
48
+ );
49
+ // Full version (~10KB, for debugging)
50
+ INKSCAPE_HATCH_POLYFILL_FULL = readFileSync(
51
+ join(__dirname, 'vendor', 'inkscape-hatch-polyfill.js'),
52
+ 'utf-8'
53
+ );
54
+ } catch (e) {
55
+ throw new Error(`Failed to load SVG2 polyfill files from vendor/ directory: ${e.message}. Ensure all vendor files are present.`);
56
+ }
57
+
58
+ // Module-level option for minification (default: true)
59
+ let useMinifiedPolyfills = true;
60
+
61
+ /**
62
+ * Set whether to use minified polyfills.
63
+ * @param {boolean} minify - True to use minified (default), false for full version
64
+ */
65
+ export function setPolyfillMinification(minify) {
66
+ useMinifiedPolyfills = minify;
67
+ }
14
68
 
15
69
  /**
16
70
  * SVG 2.0 features that can be polyfilled
@@ -29,6 +83,8 @@ export const SVG2_FEATURES = {
29
83
  * @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
30
84
  */
31
85
  export function detectSVG2Features(doc) {
86
+ if (!doc) return { meshGradients: [], hatches: [], contextPaint: false, autoStartReverse: false };
87
+
32
88
  const features = {
33
89
  meshGradients: [],
34
90
  hatches: [],
@@ -85,6 +141,8 @@ export function detectSVG2Features(doc) {
85
141
  * @returns {boolean} True if polyfills are needed
86
142
  */
87
143
  export function needsPolyfills(doc) {
144
+ if (!doc) return false;
145
+
88
146
  const features = detectSVG2Features(doc);
89
147
  return features.meshGradients.length > 0 ||
90
148
  features.hatches.length > 0 ||
@@ -94,244 +152,51 @@ export function needsPolyfills(doc) {
94
152
 
95
153
  /**
96
154
  * Generate the mesh gradient polyfill code.
155
+ * Uses the Inkscape mesh.js polyfill by Tavmjong Bah (GPLv3).
97
156
  * This polyfill renders mesh gradients to canvas and uses them as image fills.
98
157
  *
158
+ * Features:
159
+ * - Multi-patch grid support
160
+ * - Bezier curve edge parsing (l, L, c, C commands)
161
+ * - Adaptive tessellation via de Casteljau subdivision
162
+ * - gradientTransform support
163
+ * - Proper shape clipping
164
+ *
99
165
  * @returns {string} JavaScript polyfill code
100
166
  */
101
167
  function generateMeshPolyfillCode() {
102
- return `
103
- // Mesh Gradient Polyfill - SVG 2.0 to Canvas fallback
104
- // Generated by svg-matrix
105
- (function() {
106
- 'use strict';
107
-
108
- // Skip if browser supports mesh gradients natively
109
- if (typeof document.createElementNS('http://www.w3.org/2000/svg', 'meshGradient').x !== 'undefined') {
110
- return;
168
+ // Return minified or full Inkscape mesh.js polyfill based on setting
169
+ const polyfill = useMinifiedPolyfills ? INKSCAPE_MESH_POLYFILL_MIN : INKSCAPE_MESH_POLYFILL_FULL;
170
+ if (polyfill) {
171
+ return polyfill;
111
172
  }
112
-
113
- // Find all mesh gradients
114
- var meshes = document.querySelectorAll('meshGradient, meshgradient');
115
- if (!meshes.length) return;
116
-
117
- // Parse color string to RGBA
118
- function parseColor(str) {
119
- if (!str) return {r: 0, g: 0, b: 0, a: 255};
120
- var m = str.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)/);
121
- if (m) return {r: +m[1], g: +m[2], b: +m[3], a: m[4] ? Math.round(+m[4] * 255) : 255};
122
- if (str[0] === '#') {
123
- var hex = str.slice(1);
124
- if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
125
- return {r: parseInt(hex.slice(0,2), 16), g: parseInt(hex.slice(2,4), 16), b: parseInt(hex.slice(4,6), 16), a: 255};
126
- }
127
- return {r: 0, g: 0, b: 0, a: 255};
128
- }
129
-
130
- // Bilinear color interpolation
131
- function bilinearColor(c00, c10, c01, c11, u, v) {
132
- var mu = 1 - u, mv = 1 - v;
133
- return {
134
- r: Math.round(mu*mv*c00.r + u*mv*c10.r + mu*v*c01.r + u*v*c11.r),
135
- g: Math.round(mu*mv*c00.g + u*mv*c10.g + mu*v*c01.g + u*v*c11.g),
136
- b: Math.round(mu*mv*c00.b + u*mv*c10.b + mu*v*c01.b + u*v*c11.b),
137
- a: Math.round(mu*mv*c00.a + u*mv*c10.a + mu*v*c01.a + u*v*c11.a)
138
- };
139
- }
140
-
141
- // Evaluate cubic Bezier at t
142
- function evalBezier(p0, p1, p2, p3, t) {
143
- var mt = 1 - t, mt2 = mt * mt, mt3 = mt2 * mt;
144
- var t2 = t * t, t3 = t2 * t;
145
- return {
146
- x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
147
- y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
148
- };
149
- }
150
-
151
- // Process each mesh gradient
152
- meshes.forEach(function(mesh) {
153
- var id = mesh.getAttribute('id');
154
- if (!id) return;
155
-
156
- // Create canvas for rasterization
157
- var canvas = document.createElement('canvas');
158
- var ctx = canvas.getContext('2d');
159
-
160
- // Get bounding box from referencing elements
161
- var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
162
- if (!refs.length) return;
163
-
164
- var bbox = refs[0].getBBox();
165
- var size = Math.max(bbox.width, bbox.height, 256);
166
- canvas.width = canvas.height = size;
167
-
168
- // Parse mesh patches
169
- var patches = [];
170
- var rows = mesh.querySelectorAll('meshrow');
171
- rows.forEach(function(row) {
172
- var rowPatches = row.querySelectorAll('meshpatch');
173
- rowPatches.forEach(function(patch) {
174
- var stops = patch.querySelectorAll('stop');
175
- if (stops.length >= 4) {
176
- patches.push({
177
- colors: [
178
- parseColor(stops[0].getAttribute('stop-color') || stops[0].style.stopColor),
179
- parseColor(stops[1].getAttribute('stop-color') || stops[1].style.stopColor),
180
- parseColor(stops[2].getAttribute('stop-color') || stops[2].style.stopColor),
181
- parseColor(stops[3].getAttribute('stop-color') || stops[3].style.stopColor)
182
- ]
183
- });
184
- }
185
- });
186
- });
187
-
188
- // Render patches with bilinear interpolation
189
- if (patches.length > 0) {
190
- var imgData = ctx.createImageData(size, size);
191
- var data = imgData.data;
192
- var patch = patches[0];
193
- for (var y = 0; y < size; y++) {
194
- for (var x = 0; x < size; x++) {
195
- var u = x / (size - 1);
196
- var v = y / (size - 1);
197
- var c = bilinearColor(patch.colors[0], patch.colors[1], patch.colors[2], patch.colors[3], u, v);
198
- var i = (y * size + x) * 4;
199
- data[i] = c.r;
200
- data[i+1] = c.g;
201
- data[i+2] = c.b;
202
- data[i+3] = c.a;
203
- }
204
- }
205
- ctx.putImageData(imgData, 0, 0);
206
- }
207
-
208
- // Create pattern from canvas
209
- var dataUrl = canvas.toDataURL('image/png');
210
- var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
211
- pattern.setAttribute('id', id + '_polyfill');
212
- pattern.setAttribute('patternUnits', 'objectBoundingBox');
213
- pattern.setAttribute('width', '1');
214
- pattern.setAttribute('height', '1');
215
-
216
- var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
217
- img.setAttribute('href', dataUrl);
218
- img.setAttribute('width', '1');
219
- img.setAttribute('height', '1');
220
- img.setAttribute('preserveAspectRatio', 'none');
221
- pattern.appendChild(img);
222
-
223
- // Add pattern to defs
224
- var defs = mesh.closest('svg').querySelector('defs') || (function() {
225
- var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
226
- mesh.closest('svg').insertBefore(d, mesh.closest('svg').firstChild);
227
- return d;
228
- })();
229
- defs.appendChild(pattern);
230
-
231
- // Update references to use polyfill pattern
232
- refs.forEach(function(ref) {
233
- if (ref.getAttribute('fill') === 'url(#' + id + ')') {
234
- ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
235
- }
236
- if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
237
- ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
238
- }
239
- });
240
- });
241
- })();
242
- `;
173
+ // Fallback: return empty string if polyfill couldn't be loaded
174
+ console.warn('svg2-polyfills: Inkscape mesh polyfill not available');
175
+ return '';
243
176
  }
244
177
 
245
178
  /**
246
179
  * Generate the hatch pattern polyfill code.
180
+ * Uses the Inkscape hatch polyfill by Valentin Ionita (CC0/Public Domain).
247
181
  * Converts SVG 2 hatch elements to SVG 1.1 pattern elements.
248
182
  *
183
+ * Features:
184
+ * - Full SVG path command support (M, L, C, S, Q, A, etc.)
185
+ * - Proper coordinate system handling (objectBoundingBox, userSpaceOnUse)
186
+ * - Hatch rotation and transform support
187
+ * - Multiple hatchpath elements with offset values
188
+ *
249
189
  * @returns {string} JavaScript polyfill code
250
190
  */
251
191
  function generateHatchPolyfillCode() {
252
- return `
253
- // Hatch Pattern Polyfill - SVG 2.0 to SVG 1.1 pattern conversion
254
- // Generated by svg-matrix
255
- (function() {
256
- 'use strict';
257
-
258
- // Find all hatch elements
259
- var hatches = document.querySelectorAll('hatch');
260
- if (!hatches.length) return;
261
-
262
- hatches.forEach(function(hatch) {
263
- var id = hatch.getAttribute('id');
264
- if (!id) return;
265
-
266
- // Get hatch properties
267
- var href = hatch.getAttribute('href') || hatch.getAttribute('xlink:href');
268
- var hatchUnits = hatch.getAttribute('hatchUnits') || 'objectBoundingBox';
269
- var hatchContentUnits = hatch.getAttribute('hatchContentUnits') || 'userSpaceOnUse';
270
- var pitch = parseFloat(hatch.getAttribute('pitch')) || 8;
271
- var rotate = parseFloat(hatch.getAttribute('rotate')) || 0;
272
- var transform = hatch.getAttribute('transform') || '';
273
-
274
- // Create equivalent pattern
275
- var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
276
- pattern.setAttribute('id', id + '_polyfill');
277
- pattern.setAttribute('patternUnits', hatchUnits);
278
- pattern.setAttribute('width', pitch);
279
- pattern.setAttribute('height', pitch);
280
-
281
- if (transform || rotate) {
282
- var fullTransform = transform;
283
- if (rotate) fullTransform += ' rotate(' + rotate + ')';
284
- pattern.setAttribute('patternTransform', fullTransform.trim());
285
- }
286
-
287
- // Copy hatchpath children as lines
288
- var hatchpaths = hatch.querySelectorAll('hatchpath, hatchPath');
289
- hatchpaths.forEach(function(hp) {
290
- var d = hp.getAttribute('d');
291
- var strokeColor = hp.getAttribute('stroke') || 'black';
292
- var strokeWidth = hp.getAttribute('stroke-width') || '1';
293
-
294
- var line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
295
- line.setAttribute('d', d || 'M0,0 L' + pitch + ',0');
296
- line.setAttribute('stroke', strokeColor);
297
- line.setAttribute('stroke-width', strokeWidth);
298
- line.setAttribute('fill', 'none');
299
- pattern.appendChild(line);
300
- });
301
-
302
- // If no hatchpaths, create default diagonal line
303
- if (!hatchpaths.length) {
304
- var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
305
- line.setAttribute('x1', '0');
306
- line.setAttribute('y1', '0');
307
- line.setAttribute('x2', pitch);
308
- line.setAttribute('y2', pitch);
309
- line.setAttribute('stroke', 'black');
310
- line.setAttribute('stroke-width', '1');
311
- pattern.appendChild(line);
312
- }
313
-
314
- // Add pattern to defs
315
- var defs = hatch.closest('svg').querySelector('defs') || (function() {
316
- var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
317
- hatch.closest('svg').insertBefore(d, hatch.closest('svg').firstChild);
318
- return d;
319
- })();
320
- defs.appendChild(pattern);
321
-
322
- // Update references
323
- var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
324
- refs.forEach(function(ref) {
325
- if (ref.getAttribute('fill') === 'url(#' + id + ')') {
326
- ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
327
- }
328
- if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
329
- ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
330
- }
331
- });
332
- });
333
- })();
334
- `;
192
+ // Return minified or full Inkscape hatch polyfill based on setting
193
+ const polyfill = useMinifiedPolyfills ? INKSCAPE_HATCH_POLYFILL_MIN : INKSCAPE_HATCH_POLYFILL_FULL;
194
+ if (polyfill) {
195
+ return polyfill;
196
+ }
197
+ // Fallback: return empty string if polyfill couldn't be loaded
198
+ console.warn('svg2-polyfills: Inkscape hatch polyfill not available');
199
+ return '';
335
200
  }
336
201
 
337
202
  /**
@@ -370,6 +235,8 @@ export function generatePolyfillScript(features) {
370
235
  * @returns {Object} The document (modified in place)
371
236
  */
372
237
  export function injectPolyfills(doc, options = {}) {
238
+ if (!doc) return doc;
239
+
373
240
  // Use pre-detected features if provided (for when pipeline has removed SVG2 elements)
374
241
  const features = options.features || detectSVG2Features(doc);
375
242
 
@@ -418,6 +285,8 @@ export function injectPolyfills(doc, options = {}) {
418
285
  * @returns {Object} The document (modified in place)
419
286
  */
420
287
  export function removePolyfills(doc) {
288
+ if (!doc) return doc;
289
+
421
290
  const walk = (el) => {
422
291
  if (!el || !el.children) return;
423
292
 
@@ -31,20 +31,20 @@
31
31
  * @module transform-decomposition
32
32
  */
33
33
 
34
- import Decimal from 'decimal.js';
35
- import { Matrix } from './matrix.js';
34
+ import Decimal from "decimal.js";
35
+ import { Matrix } from "./matrix.js";
36
36
 
37
37
  // Set high precision for all calculations
38
38
  Decimal.set({ precision: 80 });
39
39
 
40
40
  // Helper to convert to Decimal
41
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
41
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
42
42
 
43
43
  // Near-zero threshold for comparisons
44
- const EPSILON = new Decimal('1e-40');
44
+ const EPSILON = new Decimal("1e-40");
45
45
 
46
46
  // Verification tolerance (larger than EPSILON for practical use)
47
- const VERIFICATION_TOLERANCE = new Decimal('1e-30');
47
+ const VERIFICATION_TOLERANCE = new Decimal("1e-30");
48
48
 
49
49
  // ============================================================================
50
50
  // Matrix Utilities
@@ -68,7 +68,7 @@ export function translationMatrix(tx, ty) {
68
68
  return Matrix.from([
69
69
  [1, 0, D(tx)],
70
70
  [0, 1, D(ty)],
71
- [0, 0, 1]
71
+ [0, 0, 1],
72
72
  ]);
73
73
  }
74
74
 
@@ -84,7 +84,7 @@ export function rotationMatrix(angle) {
84
84
  return Matrix.from([
85
85
  [cos, sin.neg(), 0],
86
86
  [sin, cos, 0],
87
- [0, 0, 1]
87
+ [0, 0, 1],
88
88
  ]);
89
89
  }
90
90
 
@@ -98,7 +98,7 @@ export function scaleMatrix(sx, sy) {
98
98
  return Matrix.from([
99
99
  [D(sx), 0, 0],
100
100
  [0, D(sy), 0],
101
- [0, 0, 1]
101
+ [0, 0, 1],
102
102
  ]);
103
103
  }
104
104
 
@@ -112,7 +112,7 @@ export function skewXMatrix(angle) {
112
112
  return Matrix.from([
113
113
  [1, tan, 0],
114
114
  [0, 1, 0],
115
- [0, 0, 1]
115
+ [0, 0, 1],
116
116
  ]);
117
117
  }
118
118
 
@@ -126,7 +126,7 @@ export function skewYMatrix(angle) {
126
126
  return Matrix.from([
127
127
  [1, 0, 0],
128
128
  [tan, 1, 0],
129
- [0, 0, 1]
129
+ [0, 0, 1],
130
130
  ]);
131
131
  }
132
132
 
@@ -141,7 +141,7 @@ export function extractLinearPart(matrix) {
141
141
  a: data[0][0],
142
142
  b: data[1][0],
143
143
  c: data[0][1],
144
- d: data[1][1]
144
+ d: data[1][1],
145
145
  };
146
146
  }
147
147
 
@@ -154,7 +154,7 @@ export function extractTranslation(matrix) {
154
154
  const data = matrix.data;
155
155
  return {
156
156
  tx: data[0][2],
157
- ty: data[1][2]
157
+ ty: data[1][2],
158
158
  };
159
159
  }
160
160
 
@@ -209,12 +209,12 @@ export function decomposeMatrix(matrix) {
209
209
  skewX: D(0),
210
210
  skewY: D(0),
211
211
  verified: false,
212
- verificationError: D('Infinity'),
213
- singular: true // Flag to indicate singular matrix
212
+ verificationError: D("Infinity"),
213
+ singular: true, // Flag to indicate singular matrix
214
214
  };
215
215
  }
216
216
 
217
- let scaleY = det.div(scaleX);
217
+ const scaleY = det.div(scaleX);
218
218
 
219
219
  // Handle reflection (negative determinant)
220
220
  // We put the reflection in scaleX
@@ -250,9 +250,9 @@ export function decomposeMatrix(matrix) {
250
250
 
251
251
  // Rotate back to get the scale+skew matrix
252
252
  const aPrime = a.mul(cosTheta).plus(b.mul(sinTheta));
253
- const bPrime = a.neg().mul(sinTheta).plus(b.mul(cosTheta));
253
+ const _bPrime = a.neg().mul(sinTheta).plus(b.mul(cosTheta));
254
254
  const cPrime = c.mul(cosTheta).plus(d.mul(sinTheta));
255
- const dPrime = c.neg().mul(sinTheta).plus(d.mul(cosTheta));
255
+ const _dPrime = c.neg().mul(sinTheta).plus(d.mul(cosTheta));
256
256
 
257
257
  // Now [aPrime cPrime] should be [scaleX, scaleX*tan(skewX)]
258
258
  // [bPrime dPrime] [0, scaleY ]
@@ -291,7 +291,7 @@ export function decomposeMatrix(matrix) {
291
291
  scaleX,
292
292
  scaleY,
293
293
  skewX,
294
- skewY: D(0)
294
+ skewY: D(0),
295
295
  });
296
296
 
297
297
  const maxError = matrixMaxDifference(matrix, recomposed);
@@ -306,7 +306,7 @@ export function decomposeMatrix(matrix) {
306
306
  skewX,
307
307
  skewY,
308
308
  verified,
309
- maxError
309
+ maxError,
310
310
  };
311
311
  }
312
312
 
@@ -352,7 +352,7 @@ export function decomposeMatrixNoSkew(matrix) {
352
352
  translateY: ty,
353
353
  rotation,
354
354
  scaleX,
355
- scaleY
355
+ scaleY,
356
356
  });
357
357
 
358
358
  const maxError = matrixMaxDifference(matrix, recomposed);
@@ -365,7 +365,7 @@ export function decomposeMatrixNoSkew(matrix) {
365
365
  scaleX,
366
366
  scaleY,
367
367
  verified,
368
- maxError
368
+ maxError,
369
369
  };
370
370
  }
371
371
 
@@ -434,7 +434,7 @@ export function composeTransform(components) {
434
434
  scaleX,
435
435
  scaleY,
436
436
  skewX,
437
- skewY = 0
437
+ skewY = 0,
438
438
  } = components;
439
439
 
440
440
  // Build matrices
@@ -521,7 +521,7 @@ export function verifyDecomposition(original, decomposition) {
521
521
  const maxError = matrixMaxDifference(original, recomposed);
522
522
  return {
523
523
  verified: maxError.lessThan(VERIFICATION_TOLERANCE),
524
- maxError
524
+ maxError,
525
525
  };
526
526
  }
527
527
 
@@ -549,7 +549,7 @@ export function matrixFromSVGValues(a, b, c, d, e, f) {
549
549
  return Matrix.from([
550
550
  [D(a), D(c), D(e)],
551
551
  [D(b), D(d), D(f)],
552
- [0, 0, 1]
552
+ [0, 0, 1],
553
553
  ]);
554
554
  }
555
555
 
@@ -568,7 +568,7 @@ export function matrixToSVGValues(matrix, precision = 6) {
568
568
  c: data[0][1].toFixed(precision),
569
569
  d: data[1][1].toFixed(precision),
570
570
  e: data[0][2].toFixed(precision),
571
- f: data[1][2].toFixed(precision)
571
+ f: data[1][2].toFixed(precision),
572
572
  };
573
573
  }
574
574
 
@@ -580,13 +580,19 @@ export function matrixToSVGValues(matrix, precision = 6) {
580
580
  * @returns {string} SVG transform string
581
581
  */
582
582
  export function decompositionToSVGString(decomposition, precision = 6) {
583
- const { translateX, translateY, rotation, scaleX, scaleY, skewX, skewY } = decomposition;
583
+ const { translateX, translateY, rotation, scaleX, scaleY, skewX, skewY } =
584
+ decomposition;
584
585
 
585
586
  const parts = [];
586
587
 
587
588
  // Add translate if non-zero
588
- if (!D(translateX).abs().lessThan(EPSILON) || !D(translateY).abs().lessThan(EPSILON)) {
589
- parts.push(`translate(${D(translateX).toFixed(precision)}, ${D(translateY).toFixed(precision)})`);
589
+ if (
590
+ !D(translateX).abs().lessThan(EPSILON) ||
591
+ !D(translateY).abs().lessThan(EPSILON)
592
+ ) {
593
+ parts.push(
594
+ `translate(${D(translateX).toFixed(precision)}, ${D(translateY).toFixed(precision)})`,
595
+ );
590
596
  }
591
597
 
592
598
  // Add rotate if non-zero
@@ -616,11 +622,13 @@ export function decompositionToSVGString(decomposition, precision = 6) {
616
622
  if (D(scaleX).minus(D(scaleY)).abs().lessThan(EPSILON)) {
617
623
  parts.push(`scale(${D(scaleX).toFixed(precision)})`);
618
624
  } else {
619
- parts.push(`scale(${D(scaleX).toFixed(precision)}, ${D(scaleY).toFixed(precision)})`);
625
+ parts.push(
626
+ `scale(${D(scaleX).toFixed(precision)}, ${D(scaleY).toFixed(precision)})`,
627
+ );
620
628
  }
621
629
  }
622
630
 
623
- return parts.length > 0 ? parts.join(' ') : '';
631
+ return parts.length > 0 ? parts.join(" ") : "";
624
632
  }
625
633
 
626
634
  /**
@@ -636,7 +644,7 @@ export function matrixToMinimalSVGTransform(matrix, precision = 6) {
636
644
  // Check if identity
637
645
  const identity = Matrix.identity(3);
638
646
  if (matricesEqual(matrix, identity, EPSILON)) {
639
- return { transform: '', isIdentity: true, verified: true };
647
+ return { transform: "", isIdentity: true, verified: true };
640
648
  }
641
649
 
642
650
  // Decompose
@@ -648,7 +656,7 @@ export function matrixToMinimalSVGTransform(matrix, precision = 6) {
648
656
  return {
649
657
  transform,
650
658
  isIdentity: false,
651
- verified: decomposition.verified
659
+ verified: decomposition.verified,
652
660
  };
653
661
  }
654
662
 
@@ -675,7 +683,7 @@ export function isPureTranslation(matrix) {
675
683
  return {
676
684
  isTranslation: isIdentityLinear,
677
685
  tx,
678
- ty
686
+ ty,
679
687
  };
680
688
  }
681
689
 
@@ -700,7 +708,8 @@ export function isPureRotation(matrix) {
700
708
  // Check unit columns: a² + b² = 1, c² + d² = 1
701
709
  const col1Norm = a.mul(a).plus(b.mul(b));
702
710
  const col2Norm = c.mul(c).plus(d.mul(d));
703
- const unitNorm = col1Norm.minus(1).abs().lessThan(EPSILON) &&
711
+ const unitNorm =
712
+ col1Norm.minus(1).abs().lessThan(EPSILON) &&
704
713
  col2Norm.minus(1).abs().lessThan(EPSILON);
705
714
 
706
715
  // Check determinant = 1 (no reflection)
@@ -741,7 +750,7 @@ export function isPureScale(matrix) {
741
750
  isScale: true,
742
751
  scaleX: a,
743
752
  scaleY: d,
744
- isUniform
753
+ isUniform,
745
754
  };
746
755
  }
747
756
 
@@ -760,11 +769,7 @@ export function isIdentityMatrix(matrix) {
760
769
  // Exports
761
770
  // ============================================================================
762
771
 
763
- export {
764
- EPSILON,
765
- VERIFICATION_TOLERANCE,
766
- D
767
- };
772
+ export { EPSILON, VERIFICATION_TOLERANCE, D };
768
773
 
769
774
  export default {
770
775
  // Matrix utilities
@@ -806,5 +811,5 @@ export default {
806
811
 
807
812
  // Constants
808
813
  EPSILON,
809
- VERIFICATION_TOLERANCE
814
+ VERIFICATION_TOLERANCE,
810
815
  };