@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/mask-resolver.js
CHANGED
|
@@ -131,6 +131,9 @@ export const MaskType = {
|
|
|
131
131
|
* console.log(`Number of shapes: ${maskData.children.length}`);
|
|
132
132
|
*/
|
|
133
133
|
export function parseMaskElement(maskElement) {
|
|
134
|
+
if (!maskElement) throw new Error('parseMaskElement: maskElement is required');
|
|
135
|
+
if (!maskElement.getAttribute) throw new Error('parseMaskElement: maskElement must be a DOM element');
|
|
136
|
+
|
|
134
137
|
const data = {
|
|
135
138
|
id: maskElement.getAttribute("id") || "",
|
|
136
139
|
maskUnits: maskElement.getAttribute("maskUnits") || "objectBoundingBox",
|
|
@@ -148,32 +151,42 @@ export function parseMaskElement(maskElement) {
|
|
|
148
151
|
children: [],
|
|
149
152
|
};
|
|
150
153
|
|
|
154
|
+
// Helper to safely parse float with NaN check
|
|
155
|
+
const safeParseFloat = (value, defaultValue) => {
|
|
156
|
+
const parsed = parseFloat(value);
|
|
157
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
158
|
+
};
|
|
159
|
+
|
|
151
160
|
// Set defaults based on maskUnits
|
|
152
161
|
if (data.maskUnits === "objectBoundingBox") {
|
|
153
|
-
data.x = data.x !== null ?
|
|
154
|
-
data.y = data.y !== null ?
|
|
155
|
-
data.width =
|
|
156
|
-
|
|
157
|
-
data.height =
|
|
158
|
-
data.height !== null ? parseFloat(data.height) : DEFAULT_MASK_HEIGHT;
|
|
162
|
+
data.x = data.x !== null ? safeParseFloat(data.x, DEFAULT_MASK_X) : DEFAULT_MASK_X;
|
|
163
|
+
data.y = data.y !== null ? safeParseFloat(data.y, DEFAULT_MASK_Y) : DEFAULT_MASK_Y;
|
|
164
|
+
data.width = data.width !== null ? safeParseFloat(data.width, DEFAULT_MASK_WIDTH) : DEFAULT_MASK_WIDTH;
|
|
165
|
+
data.height = data.height !== null ? safeParseFloat(data.height, DEFAULT_MASK_HEIGHT) : DEFAULT_MASK_HEIGHT;
|
|
159
166
|
} else {
|
|
160
|
-
data.x = data.x !== null ?
|
|
161
|
-
data.y = data.y !== null ?
|
|
162
|
-
data.width = data.width !== null ?
|
|
163
|
-
data.height = data.height !== null ?
|
|
167
|
+
data.x = data.x !== null ? safeParseFloat(data.x, null) : null;
|
|
168
|
+
data.y = data.y !== null ? safeParseFloat(data.y, null) : null;
|
|
169
|
+
data.width = data.width !== null ? safeParseFloat(data.width, null) : null;
|
|
170
|
+
data.height = data.height !== null ? safeParseFloat(data.height, null) : null;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// Parse child elements
|
|
174
|
+
if (!maskElement.children) return data;
|
|
175
|
+
|
|
167
176
|
for (const child of maskElement.children) {
|
|
177
|
+
if (!child.tagName) continue;
|
|
178
|
+
|
|
168
179
|
const tagName = child.tagName.toLowerCase();
|
|
169
180
|
const childData = {
|
|
170
181
|
type: tagName,
|
|
171
182
|
fill: child.getAttribute("fill") || child.style?.fill || "black",
|
|
172
|
-
fillOpacity:
|
|
183
|
+
fillOpacity: safeParseFloat(
|
|
173
184
|
child.getAttribute("fill-opacity") || child.style?.fillOpacity || "1",
|
|
185
|
+
1
|
|
174
186
|
),
|
|
175
|
-
opacity:
|
|
187
|
+
opacity: safeParseFloat(
|
|
176
188
|
child.getAttribute("opacity") || child.style?.opacity || "1",
|
|
189
|
+
1
|
|
177
190
|
),
|
|
178
191
|
transform: child.getAttribute("transform") || null,
|
|
179
192
|
};
|
|
@@ -181,23 +194,23 @@ export function parseMaskElement(maskElement) {
|
|
|
181
194
|
// Parse shape-specific attributes
|
|
182
195
|
switch (tagName) {
|
|
183
196
|
case "rect":
|
|
184
|
-
childData.x =
|
|
185
|
-
childData.y =
|
|
186
|
-
childData.width =
|
|
187
|
-
childData.height =
|
|
188
|
-
childData.rx =
|
|
189
|
-
childData.ry =
|
|
197
|
+
childData.x = safeParseFloat(child.getAttribute("x") || "0", 0);
|
|
198
|
+
childData.y = safeParseFloat(child.getAttribute("y") || "0", 0);
|
|
199
|
+
childData.width = safeParseFloat(child.getAttribute("width") || "0", 0);
|
|
200
|
+
childData.height = safeParseFloat(child.getAttribute("height") || "0", 0);
|
|
201
|
+
childData.rx = safeParseFloat(child.getAttribute("rx") || "0", 0);
|
|
202
|
+
childData.ry = safeParseFloat(child.getAttribute("ry") || "0", 0);
|
|
190
203
|
break;
|
|
191
204
|
case "circle":
|
|
192
|
-
childData.cx =
|
|
193
|
-
childData.cy =
|
|
194
|
-
childData.r =
|
|
205
|
+
childData.cx = safeParseFloat(child.getAttribute("cx") || "0", 0);
|
|
206
|
+
childData.cy = safeParseFloat(child.getAttribute("cy") || "0", 0);
|
|
207
|
+
childData.r = safeParseFloat(child.getAttribute("r") || "0", 0);
|
|
195
208
|
break;
|
|
196
209
|
case "ellipse":
|
|
197
|
-
childData.cx =
|
|
198
|
-
childData.cy =
|
|
199
|
-
childData.rx =
|
|
200
|
-
childData.ry =
|
|
210
|
+
childData.cx = safeParseFloat(child.getAttribute("cx") || "0", 0);
|
|
211
|
+
childData.cy = safeParseFloat(child.getAttribute("cy") || "0", 0);
|
|
212
|
+
childData.rx = safeParseFloat(child.getAttribute("rx") || "0", 0);
|
|
213
|
+
childData.ry = safeParseFloat(child.getAttribute("ry") || "0", 0);
|
|
201
214
|
break;
|
|
202
215
|
case "path":
|
|
203
216
|
childData.d = child.getAttribute("d") || "";
|
|
@@ -211,14 +224,20 @@ export function parseMaskElement(maskElement) {
|
|
|
211
224
|
case "g":
|
|
212
225
|
// Recursively parse group children
|
|
213
226
|
childData.children = [];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
if (child.children) {
|
|
228
|
+
for (const gc of child.children) {
|
|
229
|
+
if (gc.tagName) {
|
|
230
|
+
childData.children.push({
|
|
231
|
+
type: gc.tagName.toLowerCase(),
|
|
232
|
+
fill: gc.getAttribute("fill") || "inherit",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
220
236
|
}
|
|
221
237
|
break;
|
|
238
|
+
default:
|
|
239
|
+
// Unknown element type - childData will have basic properties only (type, fill, opacity, transform)
|
|
240
|
+
break;
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
data.children.push(childData);
|
|
@@ -273,6 +292,20 @@ export function parseMaskElement(maskElement) {
|
|
|
273
292
|
* // region.y = 50 (absolute)
|
|
274
293
|
*/
|
|
275
294
|
export function getMaskRegion(maskData, targetBBox) {
|
|
295
|
+
if (!maskData) throw new Error('getMaskRegion: maskData is required');
|
|
296
|
+
if (!targetBBox) throw new Error('getMaskRegion: targetBBox is required');
|
|
297
|
+
if (typeof targetBBox.x !== 'number' || typeof targetBBox.y !== 'number' ||
|
|
298
|
+
typeof targetBBox.width !== 'number' || typeof targetBBox.height !== 'number') {
|
|
299
|
+
throw new Error('getMaskRegion: targetBBox must have numeric x, y, width, height properties');
|
|
300
|
+
}
|
|
301
|
+
if (targetBBox.width <= 0 || targetBBox.height <= 0) {
|
|
302
|
+
throw new Error('getMaskRegion: targetBBox width and height must be positive');
|
|
303
|
+
}
|
|
304
|
+
if (!maskData.maskUnits) throw new Error('getMaskRegion: maskData.maskUnits is required');
|
|
305
|
+
if (maskData.x === undefined || maskData.y === undefined || maskData.width === undefined || maskData.height === undefined) {
|
|
306
|
+
throw new Error('getMaskRegion: maskData must have x, y, width, height properties');
|
|
307
|
+
}
|
|
308
|
+
|
|
276
309
|
if (maskData.maskUnits === "objectBoundingBox") {
|
|
277
310
|
// Coordinates are relative to target bounding box
|
|
278
311
|
return {
|
|
@@ -340,6 +373,17 @@ export function maskChildToPolygon(
|
|
|
340
373
|
contentUnits,
|
|
341
374
|
samples = 20,
|
|
342
375
|
) {
|
|
376
|
+
if (!child) throw new Error('maskChildToPolygon: child is required');
|
|
377
|
+
if (!targetBBox) throw new Error('maskChildToPolygon: targetBBox is required');
|
|
378
|
+
if (!contentUnits) throw new Error('maskChildToPolygon: contentUnits is required');
|
|
379
|
+
if (typeof samples !== 'number' || samples <= 0) {
|
|
380
|
+
throw new Error('maskChildToPolygon: samples must be a positive number');
|
|
381
|
+
}
|
|
382
|
+
if (typeof targetBBox.x !== 'number' || typeof targetBBox.y !== 'number' ||
|
|
383
|
+
typeof targetBBox.width !== 'number' || typeof targetBBox.height !== 'number') {
|
|
384
|
+
throw new Error('maskChildToPolygon: targetBBox must have numeric x, y, width, height properties');
|
|
385
|
+
}
|
|
386
|
+
|
|
343
387
|
// Create element-like object for ClipPathResolver
|
|
344
388
|
const element = {
|
|
345
389
|
type: child.type,
|
|
@@ -350,8 +394,16 @@ export function maskChildToPolygon(
|
|
|
350
394
|
// Get polygon using ClipPathResolver
|
|
351
395
|
let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
|
|
352
396
|
|
|
397
|
+
// Validate polygon is an array
|
|
398
|
+
if (!Array.isArray(polygon)) {
|
|
399
|
+
return []; // Return empty array if polygon conversion failed
|
|
400
|
+
}
|
|
401
|
+
|
|
353
402
|
// Apply objectBoundingBox scaling if needed
|
|
354
403
|
if (contentUnits === "objectBoundingBox" && polygon.length > 0) {
|
|
404
|
+
if (targetBBox.width === 0 || targetBBox.height === 0) {
|
|
405
|
+
return []; // Cannot scale to zero-size bbox
|
|
406
|
+
}
|
|
355
407
|
polygon = polygon.map((p) => ({
|
|
356
408
|
x: D(targetBBox.x).plus(p.x.mul(targetBBox.width)),
|
|
357
409
|
y: D(targetBBox.y).plus(p.y.mul(targetBBox.height)),
|
|
@@ -412,13 +464,23 @@ export function colorToLuminance(colorStr) {
|
|
|
412
464
|
return 0;
|
|
413
465
|
}
|
|
414
466
|
|
|
467
|
+
if (typeof colorStr !== 'string') {
|
|
468
|
+
return 1; // Default to opaque if not a string
|
|
469
|
+
}
|
|
470
|
+
|
|
415
471
|
// Parse RGB values
|
|
416
472
|
const rgbMatch = colorStr.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
417
473
|
if (rgbMatch) {
|
|
418
|
-
const r = parseInt(rgbMatch[1], 10)
|
|
419
|
-
const g = parseInt(rgbMatch[2], 10)
|
|
420
|
-
const b = parseInt(rgbMatch[3], 10)
|
|
421
|
-
|
|
474
|
+
const r = parseInt(rgbMatch[1], 10);
|
|
475
|
+
const g = parseInt(rgbMatch[2], 10);
|
|
476
|
+
const b = parseInt(rgbMatch[3], 10);
|
|
477
|
+
// Validate parsed values and clamp to 0-255 range
|
|
478
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return 1;
|
|
479
|
+
const rClamped = Math.max(0, Math.min(255, r)) / 255;
|
|
480
|
+
const gClamped = Math.max(0, Math.min(255, g)) / 255;
|
|
481
|
+
const bClamped = Math.max(0, Math.min(255, b)) / 255;
|
|
482
|
+
const luminance = 0.2126 * rClamped + 0.7152 * gClamped + 0.0722 * bClamped;
|
|
483
|
+
return isNaN(luminance) ? 1 : luminance;
|
|
422
484
|
}
|
|
423
485
|
|
|
424
486
|
// Parse hex colors
|
|
@@ -428,10 +490,13 @@ export function colorToLuminance(colorStr) {
|
|
|
428
490
|
if (hex.length === 3) {
|
|
429
491
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
430
492
|
}
|
|
431
|
-
const r = parseInt(hex.slice(0, 2), 16)
|
|
432
|
-
const g = parseInt(hex.slice(2, 4), 16)
|
|
433
|
-
const b = parseInt(hex.slice(4, 6), 16)
|
|
434
|
-
|
|
493
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
494
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
495
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
496
|
+
// Validate parsed values
|
|
497
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return 1;
|
|
498
|
+
const luminance = 0.2126 * (r / 255) + 0.7152 * (g / 255) + 0.0722 * (b / 255);
|
|
499
|
+
return isNaN(luminance) ? 1 : luminance;
|
|
435
500
|
}
|
|
436
501
|
|
|
437
502
|
// Named colors (common ones)
|
|
@@ -499,8 +564,14 @@ export function colorToLuminance(colorStr) {
|
|
|
499
564
|
* // Returns: 1.0 × 1.0 × 0.0 = 0.0 (black = fully transparent in luminance mask)
|
|
500
565
|
*/
|
|
501
566
|
export function getMaskChildOpacity(child, maskType) {
|
|
502
|
-
|
|
503
|
-
|
|
567
|
+
if (!child) throw new Error('getMaskChildOpacity: child is required');
|
|
568
|
+
if (!maskType) throw new Error('getMaskChildOpacity: maskType is required');
|
|
569
|
+
if (maskType !== MaskType.LUMINANCE && maskType !== MaskType.ALPHA) {
|
|
570
|
+
throw new Error('getMaskChildOpacity: maskType must be "luminance" or "alpha"');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const fillOpacity = typeof child.fillOpacity === 'number' && !isNaN(child.fillOpacity) ? child.fillOpacity : 1;
|
|
574
|
+
const opacity = typeof child.opacity === 'number' && !isNaN(child.opacity) ? child.opacity : 1;
|
|
504
575
|
const baseOpacity = fillOpacity * opacity;
|
|
505
576
|
|
|
506
577
|
if (maskType === MaskType.ALPHA) {
|
|
@@ -509,7 +580,8 @@ export function getMaskChildOpacity(child, maskType) {
|
|
|
509
580
|
|
|
510
581
|
// Luminance mask: multiply by luminance of fill color
|
|
511
582
|
const luminance = colorToLuminance(child.fill);
|
|
512
|
-
|
|
583
|
+
const result = baseOpacity * luminance;
|
|
584
|
+
return isNaN(result) ? 0 : result;
|
|
513
585
|
}
|
|
514
586
|
|
|
515
587
|
/**
|
|
@@ -554,11 +626,21 @@ export function getMaskChildOpacity(child, maskType) {
|
|
|
554
626
|
* });
|
|
555
627
|
*/
|
|
556
628
|
export function resolveMask(maskData, targetBBox, options = {}) {
|
|
629
|
+
if (!maskData) throw new Error('resolveMask: maskData is required');
|
|
630
|
+
if (!targetBBox) throw new Error('resolveMask: targetBBox is required');
|
|
631
|
+
if (!Array.isArray(maskData.children)) throw new Error('resolveMask: maskData.children must be an array');
|
|
632
|
+
|
|
557
633
|
const { samples = 20 } = options;
|
|
634
|
+
if (typeof samples !== 'number' || samples <= 0) {
|
|
635
|
+
throw new Error('resolveMask: samples must be a positive number');
|
|
636
|
+
}
|
|
637
|
+
|
|
558
638
|
const maskType = maskData.maskType || MaskType.LUMINANCE;
|
|
559
639
|
const result = [];
|
|
560
640
|
|
|
561
641
|
for (const child of maskData.children) {
|
|
642
|
+
if (!child) continue; // Skip null/undefined children
|
|
643
|
+
|
|
562
644
|
const polygon = maskChildToPolygon(
|
|
563
645
|
child,
|
|
564
646
|
targetBBox,
|
|
@@ -630,11 +712,18 @@ export function resolveMask(maskData, targetBBox, options = {}) {
|
|
|
630
712
|
* });
|
|
631
713
|
*/
|
|
632
714
|
export function applyMask(targetPolygon, maskData, targetBBox, options = {}) {
|
|
715
|
+
if (!targetPolygon) throw new Error('applyMask: targetPolygon is required');
|
|
716
|
+
if (!Array.isArray(targetPolygon)) throw new Error('applyMask: targetPolygon must be an array');
|
|
717
|
+
if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
|
|
718
|
+
if (!maskData) throw new Error('applyMask: maskData is required');
|
|
719
|
+
if (!targetBBox) throw new Error('applyMask: targetBBox is required');
|
|
720
|
+
|
|
633
721
|
const maskRegions = resolveMask(maskData, targetBBox, options);
|
|
634
722
|
const result = [];
|
|
635
723
|
|
|
636
724
|
for (const { polygon: maskPoly, opacity } of maskRegions) {
|
|
637
725
|
if (opacity <= 0) continue;
|
|
726
|
+
if (!maskPoly || maskPoly.length < 3) continue; // Skip invalid mask polygons
|
|
638
727
|
|
|
639
728
|
const intersection = PolygonClip.polygonIntersection(
|
|
640
729
|
targetPolygon,
|
|
@@ -705,13 +794,19 @@ export function maskToClipPath(
|
|
|
705
794
|
opacityThreshold = 0.5,
|
|
706
795
|
options = {},
|
|
707
796
|
) {
|
|
797
|
+
if (!maskData) throw new Error('maskToClipPath: maskData is required');
|
|
798
|
+
if (!targetBBox) throw new Error('maskToClipPath: targetBBox is required');
|
|
799
|
+
if (typeof opacityThreshold !== 'number' || opacityThreshold < 0 || opacityThreshold > 1) {
|
|
800
|
+
throw new Error('maskToClipPath: opacityThreshold must be a number between 0 and 1');
|
|
801
|
+
}
|
|
802
|
+
|
|
708
803
|
const maskRegions = resolveMask(maskData, targetBBox, options);
|
|
709
804
|
|
|
710
805
|
// Union all regions above threshold
|
|
711
806
|
let result = [];
|
|
712
807
|
|
|
713
808
|
for (const { polygon, opacity } of maskRegions) {
|
|
714
|
-
if (opacity >= opacityThreshold && polygon.length >= 3) {
|
|
809
|
+
if (opacity >= opacityThreshold && polygon && polygon.length >= 3) {
|
|
715
810
|
if (result.length === 0) {
|
|
716
811
|
result = polygon;
|
|
717
812
|
} else {
|
|
@@ -767,6 +862,9 @@ export function maskToClipPath(
|
|
|
767
862
|
* `;
|
|
768
863
|
*/
|
|
769
864
|
export function maskToPathData(maskData, targetBBox, options = {}) {
|
|
865
|
+
if (!maskData) throw new Error('maskToPathData: maskData is required');
|
|
866
|
+
if (!targetBBox) throw new Error('maskToPathData: targetBBox is required');
|
|
867
|
+
|
|
770
868
|
const polygon = maskToClipPath(maskData, targetBBox, 0.5, options);
|
|
771
869
|
|
|
772
870
|
if (polygon.length < 3) return "";
|
|
@@ -774,8 +872,12 @@ export function maskToPathData(maskData, targetBBox, options = {}) {
|
|
|
774
872
|
let d = "";
|
|
775
873
|
for (let i = 0; i < polygon.length; i++) {
|
|
776
874
|
const p = polygon[i];
|
|
777
|
-
|
|
778
|
-
const
|
|
875
|
+
if (!p || p.x === undefined || p.y === undefined) continue; // Skip invalid points
|
|
876
|
+
const xNum = Number(p.x);
|
|
877
|
+
const yNum = Number(p.y);
|
|
878
|
+
if (isNaN(xNum) || isNaN(yNum) || !isFinite(xNum) || !isFinite(yNum)) continue; // Skip NaN/Infinity
|
|
879
|
+
const x = xNum.toFixed(6);
|
|
880
|
+
const y = yNum.toFixed(6);
|
|
779
881
|
d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
|
780
882
|
}
|
|
781
883
|
d += " Z";
|
|
@@ -860,10 +962,19 @@ export function parseGradientReference(fill) {
|
|
|
860
962
|
*/
|
|
861
963
|
export function rgbToLuminance(color) {
|
|
862
964
|
if (!color) return 0;
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
965
|
+
if (typeof color !== 'object') return 0;
|
|
966
|
+
|
|
967
|
+
// Validate and clamp RGB values to 0-255 range
|
|
968
|
+
const rVal = typeof color.r === 'number' ? Math.max(0, Math.min(255, color.r)) : 0;
|
|
969
|
+
const gVal = typeof color.g === 'number' ? Math.max(0, Math.min(255, color.g)) : 0;
|
|
970
|
+
const bVal = typeof color.b === 'number' ? Math.max(0, Math.min(255, color.b)) : 0;
|
|
971
|
+
|
|
972
|
+
const r = rVal / 255;
|
|
973
|
+
const g = gVal / 255;
|
|
974
|
+
const b = bVal / 255;
|
|
975
|
+
|
|
976
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
977
|
+
return isNaN(luminance) ? 0 : luminance;
|
|
867
978
|
}
|
|
868
979
|
|
|
869
980
|
/**
|
|
@@ -921,7 +1032,17 @@ export function sampleMeshGradientForMask(
|
|
|
921
1032
|
maskType = "luminance",
|
|
922
1033
|
options = {},
|
|
923
1034
|
) {
|
|
1035
|
+
if (!meshData) throw new Error('sampleMeshGradientForMask: meshData is required');
|
|
1036
|
+
if (!shapeBBox) throw new Error('sampleMeshGradientForMask: shapeBBox is required');
|
|
1037
|
+
if (maskType !== MaskType.LUMINANCE && maskType !== MaskType.ALPHA) {
|
|
1038
|
+
throw new Error('sampleMeshGradientForMask: maskType must be "luminance" or "alpha"');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
924
1041
|
const { subdivisions = 4 } = options;
|
|
1042
|
+
if (typeof subdivisions !== 'number' || subdivisions <= 0) {
|
|
1043
|
+
throw new Error('sampleMeshGradientForMask: subdivisions must be a positive number');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
925
1046
|
const result = [];
|
|
926
1047
|
|
|
927
1048
|
// Get mesh gradient polygons with colors
|
|
@@ -929,19 +1050,23 @@ export function sampleMeshGradientForMask(
|
|
|
929
1050
|
subdivisions,
|
|
930
1051
|
});
|
|
931
1052
|
|
|
1053
|
+
if (!Array.isArray(meshPolygons)) return result;
|
|
1054
|
+
|
|
932
1055
|
for (const { polygon, color } of meshPolygons) {
|
|
933
|
-
if (polygon.length < 3) continue;
|
|
1056
|
+
if (!polygon || polygon.length < 3) continue;
|
|
1057
|
+
if (!color) continue;
|
|
934
1058
|
|
|
935
1059
|
let opacity;
|
|
936
1060
|
if (maskType === MaskType.ALPHA) {
|
|
937
1061
|
// Alpha mask: use alpha channel
|
|
938
|
-
|
|
1062
|
+
const alphaVal = typeof color.a === 'number' ? Math.max(0, Math.min(255, color.a)) : 255;
|
|
1063
|
+
opacity = alphaVal / 255;
|
|
939
1064
|
} else {
|
|
940
1065
|
// Luminance mask: calculate from RGB
|
|
941
1066
|
opacity = rgbToLuminance(color);
|
|
942
1067
|
}
|
|
943
1068
|
|
|
944
|
-
if (opacity > 0) {
|
|
1069
|
+
if (opacity > 0 && !isNaN(opacity)) {
|
|
945
1070
|
result.push({ polygon, opacity });
|
|
946
1071
|
}
|
|
947
1072
|
}
|
|
@@ -1001,6 +1126,12 @@ export function applyMeshGradientMask(
|
|
|
1001
1126
|
maskType = "luminance",
|
|
1002
1127
|
options = {},
|
|
1003
1128
|
) {
|
|
1129
|
+
if (!targetPolygon) throw new Error('applyMeshGradientMask: targetPolygon is required');
|
|
1130
|
+
if (!Array.isArray(targetPolygon)) throw new Error('applyMeshGradientMask: targetPolygon must be an array');
|
|
1131
|
+
if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
|
|
1132
|
+
if (!meshData) throw new Error('applyMeshGradientMask: meshData is required');
|
|
1133
|
+
if (!targetBBox) throw new Error('applyMeshGradientMask: targetBBox is required');
|
|
1134
|
+
|
|
1004
1135
|
const meshMaskRegions = sampleMeshGradientForMask(
|
|
1005
1136
|
meshData,
|
|
1006
1137
|
targetBBox,
|
|
@@ -1011,14 +1142,17 @@ export function applyMeshGradientMask(
|
|
|
1011
1142
|
|
|
1012
1143
|
for (const { polygon: maskPoly, opacity } of meshMaskRegions) {
|
|
1013
1144
|
if (opacity <= 0) continue;
|
|
1145
|
+
if (!maskPoly || maskPoly.length < 3) continue; // Skip invalid mask polygons
|
|
1014
1146
|
|
|
1015
1147
|
const intersection = PolygonClip.polygonIntersection(
|
|
1016
1148
|
targetPolygon,
|
|
1017
1149
|
maskPoly,
|
|
1018
1150
|
);
|
|
1019
1151
|
|
|
1152
|
+
if (!Array.isArray(intersection)) continue;
|
|
1153
|
+
|
|
1020
1154
|
for (const clippedPoly of intersection) {
|
|
1021
|
-
if (clippedPoly.length >= 3) {
|
|
1155
|
+
if (clippedPoly && clippedPoly.length >= 3) {
|
|
1022
1156
|
result.push({ polygon: clippedPoly, opacity });
|
|
1023
1157
|
}
|
|
1024
1158
|
}
|
|
@@ -1097,16 +1231,30 @@ export function resolveMaskWithGradients(
|
|
|
1097
1231
|
gradientDefs = {},
|
|
1098
1232
|
options = {},
|
|
1099
1233
|
) {
|
|
1234
|
+
if (!maskData) throw new Error('resolveMaskWithGradients: maskData is required');
|
|
1235
|
+
if (!targetBBox) throw new Error('resolveMaskWithGradients: targetBBox is required');
|
|
1236
|
+
if (!Array.isArray(maskData.children)) throw new Error('resolveMaskWithGradients: maskData.children must be an array');
|
|
1237
|
+
|
|
1100
1238
|
const { samples = 20, subdivisions = 4 } = options;
|
|
1239
|
+
if (typeof samples !== 'number' || samples <= 0) {
|
|
1240
|
+
throw new Error('resolveMaskWithGradients: samples must be a positive number');
|
|
1241
|
+
}
|
|
1242
|
+
if (typeof subdivisions !== 'number' || subdivisions <= 0) {
|
|
1243
|
+
throw new Error('resolveMaskWithGradients: subdivisions must be a positive number');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1101
1246
|
const maskType = maskData.maskType || MaskType.LUMINANCE;
|
|
1102
1247
|
const result = [];
|
|
1103
1248
|
|
|
1104
1249
|
for (const child of maskData.children) {
|
|
1250
|
+
if (!child) continue; // Skip null/undefined children
|
|
1251
|
+
|
|
1105
1252
|
// Check if fill references a gradient
|
|
1106
1253
|
const gradientId = parseGradientReference(child.fill);
|
|
1107
1254
|
|
|
1108
1255
|
if (gradientId && gradientDefs[gradientId]) {
|
|
1109
1256
|
const gradientData = gradientDefs[gradientId];
|
|
1257
|
+
if (!gradientData) continue;
|
|
1110
1258
|
|
|
1111
1259
|
// Get the shape polygon first
|
|
1112
1260
|
const shapePolygon = maskChildToPolygon(
|
|
@@ -1133,17 +1281,22 @@ export function resolveMaskWithGradients(
|
|
|
1133
1281
|
polygon: meshPoly,
|
|
1134
1282
|
opacity: meshOpacity,
|
|
1135
1283
|
} of meshMaskRegions) {
|
|
1284
|
+
if (!meshPoly || meshPoly.length < 3) continue;
|
|
1285
|
+
|
|
1136
1286
|
const clipped = PolygonClip.polygonIntersection(
|
|
1137
1287
|
shapePolygon,
|
|
1138
1288
|
meshPoly,
|
|
1139
1289
|
);
|
|
1140
1290
|
|
|
1291
|
+
if (!Array.isArray(clipped)) continue;
|
|
1292
|
+
|
|
1141
1293
|
for (const clippedPoly of clipped) {
|
|
1142
|
-
if (clippedPoly.length >= 3) {
|
|
1294
|
+
if (clippedPoly && clippedPoly.length >= 3) {
|
|
1143
1295
|
// Combine mesh opacity with child opacity
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1296
|
+
const childOpacity = typeof child.opacity === 'number' ? child.opacity : 1;
|
|
1297
|
+
const childFillOpacity = typeof child.fillOpacity === 'number' ? child.fillOpacity : 1;
|
|
1298
|
+
const combinedOpacity = meshOpacity * childOpacity * childFillOpacity;
|
|
1299
|
+
if (combinedOpacity > 0 && !isNaN(combinedOpacity)) {
|
|
1147
1300
|
result.push({ polygon: clippedPoly, opacity: combinedOpacity });
|
|
1148
1301
|
}
|
|
1149
1302
|
}
|
|
@@ -1229,6 +1382,13 @@ export function createMeshGradientMask(
|
|
|
1229
1382
|
maskType = "luminance",
|
|
1230
1383
|
options = {},
|
|
1231
1384
|
) {
|
|
1385
|
+
if (!meshData) throw new Error('createMeshGradientMask: meshData is required');
|
|
1386
|
+
if (!bounds) throw new Error('createMeshGradientMask: bounds is required');
|
|
1387
|
+
if (typeof bounds !== 'object' || typeof bounds.x !== 'number' || typeof bounds.y !== 'number' ||
|
|
1388
|
+
typeof bounds.width !== 'number' || typeof bounds.height !== 'number') {
|
|
1389
|
+
throw new Error('createMeshGradientMask: bounds must have numeric x, y, width, height properties');
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1232
1392
|
const regions = sampleMeshGradientForMask(
|
|
1233
1393
|
meshData,
|
|
1234
1394
|
bounds,
|
|
@@ -1291,9 +1451,14 @@ export function createMeshGradientMask(
|
|
|
1291
1451
|
* console.log(`Boundary has ${boundary.length} vertices`);
|
|
1292
1452
|
*/
|
|
1293
1453
|
export function getMeshGradientBoundary(meshData, options = {}) {
|
|
1454
|
+
if (!meshData) throw new Error('getMeshGradientBoundary: meshData is required');
|
|
1455
|
+
|
|
1294
1456
|
const { samples = 20 } = options;
|
|
1457
|
+
if (typeof samples !== 'number' || samples <= 0) {
|
|
1458
|
+
throw new Error('getMeshGradientBoundary: samples must be a positive number');
|
|
1459
|
+
}
|
|
1295
1460
|
|
|
1296
|
-
if (!meshData.patches || meshData.patches.length === 0) {
|
|
1461
|
+
if (!meshData.patches || !Array.isArray(meshData.patches) || meshData.patches.length === 0) {
|
|
1297
1462
|
return [];
|
|
1298
1463
|
}
|
|
1299
1464
|
|
|
@@ -1302,25 +1467,35 @@ export function getMeshGradientBoundary(meshData, options = {}) {
|
|
|
1302
1467
|
const allPoints = [];
|
|
1303
1468
|
|
|
1304
1469
|
for (const patch of meshData.patches) {
|
|
1470
|
+
if (!patch) continue; // Skip null/undefined patches
|
|
1471
|
+
|
|
1305
1472
|
// Sample each edge of the patch
|
|
1306
1473
|
if (patch.top) {
|
|
1307
1474
|
const topPoints = MeshGradient.sampleBezierCurve(patch.top, samples);
|
|
1308
|
-
|
|
1475
|
+
if (Array.isArray(topPoints)) {
|
|
1476
|
+
allPoints.push(...topPoints);
|
|
1477
|
+
}
|
|
1309
1478
|
}
|
|
1310
1479
|
if (patch.right) {
|
|
1311
1480
|
const rightPoints = MeshGradient.sampleBezierCurve(patch.right, samples);
|
|
1312
|
-
|
|
1481
|
+
if (Array.isArray(rightPoints)) {
|
|
1482
|
+
allPoints.push(...rightPoints);
|
|
1483
|
+
}
|
|
1313
1484
|
}
|
|
1314
1485
|
if (patch.bottom) {
|
|
1315
1486
|
const bottomPoints = MeshGradient.sampleBezierCurve(
|
|
1316
1487
|
patch.bottom,
|
|
1317
1488
|
samples,
|
|
1318
1489
|
);
|
|
1319
|
-
|
|
1490
|
+
if (Array.isArray(bottomPoints)) {
|
|
1491
|
+
allPoints.push(...bottomPoints);
|
|
1492
|
+
}
|
|
1320
1493
|
}
|
|
1321
1494
|
if (patch.left) {
|
|
1322
1495
|
const leftPoints = MeshGradient.sampleBezierCurve(patch.left, samples);
|
|
1323
|
-
|
|
1496
|
+
if (Array.isArray(leftPoints)) {
|
|
1497
|
+
allPoints.push(...leftPoints);
|
|
1498
|
+
}
|
|
1324
1499
|
}
|
|
1325
1500
|
}
|
|
1326
1501
|
|
|
@@ -1394,26 +1569,40 @@ export function clipWithMeshGradientShape(
|
|
|
1394
1569
|
meshData,
|
|
1395
1570
|
options = {},
|
|
1396
1571
|
) {
|
|
1572
|
+
if (!targetPolygon) throw new Error('clipWithMeshGradientShape: targetPolygon is required');
|
|
1573
|
+
if (!Array.isArray(targetPolygon)) throw new Error('clipWithMeshGradientShape: targetPolygon must be an array');
|
|
1574
|
+
if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
|
|
1575
|
+
if (!meshData) throw new Error('clipWithMeshGradientShape: meshData is required');
|
|
1576
|
+
|
|
1397
1577
|
const { subdivisions = 4 } = options;
|
|
1578
|
+
if (typeof subdivisions !== 'number' || subdivisions <= 0) {
|
|
1579
|
+
throw new Error('clipWithMeshGradientShape: subdivisions must be a positive number');
|
|
1580
|
+
}
|
|
1398
1581
|
|
|
1399
1582
|
// Get all patch polygons (ignoring colors)
|
|
1400
1583
|
const meshPolygons = MeshGradient.meshGradientToPolygons(meshData, {
|
|
1401
1584
|
subdivisions,
|
|
1402
1585
|
});
|
|
1403
1586
|
|
|
1404
|
-
if (meshPolygons.length === 0) {
|
|
1587
|
+
if (!Array.isArray(meshPolygons) || meshPolygons.length === 0) {
|
|
1405
1588
|
return [];
|
|
1406
1589
|
}
|
|
1407
1590
|
|
|
1408
1591
|
// Union all patch polygons to get the complete mesh shape
|
|
1409
1592
|
let meshShape = meshPolygons[0].polygon;
|
|
1593
|
+
if (!meshShape || meshShape.length < 3) {
|
|
1594
|
+
return [];
|
|
1595
|
+
}
|
|
1410
1596
|
|
|
1411
1597
|
for (let i = 1; i < meshPolygons.length; i++) {
|
|
1598
|
+
if (!meshPolygons[i] || !meshPolygons[i].polygon || meshPolygons[i].polygon.length < 3) {
|
|
1599
|
+
continue; // Skip invalid polygons
|
|
1600
|
+
}
|
|
1412
1601
|
const unionResult = PolygonClip.polygonUnion(
|
|
1413
1602
|
meshShape,
|
|
1414
1603
|
meshPolygons[i].polygon,
|
|
1415
1604
|
);
|
|
1416
|
-
if (unionResult.length > 0 && unionResult[0].length >= 3) {
|
|
1605
|
+
if (Array.isArray(unionResult) && unionResult.length > 0 && unionResult[0].length >= 3) {
|
|
1417
1606
|
meshShape = unionResult[0];
|
|
1418
1607
|
}
|
|
1419
1608
|
}
|
|
@@ -1469,24 +1658,34 @@ export function clipWithMeshGradientShape(
|
|
|
1469
1658
|
* const svgClipPath = `<clipPath id="mesh-shape"><path d="${pathData}"/></clipPath>`;
|
|
1470
1659
|
*/
|
|
1471
1660
|
export function meshGradientToClipPath(meshData, options = {}) {
|
|
1661
|
+
if (!meshData) throw new Error('meshGradientToClipPath: meshData is required');
|
|
1662
|
+
|
|
1472
1663
|
const { subdivisions = 4 } = options;
|
|
1664
|
+
if (typeof subdivisions !== 'number' || subdivisions <= 0) {
|
|
1665
|
+
throw new Error('meshGradientToClipPath: subdivisions must be a positive number');
|
|
1666
|
+
}
|
|
1473
1667
|
|
|
1474
1668
|
const meshPolygons = MeshGradient.meshGradientToPolygons(meshData, {
|
|
1475
1669
|
subdivisions,
|
|
1476
1670
|
});
|
|
1477
1671
|
|
|
1478
|
-
if (meshPolygons.length === 0) {
|
|
1672
|
+
if (!Array.isArray(meshPolygons) || meshPolygons.length === 0) {
|
|
1479
1673
|
return [];
|
|
1480
1674
|
}
|
|
1481
1675
|
|
|
1482
1676
|
// Union all patches into one shape
|
|
1483
1677
|
let result = meshPolygons[0].polygon;
|
|
1678
|
+
if (!result || result.length < 3) {
|
|
1679
|
+
return [];
|
|
1680
|
+
}
|
|
1484
1681
|
|
|
1485
1682
|
for (let i = 1; i < meshPolygons.length; i++) {
|
|
1683
|
+
if (!meshPolygons[i] || !meshPolygons[i].polygon) continue; // Skip invalid polygons
|
|
1684
|
+
|
|
1486
1685
|
const poly = meshPolygons[i].polygon;
|
|
1487
1686
|
if (poly.length >= 3) {
|
|
1488
1687
|
const unionResult = PolygonClip.polygonUnion(result, poly);
|
|
1489
|
-
if (unionResult.length > 0 && unionResult[0].length >= 3) {
|
|
1688
|
+
if (Array.isArray(unionResult) && unionResult.length > 0 && unionResult[0].length >= 3) {
|
|
1490
1689
|
result = unionResult[0];
|
|
1491
1690
|
}
|
|
1492
1691
|
}
|