@shotstack/shotstack-canvas 1.7.2 → 1.8.1

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/dist/entry.web.js CHANGED
@@ -12,7 +12,16 @@ import {
12
12
  richTextShadowSchema,
13
13
  richTextBackgroundSchema,
14
14
  richTextAlignmentSchema,
15
- richTextAnimationSchema
15
+ richTextAnimationSchema,
16
+ svgAssetSchema,
17
+ svgShapeSchema,
18
+ svgSolidFillSchema,
19
+ svgLinearGradientFillSchema,
20
+ svgRadialGradientFillSchema,
21
+ svgStrokeSchema,
22
+ svgShadowSchema,
23
+ svgTransformSchema,
24
+ svgGradientStopSchema
16
25
  } from "@shotstack/schemas/zod";
17
26
 
18
27
  // src/config/canvas-constants.ts
@@ -168,6 +177,7 @@ var CanvasRichTextAssetSchema = richTextAssetSchema.extend({
168
177
  animation: canvasAnimationSchema.optional(),
169
178
  customFonts: z.array(customFontSchema).optional()
170
179
  }).strict();
180
+ var CanvasSvgAssetSchema = svgAssetSchema;
171
181
 
172
182
  // src/wasm/hb-loader.ts
173
183
  var hbSingleton = null;
@@ -404,6 +414,9 @@ var _FontRegistry = class _FontRegistry {
404
414
  __publicField(this, "fonts", /* @__PURE__ */ new Map());
405
415
  __publicField(this, "blobs", /* @__PURE__ */ new Map());
406
416
  __publicField(this, "fontkitFonts", /* @__PURE__ */ new Map());
417
+ __publicField(this, "fontkitBaseFonts", /* @__PURE__ */ new Map());
418
+ __publicField(this, "colorEmojiFonts", /* @__PURE__ */ new Set());
419
+ __publicField(this, "colorEmojiFontBytes", /* @__PURE__ */ new Map());
407
420
  __publicField(this, "wasmBaseURL");
408
421
  __publicField(this, "initPromise");
409
422
  __publicField(this, "emojiFallbackDesc");
@@ -465,6 +478,49 @@ var _FontRegistry = class _FontRegistry {
465
478
  setEmojiFallback(desc) {
466
479
  this.emojiFallbackDesc = desc;
467
480
  }
481
+ isColorEmojiFont(family) {
482
+ return this.colorEmojiFonts.has(family);
483
+ }
484
+ getColorEmojiFontBytes(family) {
485
+ return this.colorEmojiFontBytes.get(family);
486
+ }
487
+ getColorEmojiFontFamilies() {
488
+ return Array.from(this.colorEmojiFonts);
489
+ }
490
+ detectColorEmojiFont(fkFont) {
491
+ try {
492
+ if (fkFont.directory && fkFont.directory.tables) {
493
+ const tables = fkFont.directory.tables;
494
+ if (tables.CBDT || tables.CBLC) {
495
+ return true;
496
+ }
497
+ if (tables.sbix) {
498
+ return true;
499
+ }
500
+ if (tables.COLR && tables.CPAL) {
501
+ return true;
502
+ }
503
+ if (tables.SVG) {
504
+ return true;
505
+ }
506
+ }
507
+ if (fkFont.availableTables) {
508
+ const tables = fkFont.availableTables;
509
+ if (tables.includes("CBDT") || tables.includes("CBLC") || tables.includes("sbix") || tables.includes("COLR") && tables.includes("CPAL") || tables.includes("SVG")) {
510
+ return true;
511
+ }
512
+ }
513
+ if (fkFont._tables) {
514
+ const tableNames = Object.keys(fkFont._tables);
515
+ if (tableNames.includes("CBDT") || tableNames.includes("CBLC") || tableNames.includes("sbix") || tableNames.includes("COLR") && tableNames.includes("CPAL") || tableNames.includes("SVG")) {
516
+ return true;
517
+ }
518
+ }
519
+ return false;
520
+ } catch {
521
+ return false;
522
+ }
523
+ }
468
524
  async init() {
469
525
  if (this.initPromise) {
470
526
  await this.initPromise;
@@ -530,13 +586,29 @@ var _FontRegistry = class _FontRegistry {
530
586
  try {
531
587
  const buffer = typeof Buffer !== "undefined" ? Buffer.from(bytes) : new Uint8Array(bytes);
532
588
  const fkFont = fontkit.create(buffer);
589
+ const baseFontKey = desc.family;
590
+ if (!this.fontkitBaseFonts.has(baseFontKey)) {
591
+ this.fontkitBaseFonts.set(baseFontKey, fkFont);
592
+ }
593
+ const isColorEmojiFont = this.detectColorEmojiFont(fkFont);
594
+ if (isColorEmojiFont) {
595
+ this.colorEmojiFonts.add(desc.family);
596
+ if (!this.colorEmojiFontBytes.has(desc.family)) {
597
+ this.colorEmojiFontBytes.set(desc.family, bytes.slice(0));
598
+ }
599
+ console.log(`\u{1F3A8} Registered color emoji font: ${desc.family}`);
600
+ }
533
601
  if (fkFont.variationAxes && Object.keys(fkFont.variationAxes).length > 0) {
534
602
  const variationFont = fkFont.getVariation({ wght: weightNum });
535
603
  this.fontkitFonts.set(k, variationFont);
536
- console.log(`\u2705 Registered variable font: ${desc.family} weight=${weightNum}`);
604
+ if (!isColorEmojiFont) {
605
+ console.log(`\u2705 Registered variable font: ${desc.family} weight=${weightNum}`);
606
+ }
537
607
  } else {
538
608
  this.fontkitFonts.set(k, fkFont);
539
- console.log(`\u2705 Registered static font: ${desc.family}`);
609
+ if (!isColorEmojiFont) {
610
+ console.log(`\u2705 Registered static font: ${desc.family}`);
611
+ }
540
612
  }
541
613
  } catch (err) {
542
614
  console.warn(`\u26A0\uFE0F Fontkit failed for ${desc.family}:`, err);
@@ -575,6 +647,9 @@ var _FontRegistry = class _FontRegistry {
575
647
  const installed = await this.tryFallbackInstall(desc);
576
648
  f = installed ? this.fonts.get(k) : void 0;
577
649
  }
650
+ if (!f) {
651
+ f = await this.tryDeriveFromExistingFont(desc);
652
+ }
578
653
  if (!f) throw new Error(`Font not registered for ${k}`);
579
654
  return f;
580
655
  } catch (err) {
@@ -586,6 +661,53 @@ var _FontRegistry = class _FontRegistry {
586
661
  );
587
662
  }
588
663
  }
664
+ async tryDeriveFromExistingFont(desc) {
665
+ const targetFamily = desc.family;
666
+ const targetWeight = normalizeWeight(desc.weight);
667
+ const targetWeightNum = parseInt(targetWeight, 10);
668
+ let existingBlob;
669
+ for (const [key, blob] of this.blobs) {
670
+ if (key.startsWith(`${targetFamily}__`)) {
671
+ existingBlob = blob;
672
+ break;
673
+ }
674
+ }
675
+ const baseFkFont = this.fontkitBaseFonts.get(targetFamily);
676
+ if (!existingBlob || !baseFkFont) {
677
+ return void 0;
678
+ }
679
+ const hasWeightAxis = baseFkFont.variationAxes && baseFkFont.variationAxes["wght"] !== void 0;
680
+ if (!hasWeightAxis) {
681
+ console.warn(
682
+ `Font ${targetFamily} is not a variable font, cannot derive weight ${targetWeight}`
683
+ );
684
+ return void 0;
685
+ }
686
+ const k = this.key(desc);
687
+ const face = this.hb.createFace(existingBlob, 0);
688
+ const font = this.hb.createFont(face);
689
+ const upem = face.upem || 1e3;
690
+ font.setScale(upem, upem);
691
+ if (typeof font.setVariations === "function") {
692
+ try {
693
+ font.setVariations(`wght=${targetWeightNum}`);
694
+ } catch (e) {
695
+ }
696
+ }
697
+ this.faces.set(k, face);
698
+ this.fonts.set(k, font);
699
+ try {
700
+ const variationFont = baseFkFont.getVariation({ wght: targetWeightNum });
701
+ this.fontkitFonts.set(k, variationFont);
702
+ console.log(
703
+ `\u2705 Derived variable font: ${targetFamily} weight=${targetWeightNum} from existing registration`
704
+ );
705
+ } catch (err) {
706
+ this.fontkitFonts.set(k, baseFkFont);
707
+ console.warn(`\u26A0\uFE0F Could not apply weight variation, using base font for ${targetFamily}`);
708
+ }
709
+ return font;
710
+ }
589
711
  async getFace(desc) {
590
712
  try {
591
713
  if (!this.hb) await this.init();
@@ -595,6 +717,10 @@ var _FontRegistry = class _FontRegistry {
595
717
  const installed = await this.tryFallbackInstall(desc);
596
718
  face = installed ? this.faces.get(k) : void 0;
597
719
  }
720
+ if (!face) {
721
+ await this.tryDeriveFromExistingFont(desc);
722
+ face = this.faces.get(k);
723
+ }
598
724
  return face;
599
725
  } catch (err) {
600
726
  throw new Error(
@@ -665,6 +791,9 @@ var _FontRegistry = class _FontRegistry {
665
791
  this.faces.clear();
666
792
  this.blobs.clear();
667
793
  this.fontkitFonts.clear();
794
+ this.fontkitBaseFonts.clear();
795
+ this.colorEmojiFonts.clear();
796
+ this.colorEmojiFontBytes.clear();
668
797
  this.hb = void 0;
669
798
  this.initPromise = void 0;
670
799
  } catch (err) {
@@ -794,7 +923,10 @@ var LayoutEngine = class {
794
923
  const charIndex = g.cl;
795
924
  let char;
796
925
  if (charIndex >= 0 && charIndex < input.length) {
797
- char = input[charIndex];
926
+ const codePoint = input.codePointAt(charIndex);
927
+ if (codePoint !== void 0) {
928
+ char = String.fromCodePoint(codePoint);
929
+ }
798
930
  }
799
931
  return {
800
932
  id: g.g,
@@ -979,13 +1111,27 @@ async function buildDrawOps(p) {
979
1111
  });
980
1112
  }
981
1113
  for (const glyph of line.glyphs) {
1114
+ const glyphX = xCursor + glyph.xOffset;
1115
+ const glyphY = baselineY + glyph.yOffset;
1116
+ const glyphFamily = glyph.fontDesc?.family;
1117
+ const isColorEmoji = glyph.isColorEmoji || glyphFamily && p.isColorEmojiFont && p.isColorEmojiFont(glyphFamily);
1118
+ if (isColorEmoji && glyph.char) {
1119
+ textOps.push({
1120
+ op: "DrawColorEmoji",
1121
+ char: glyph.char,
1122
+ x: glyphX,
1123
+ y: glyphY,
1124
+ fontSize: p.font.size,
1125
+ fontFamily: glyphFamily || "NotoColorEmoji"
1126
+ });
1127
+ xCursor += glyph.xAdvance;
1128
+ continue;
1129
+ }
982
1130
  const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
983
1131
  if (!path || path === "M 0 0") {
984
1132
  xCursor += glyph.xAdvance;
985
1133
  continue;
986
1134
  }
987
- const glyphX = xCursor + glyph.xOffset;
988
- const glyphY = baselineY + glyph.yOffset;
989
1135
  const pb = computePathBounds(path);
990
1136
  const x1 = glyphX + scale * pb.x;
991
1137
  const x2 = glyphX + scale * (pb.x + pb.w);
@@ -1913,6 +2059,81 @@ function createWebPainter(canvas) {
1913
2059
  ctx.restore();
1914
2060
  continue;
1915
2061
  }
2062
+ if (op.op === "DrawColorEmoji") {
2063
+ ctx.save();
2064
+ ctx.font = `${op.fontSize}px "${op.fontFamily}"`;
2065
+ ctx.textBaseline = "alphabetic";
2066
+ ctx.fillText(op.char, op.x, op.y);
2067
+ ctx.restore();
2068
+ continue;
2069
+ }
2070
+ if (op.op === "Ellipse") {
2071
+ ctx.save();
2072
+ const bbox = op.gradientBBox ?? {
2073
+ x: op.cx - op.rx,
2074
+ y: op.cy - op.ry,
2075
+ w: op.rx * 2,
2076
+ h: op.ry * 2
2077
+ };
2078
+ const fill = makeGradientFromBBox(ctx, op.fill, bbox);
2079
+ ctx.fillStyle = fill;
2080
+ ctx.beginPath();
2081
+ const rotation = (op.rotation ?? 0) * Math.PI / 180;
2082
+ ctx.ellipse(op.cx, op.cy, op.rx, op.ry, rotation, 0, Math.PI * 2);
2083
+ ctx.fill();
2084
+ ctx.restore();
2085
+ continue;
2086
+ }
2087
+ if (op.op === "EllipseStroke") {
2088
+ ctx.save();
2089
+ applyStrokeSpec(ctx, op.stroke);
2090
+ ctx.beginPath();
2091
+ const rotation = (op.rotation ?? 0) * Math.PI / 180;
2092
+ ctx.ellipse(op.cx, op.cy, op.rx, op.ry, rotation, 0, Math.PI * 2);
2093
+ ctx.stroke();
2094
+ ctx.restore();
2095
+ continue;
2096
+ }
2097
+ if (op.op === "Polygon") {
2098
+ ctx.save();
2099
+ const bbox = op.gradientBBox ?? computePointsBounds(op.points);
2100
+ const fill = makeGradientFromBBox(ctx, op.fill, bbox);
2101
+ ctx.fillStyle = fill;
2102
+ ctx.beginPath();
2103
+ drawPolygonPath(ctx, op.points, op.closed ?? true);
2104
+ ctx.fill();
2105
+ ctx.restore();
2106
+ continue;
2107
+ }
2108
+ if (op.op === "PolygonStroke") {
2109
+ ctx.save();
2110
+ applyStrokeSpec(ctx, op.stroke);
2111
+ ctx.beginPath();
2112
+ drawPolygonPath(ctx, op.points, op.closed ?? true);
2113
+ ctx.stroke();
2114
+ ctx.restore();
2115
+ continue;
2116
+ }
2117
+ if (op.op === "ShapePath") {
2118
+ ctx.save();
2119
+ ctx.translate(op.x, op.y);
2120
+ const bbox = op.gradientBBox ?? computePathBounds2(op.path);
2121
+ const fill = makeGradientFromBBox(ctx, op.fill, bbox);
2122
+ ctx.fillStyle = fill;
2123
+ const p = new Path2D(op.path);
2124
+ ctx.fill(p);
2125
+ ctx.restore();
2126
+ continue;
2127
+ }
2128
+ if (op.op === "ShapePathStroke") {
2129
+ ctx.save();
2130
+ ctx.translate(op.x, op.y);
2131
+ applyStrokeSpec(ctx, op.stroke);
2132
+ const p = new Path2D(op.path);
2133
+ ctx.stroke(p);
2134
+ ctx.restore();
2135
+ continue;
2136
+ }
1916
2137
  }
1917
2138
  }
1918
2139
  };
@@ -2052,6 +2273,48 @@ function computePathBounds2(d) {
2052
2273
  function tokenizePath2(d) {
2053
2274
  return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
2054
2275
  }
2276
+ function applyStrokeSpec(ctx, stroke) {
2277
+ const c = parseHex6(stroke.color, stroke.opacity);
2278
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
2279
+ ctx.lineWidth = stroke.width;
2280
+ ctx.lineCap = stroke.lineCap ?? "butt";
2281
+ ctx.lineJoin = stroke.lineJoin ?? "miter";
2282
+ if (stroke.dashArray && stroke.dashArray.length > 0) {
2283
+ ctx.setLineDash(stroke.dashArray);
2284
+ }
2285
+ }
2286
+ function computePointsBounds(points) {
2287
+ if (points.length === 0) {
2288
+ return { x: 0, y: 0, w: 0, h: 0 };
2289
+ }
2290
+ let minX = points[0].x;
2291
+ let minY = points[0].y;
2292
+ let maxX = points[0].x;
2293
+ let maxY = points[0].y;
2294
+ for (let i = 1; i < points.length; i++) {
2295
+ const p = points[i];
2296
+ if (p.x < minX) minX = p.x;
2297
+ if (p.y < minY) minY = p.y;
2298
+ if (p.x > maxX) maxX = p.x;
2299
+ if (p.y > maxY) maxY = p.y;
2300
+ }
2301
+ return {
2302
+ x: minX,
2303
+ y: minY,
2304
+ w: maxX - minX,
2305
+ h: maxY - minY
2306
+ };
2307
+ }
2308
+ function drawPolygonPath(ctx, points, closed) {
2309
+ if (points.length === 0) return;
2310
+ ctx.moveTo(points[0].x, points[0].y);
2311
+ for (let i = 1; i < points.length; i++) {
2312
+ ctx.lineTo(points[i].x, points[i].y);
2313
+ }
2314
+ if (closed) {
2315
+ ctx.closePath();
2316
+ }
2317
+ }
2055
2318
 
2056
2319
  // src/io/web.ts
2057
2320
  async function fetchToArrayBuffer(url) {
@@ -2086,6 +2349,1131 @@ var isGlyphFill2 = (op) => {
2086
2349
  return op.op === "FillPath" && op.isShadow !== true;
2087
2350
  };
2088
2351
 
2352
+ // src/core/shape-generators.ts
2353
+ var KAPPA = 0.5522847498307936;
2354
+ var DEG_TO_RAD = Math.PI / 180;
2355
+ function generateShapePath(params) {
2356
+ switch (params.shape) {
2357
+ case "rectangle":
2358
+ return generateRectanglePath(params);
2359
+ case "circle":
2360
+ return generateCirclePath(params);
2361
+ case "ellipse":
2362
+ return generateEllipsePath(params);
2363
+ case "line":
2364
+ return generateLinePath(params);
2365
+ case "triangle":
2366
+ return generateTrianglePath(params);
2367
+ case "polygon":
2368
+ return generatePolygonPath(params);
2369
+ case "star":
2370
+ return generateStarPath(params);
2371
+ case "arrow":
2372
+ return generateArrowPath(params);
2373
+ case "heart":
2374
+ return generateHeartPath(params);
2375
+ case "cross":
2376
+ return generateCrossPath(params);
2377
+ case "ring":
2378
+ return generateRingPath(params);
2379
+ }
2380
+ }
2381
+ function generateRectanglePath(params) {
2382
+ const { width, height, cornerRadius = 0 } = params;
2383
+ if (cornerRadius === 0) {
2384
+ return `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`;
2385
+ }
2386
+ const r = Math.min(cornerRadius, width / 2, height / 2);
2387
+ return [
2388
+ `M ${r} 0`,
2389
+ `L ${width - r} 0`,
2390
+ `Q ${width} 0 ${width} ${r}`,
2391
+ `L ${width} ${height - r}`,
2392
+ `Q ${width} ${height} ${width - r} ${height}`,
2393
+ `L ${r} ${height}`,
2394
+ `Q 0 ${height} 0 ${height - r}`,
2395
+ `L 0 ${r}`,
2396
+ `Q 0 0 ${r} 0`,
2397
+ "Z"
2398
+ ].join(" ");
2399
+ }
2400
+ function generateCirclePath(params) {
2401
+ const { radius } = params;
2402
+ return generateEllipsePath({ radiusX: radius, radiusY: radius });
2403
+ }
2404
+ function generateEllipsePath(params) {
2405
+ const { radiusX, radiusY } = params;
2406
+ const kx = radiusX * KAPPA;
2407
+ const ky = radiusY * KAPPA;
2408
+ return [
2409
+ `M ${radiusX} 0`,
2410
+ `C ${radiusX} ${ky} ${kx} ${radiusY} 0 ${radiusY}`,
2411
+ `C ${-kx} ${radiusY} ${-radiusX} ${ky} ${-radiusX} 0`,
2412
+ `C ${-radiusX} ${-ky} ${-kx} ${-radiusY} 0 ${-radiusY}`,
2413
+ `C ${kx} ${-radiusY} ${radiusX} ${-ky} ${radiusX} 0`,
2414
+ "Z"
2415
+ ].join(" ");
2416
+ }
2417
+ function generateLinePath(params) {
2418
+ const { length, thickness } = params;
2419
+ const halfThickness = thickness / 2;
2420
+ return [
2421
+ `M 0 ${-halfThickness}`,
2422
+ `L ${length} ${-halfThickness}`,
2423
+ `L ${length} ${halfThickness}`,
2424
+ `L 0 ${halfThickness}`,
2425
+ "Z"
2426
+ ].join(" ");
2427
+ }
2428
+ function generateTrianglePath(params) {
2429
+ const { width, height } = params;
2430
+ const halfWidth = width / 2;
2431
+ return `M 0 ${height} L ${halfWidth} 0 L ${width} ${height} Z`;
2432
+ }
2433
+ function generatePolygonPath(params) {
2434
+ const { sides, radius } = params;
2435
+ const points = generatePolygonPoints(sides, radius);
2436
+ return pointsToPath(points, true);
2437
+ }
2438
+ function generatePolygonPoints(sides, radius) {
2439
+ const points = [];
2440
+ const angleStep = 2 * Math.PI / sides;
2441
+ const startAngle = -Math.PI / 2;
2442
+ for (let i = 0; i < sides; i++) {
2443
+ const angle = startAngle + i * angleStep;
2444
+ points.push({
2445
+ x: radius * Math.cos(angle),
2446
+ y: radius * Math.sin(angle)
2447
+ });
2448
+ }
2449
+ return points;
2450
+ }
2451
+ function generateStarPath(params) {
2452
+ const { points, outerRadius, innerRadius } = params;
2453
+ const starPoints = generateStarPoints(points, outerRadius, innerRadius);
2454
+ return pointsToPath(starPoints, true);
2455
+ }
2456
+ function generateStarPoints(pointCount, outerRadius, innerRadius) {
2457
+ const points = [];
2458
+ const totalPoints = pointCount * 2;
2459
+ const angleStep = Math.PI / pointCount;
2460
+ const startAngle = -Math.PI / 2;
2461
+ for (let i = 0; i < totalPoints; i++) {
2462
+ const radius = i % 2 === 0 ? outerRadius : innerRadius;
2463
+ const angle = startAngle + i * angleStep;
2464
+ points.push({
2465
+ x: radius * Math.cos(angle),
2466
+ y: radius * Math.sin(angle)
2467
+ });
2468
+ }
2469
+ return points;
2470
+ }
2471
+ function generateArrowPath(params) {
2472
+ const { length, headWidth, headLength, shaftWidth } = params;
2473
+ const halfShaft = shaftWidth / 2;
2474
+ const halfHead = headWidth / 2;
2475
+ const shaftLength = length - headLength;
2476
+ return [
2477
+ `M 0 ${-halfShaft}`,
2478
+ `L ${shaftLength} ${-halfShaft}`,
2479
+ `L ${shaftLength} ${-halfHead}`,
2480
+ `L ${length} 0`,
2481
+ `L ${shaftLength} ${halfHead}`,
2482
+ `L ${shaftLength} ${halfShaft}`,
2483
+ `L 0 ${halfShaft}`,
2484
+ "Z"
2485
+ ].join(" ");
2486
+ }
2487
+ function generateHeartPath(params) {
2488
+ const { size } = params;
2489
+ const s = size / 2;
2490
+ return [
2491
+ `M 0 ${s * 0.3}`,
2492
+ `C 0 ${-s * 0.3} ${-s} ${-s * 0.3} ${-s} ${s * 0.3}`,
2493
+ `C ${-s} ${s * 0.7} 0 ${s} 0 ${s * 1.2}`,
2494
+ `C 0 ${s} ${s} ${s * 0.7} ${s} ${s * 0.3}`,
2495
+ `C ${s} ${-s * 0.3} 0 ${-s * 0.3} 0 ${s * 0.3}`,
2496
+ "Z"
2497
+ ].join(" ");
2498
+ }
2499
+ function generateCrossPath(params) {
2500
+ const { width, height, thickness } = params;
2501
+ const halfThickness = thickness / 2;
2502
+ const halfWidth = width / 2;
2503
+ const halfHeight = height / 2;
2504
+ return [
2505
+ `M ${-halfThickness} ${-halfHeight}`,
2506
+ `L ${halfThickness} ${-halfHeight}`,
2507
+ `L ${halfThickness} ${-halfThickness}`,
2508
+ `L ${halfWidth} ${-halfThickness}`,
2509
+ `L ${halfWidth} ${halfThickness}`,
2510
+ `L ${halfThickness} ${halfThickness}`,
2511
+ `L ${halfThickness} ${halfHeight}`,
2512
+ `L ${-halfThickness} ${halfHeight}`,
2513
+ `L ${-halfThickness} ${halfThickness}`,
2514
+ `L ${-halfWidth} ${halfThickness}`,
2515
+ `L ${-halfWidth} ${-halfThickness}`,
2516
+ `L ${-halfThickness} ${-halfThickness}`,
2517
+ "Z"
2518
+ ].join(" ");
2519
+ }
2520
+ function generateRingPath(params) {
2521
+ const { outerRadius, innerRadius } = params;
2522
+ const outerPath = generateCirclePath({ radius: outerRadius });
2523
+ const innerPath = generateCirclePath({ radius: innerRadius });
2524
+ return `${outerPath} ${reverseWindingOrder(innerPath)}`;
2525
+ }
2526
+ function pointsToPath(points, closed = true) {
2527
+ if (points.length === 0) return "";
2528
+ const commands = [`M ${points[0].x} ${points[0].y}`];
2529
+ for (let i = 1; i < points.length; i++) {
2530
+ commands.push(`L ${points[i].x} ${points[i].y}`);
2531
+ }
2532
+ if (closed) {
2533
+ commands.push("Z");
2534
+ }
2535
+ return commands.join(" ");
2536
+ }
2537
+ function reverseWindingOrder(path) {
2538
+ const commands = parsePathCommands(path);
2539
+ const reversed = [];
2540
+ const points = [];
2541
+ let currentX = 0;
2542
+ let currentY = 0;
2543
+ for (const cmd of commands) {
2544
+ if (cmd.type === "M" || cmd.type === "L") {
2545
+ points.push({ x: cmd.x, y: cmd.y });
2546
+ currentX = cmd.x;
2547
+ currentY = cmd.y;
2548
+ } else if (cmd.type === "C") {
2549
+ points.push({ x: cmd.x, y: cmd.y });
2550
+ currentX = cmd.x;
2551
+ currentY = cmd.y;
2552
+ }
2553
+ }
2554
+ if (points.length === 0) return path;
2555
+ points.reverse();
2556
+ reversed.push(`M ${points[0].x} ${points[0].y}`);
2557
+ for (let i = 1; i < points.length; i++) {
2558
+ reversed.push(`L ${points[i].x} ${points[i].y}`);
2559
+ }
2560
+ reversed.push("Z");
2561
+ return reversed.join(" ");
2562
+ }
2563
+ function parsePathCommands(path) {
2564
+ const commands = [];
2565
+ const regex = /([MLCQZ])\s*([^MLCQZ]*)/gi;
2566
+ let match;
2567
+ while ((match = regex.exec(path)) !== null) {
2568
+ const type = match[1].toUpperCase();
2569
+ const args = match[2].trim().split(/[\s,]+/).filter((s) => s.length > 0).map(parseFloat);
2570
+ if (type === "M" || type === "L") {
2571
+ commands.push({ type, x: args[0], y: args[1] });
2572
+ } else if (type === "C") {
2573
+ commands.push({
2574
+ type,
2575
+ x1: args[0],
2576
+ y1: args[1],
2577
+ x2: args[2],
2578
+ y2: args[3],
2579
+ x: args[4],
2580
+ y: args[5]
2581
+ });
2582
+ } else if (type === "Q") {
2583
+ commands.push({
2584
+ type,
2585
+ x1: args[0],
2586
+ y1: args[1],
2587
+ x: args[2],
2588
+ y: args[3]
2589
+ });
2590
+ } else if (type === "Z") {
2591
+ commands.push({ type });
2592
+ }
2593
+ }
2594
+ return commands;
2595
+ }
2596
+ function computePathBounds3(path) {
2597
+ const commands = parsePathCommands(path);
2598
+ let minX = Infinity;
2599
+ let minY = Infinity;
2600
+ let maxX = -Infinity;
2601
+ let maxY = -Infinity;
2602
+ const updateBounds = (x, y) => {
2603
+ minX = Math.min(minX, x);
2604
+ minY = Math.min(minY, y);
2605
+ maxX = Math.max(maxX, x);
2606
+ maxY = Math.max(maxY, y);
2607
+ };
2608
+ for (const cmd of commands) {
2609
+ if (cmd.x !== void 0 && cmd.y !== void 0) {
2610
+ updateBounds(cmd.x, cmd.y);
2611
+ }
2612
+ if (cmd.x1 !== void 0 && cmd.y1 !== void 0) {
2613
+ updateBounds(cmd.x1, cmd.y1);
2614
+ }
2615
+ if (cmd.x2 !== void 0 && cmd.y2 !== void 0) {
2616
+ updateBounds(cmd.x2, cmd.y2);
2617
+ }
2618
+ }
2619
+ if (minX === Infinity) {
2620
+ return { x: 0, y: 0, w: 0, h: 0 };
2621
+ }
2622
+ return {
2623
+ x: minX,
2624
+ y: minY,
2625
+ w: maxX - minX,
2626
+ h: maxY - minY
2627
+ };
2628
+ }
2629
+ function translatePath(path, dx, dy) {
2630
+ return path.replace(
2631
+ /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s+(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi,
2632
+ (match, x, y) => {
2633
+ const newX = parseFloat(x) + dx;
2634
+ const newY = parseFloat(y) + dy;
2635
+ return `${newX} ${newY}`;
2636
+ }
2637
+ );
2638
+ }
2639
+ function scalePath(path, sx, sy = sx) {
2640
+ return path.replace(
2641
+ /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s+(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi,
2642
+ (match, x, y) => {
2643
+ const newX = parseFloat(x) * sx;
2644
+ const newY = parseFloat(y) * sy;
2645
+ return `${newX} ${newY}`;
2646
+ }
2647
+ );
2648
+ }
2649
+ function rotatePath(path, angleDegrees) {
2650
+ const angleRadians = angleDegrees * DEG_TO_RAD;
2651
+ const cos = Math.cos(angleRadians);
2652
+ const sin = Math.sin(angleRadians);
2653
+ return path.replace(
2654
+ /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s+(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi,
2655
+ (match, x, y) => {
2656
+ const px = parseFloat(x);
2657
+ const py = parseFloat(y);
2658
+ const newX = px * cos - py * sin;
2659
+ const newY = px * sin + py * cos;
2660
+ return `${newX} ${newY}`;
2661
+ }
2662
+ );
2663
+ }
2664
+ function centerPath(path) {
2665
+ const bounds = computePathBounds3(path);
2666
+ const cx = bounds.x + bounds.w / 2;
2667
+ const cy = bounds.y + bounds.h / 2;
2668
+ return translatePath(path, -cx, -cy);
2669
+ }
2670
+ function normalizePathToSize(path, targetWidth, targetHeight) {
2671
+ const bounds = computePathBounds3(path);
2672
+ if (bounds.w === 0 || bounds.h === 0) return path;
2673
+ const scaleX = targetWidth / bounds.w;
2674
+ const scaleY = targetHeight / bounds.h;
2675
+ const scale = Math.min(scaleX, scaleY);
2676
+ let normalized = translatePath(path, -bounds.x, -bounds.y);
2677
+ normalized = scalePath(normalized, scale);
2678
+ return normalized;
2679
+ }
2680
+
2681
+ // src/core/svg-path-utils.ts
2682
+ var TAU = Math.PI * 2;
2683
+ var PATH_COMMAND_REGEX = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
2684
+ var NUMBER_REGEX = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
2685
+ function parseSvgPath(pathData) {
2686
+ const commands = [];
2687
+ let currentX = 0;
2688
+ let currentY = 0;
2689
+ let startX = 0;
2690
+ let startY = 0;
2691
+ let lastCommand = null;
2692
+ let lastControlX = 0;
2693
+ let lastControlY = 0;
2694
+ const matches = pathData.matchAll(PATH_COMMAND_REGEX);
2695
+ for (const match of matches) {
2696
+ const commandType = match[1];
2697
+ const argsString = match[2];
2698
+ const args = extractNumbers(argsString);
2699
+ const isRelative = commandType === commandType.toLowerCase();
2700
+ switch (commandType.toUpperCase()) {
2701
+ case "M": {
2702
+ const coords = parseCoordinatePairs(args, isRelative, currentX, currentY);
2703
+ for (let i = 0; i < coords.length; i++) {
2704
+ const { x, y } = coords[i];
2705
+ if (i === 0) {
2706
+ commands.push({ type: "M", x, y });
2707
+ startX = x;
2708
+ startY = y;
2709
+ } else {
2710
+ commands.push({ type: "L", x, y });
2711
+ }
2712
+ currentX = x;
2713
+ currentY = y;
2714
+ }
2715
+ break;
2716
+ }
2717
+ case "L": {
2718
+ const coords = parseCoordinatePairs(args, isRelative, currentX, currentY);
2719
+ for (const { x, y } of coords) {
2720
+ commands.push({ type: "L", x, y });
2721
+ currentX = x;
2722
+ currentY = y;
2723
+ }
2724
+ break;
2725
+ }
2726
+ case "H": {
2727
+ for (const arg of args) {
2728
+ const x = isRelative ? currentX + arg : arg;
2729
+ commands.push({ type: "L", x, y: currentY });
2730
+ currentX = x;
2731
+ }
2732
+ break;
2733
+ }
2734
+ case "V": {
2735
+ for (const arg of args) {
2736
+ const y = isRelative ? currentY + arg : arg;
2737
+ commands.push({ type: "L", x: currentX, y });
2738
+ currentY = y;
2739
+ }
2740
+ break;
2741
+ }
2742
+ case "C": {
2743
+ for (let i = 0; i + 5 < args.length + 1; i += 6) {
2744
+ let x1 = args[i];
2745
+ let y1 = args[i + 1];
2746
+ let x2 = args[i + 2];
2747
+ let y2 = args[i + 3];
2748
+ let x = args[i + 4];
2749
+ let y = args[i + 5];
2750
+ if (isRelative) {
2751
+ x1 += currentX;
2752
+ y1 += currentY;
2753
+ x2 += currentX;
2754
+ y2 += currentY;
2755
+ x += currentX;
2756
+ y += currentY;
2757
+ }
2758
+ commands.push({ type: "C", x1, y1, x2, y2, x, y });
2759
+ lastControlX = x2;
2760
+ lastControlY = y2;
2761
+ currentX = x;
2762
+ currentY = y;
2763
+ }
2764
+ break;
2765
+ }
2766
+ case "S": {
2767
+ for (let i = 0; i + 3 < args.length + 1; i += 4) {
2768
+ let x1;
2769
+ let y1;
2770
+ if (lastCommand === "C" || lastCommand === "S") {
2771
+ x1 = 2 * currentX - lastControlX;
2772
+ y1 = 2 * currentY - lastControlY;
2773
+ } else {
2774
+ x1 = currentX;
2775
+ y1 = currentY;
2776
+ }
2777
+ let x2 = args[i];
2778
+ let y2 = args[i + 1];
2779
+ let x = args[i + 2];
2780
+ let y = args[i + 3];
2781
+ if (isRelative) {
2782
+ x2 += currentX;
2783
+ y2 += currentY;
2784
+ x += currentX;
2785
+ y += currentY;
2786
+ }
2787
+ commands.push({ type: "C", x1, y1, x2, y2, x, y });
2788
+ lastControlX = x2;
2789
+ lastControlY = y2;
2790
+ currentX = x;
2791
+ currentY = y;
2792
+ lastCommand = "S";
2793
+ }
2794
+ break;
2795
+ }
2796
+ case "Q": {
2797
+ for (let i = 0; i + 3 < args.length + 1; i += 4) {
2798
+ let x1 = args[i];
2799
+ let y1 = args[i + 1];
2800
+ let x = args[i + 2];
2801
+ let y = args[i + 3];
2802
+ if (isRelative) {
2803
+ x1 += currentX;
2804
+ y1 += currentY;
2805
+ x += currentX;
2806
+ y += currentY;
2807
+ }
2808
+ commands.push({ type: "Q", x1, y1, x, y });
2809
+ lastControlX = x1;
2810
+ lastControlY = y1;
2811
+ currentX = x;
2812
+ currentY = y;
2813
+ }
2814
+ break;
2815
+ }
2816
+ case "T": {
2817
+ for (let i = 0; i + 1 < args.length + 1; i += 2) {
2818
+ let x1;
2819
+ let y1;
2820
+ if (lastCommand === "Q" || lastCommand === "T") {
2821
+ x1 = 2 * currentX - lastControlX;
2822
+ y1 = 2 * currentY - lastControlY;
2823
+ } else {
2824
+ x1 = currentX;
2825
+ y1 = currentY;
2826
+ }
2827
+ let x = args[i];
2828
+ let y = args[i + 1];
2829
+ if (isRelative) {
2830
+ x += currentX;
2831
+ y += currentY;
2832
+ }
2833
+ commands.push({ type: "Q", x1, y1, x, y });
2834
+ lastControlX = x1;
2835
+ lastControlY = y1;
2836
+ currentX = x;
2837
+ currentY = y;
2838
+ lastCommand = "T";
2839
+ }
2840
+ break;
2841
+ }
2842
+ case "A": {
2843
+ for (let i = 0; i + 6 < args.length + 1; i += 7) {
2844
+ const rx = Math.abs(args[i]);
2845
+ const ry = Math.abs(args[i + 1]);
2846
+ const xAxisRotation = args[i + 2];
2847
+ const largeArcFlag = args[i + 3] !== 0;
2848
+ const sweepFlag = args[i + 4] !== 0;
2849
+ let x = args[i + 5];
2850
+ let y = args[i + 6];
2851
+ if (isRelative) {
2852
+ x += currentX;
2853
+ y += currentY;
2854
+ }
2855
+ commands.push({
2856
+ type: "A",
2857
+ rx,
2858
+ ry,
2859
+ xAxisRotation,
2860
+ largeArcFlag,
2861
+ sweepFlag,
2862
+ x,
2863
+ y
2864
+ });
2865
+ currentX = x;
2866
+ currentY = y;
2867
+ }
2868
+ break;
2869
+ }
2870
+ case "Z": {
2871
+ commands.push({ type: "Z" });
2872
+ currentX = startX;
2873
+ currentY = startY;
2874
+ break;
2875
+ }
2876
+ }
2877
+ lastCommand = commandType.toUpperCase();
2878
+ }
2879
+ return commands;
2880
+ }
2881
+ function normalizePath(commands) {
2882
+ const normalized = [];
2883
+ let currentX = 0;
2884
+ let currentY = 0;
2885
+ for (const cmd of commands) {
2886
+ switch (cmd.type) {
2887
+ case "M":
2888
+ case "L":
2889
+ case "C":
2890
+ case "Q":
2891
+ case "Z":
2892
+ normalized.push(cmd);
2893
+ if (cmd.type !== "Z") {
2894
+ currentX = cmd.x;
2895
+ currentY = cmd.y;
2896
+ }
2897
+ break;
2898
+ case "A": {
2899
+ const cubicCurves = arcToCubicBeziers(
2900
+ currentX,
2901
+ currentY,
2902
+ cmd.rx,
2903
+ cmd.ry,
2904
+ cmd.xAxisRotation,
2905
+ cmd.largeArcFlag,
2906
+ cmd.sweepFlag,
2907
+ cmd.x,
2908
+ cmd.y
2909
+ );
2910
+ normalized.push(...cubicCurves);
2911
+ currentX = cmd.x;
2912
+ currentY = cmd.y;
2913
+ break;
2914
+ }
2915
+ }
2916
+ }
2917
+ return normalized;
2918
+ }
2919
+ function commandsToPathString(commands) {
2920
+ const parts = [];
2921
+ for (const cmd of commands) {
2922
+ switch (cmd.type) {
2923
+ case "M":
2924
+ parts.push(`M ${cmd.x} ${cmd.y}`);
2925
+ break;
2926
+ case "L":
2927
+ parts.push(`L ${cmd.x} ${cmd.y}`);
2928
+ break;
2929
+ case "C":
2930
+ parts.push(`C ${cmd.x1} ${cmd.y1} ${cmd.x2} ${cmd.y2} ${cmd.x} ${cmd.y}`);
2931
+ break;
2932
+ case "Q":
2933
+ parts.push(`Q ${cmd.x1} ${cmd.y1} ${cmd.x} ${cmd.y}`);
2934
+ break;
2935
+ case "Z":
2936
+ parts.push("Z");
2937
+ break;
2938
+ }
2939
+ }
2940
+ return parts.join(" ");
2941
+ }
2942
+ function normalizePathString(pathData) {
2943
+ const parsed = parseSvgPath(pathData);
2944
+ const normalized = normalizePath(parsed);
2945
+ return commandsToPathString(normalized);
2946
+ }
2947
+ function extractNumbers(str) {
2948
+ const matches = str.match(NUMBER_REGEX);
2949
+ return matches ? matches.map(parseFloat) : [];
2950
+ }
2951
+ function parseCoordinatePairs(args, isRelative, currentX, currentY) {
2952
+ const coords = [];
2953
+ for (let i = 0; i + 1 < args.length + 1; i += 2) {
2954
+ let x = args[i];
2955
+ let y = args[i + 1];
2956
+ if (isRelative && coords.length === 0) {
2957
+ x += currentX;
2958
+ y += currentY;
2959
+ } else if (isRelative) {
2960
+ x += coords[coords.length - 1].x;
2961
+ y += coords[coords.length - 1].y;
2962
+ }
2963
+ coords.push({ x, y });
2964
+ }
2965
+ return coords;
2966
+ }
2967
+ function arcToCubicBeziers(x1, y1, rx, ry, phi, largeArc, sweep, x2, y2) {
2968
+ if (rx === 0 || ry === 0) {
2969
+ return [{ type: "L", x: x2, y: y2 }];
2970
+ }
2971
+ if (x1 === x2 && y1 === y2) {
2972
+ return [];
2973
+ }
2974
+ const sinPhi = Math.sin(phi * Math.PI / 180);
2975
+ const cosPhi = Math.cos(phi * Math.PI / 180);
2976
+ const x1p = cosPhi * (x1 - x2) / 2 + sinPhi * (y1 - y2) / 2;
2977
+ const y1p = -sinPhi * (x1 - x2) / 2 + cosPhi * (y1 - y2) / 2;
2978
+ const x1pSq = x1p * x1p;
2979
+ const y1pSq = y1p * y1p;
2980
+ let rxSq = rx * rx;
2981
+ let rySq = ry * ry;
2982
+ const lambda = x1pSq / rxSq + y1pSq / rySq;
2983
+ if (lambda > 1) {
2984
+ const sqrtLambda = Math.sqrt(lambda);
2985
+ rx *= sqrtLambda;
2986
+ ry *= sqrtLambda;
2987
+ rxSq = rx * rx;
2988
+ rySq = ry * ry;
2989
+ }
2990
+ let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq);
2991
+ sq = sq < 0 ? 0 : sq;
2992
+ const coef = (largeArc === sweep ? -1 : 1) * Math.sqrt(sq);
2993
+ const cxp = coef * rx * y1p / ry;
2994
+ const cyp = -coef * ry * x1p / rx;
2995
+ const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
2996
+ const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
2997
+ const theta1 = vectorAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
2998
+ let dTheta = vectorAngle(
2999
+ (x1p - cxp) / rx,
3000
+ (y1p - cyp) / ry,
3001
+ (-x1p - cxp) / rx,
3002
+ (-y1p - cyp) / ry
3003
+ );
3004
+ if (!sweep && dTheta > 0) {
3005
+ dTheta -= TAU;
3006
+ } else if (sweep && dTheta < 0) {
3007
+ dTheta += TAU;
3008
+ }
3009
+ const segments = Math.max(Math.ceil(Math.abs(dTheta) / (Math.PI / 2)), 1);
3010
+ const segmentAngle = dTheta / segments;
3011
+ const curves = [];
3012
+ let currentTheta = theta1;
3013
+ for (let i = 0; i < segments; i++) {
3014
+ const nextTheta = currentTheta + segmentAngle;
3015
+ const curve = arcSegmentToCubic(cx, cy, rx, ry, phi, currentTheta, nextTheta);
3016
+ curves.push(curve);
3017
+ currentTheta = nextTheta;
3018
+ }
3019
+ return curves;
3020
+ }
3021
+ function vectorAngle(ux, uy, vx, vy) {
3022
+ const sign = ux * vy - uy * vx < 0 ? -1 : 1;
3023
+ const dot = ux * vx + uy * vy;
3024
+ const uLen = Math.sqrt(ux * ux + uy * uy);
3025
+ const vLen = Math.sqrt(vx * vx + vy * vy);
3026
+ let cos = dot / (uLen * vLen);
3027
+ cos = Math.max(-1, Math.min(1, cos));
3028
+ return sign * Math.acos(cos);
3029
+ }
3030
+ function arcSegmentToCubic(cx, cy, rx, ry, phi, theta1, theta2) {
3031
+ const sinPhi = Math.sin(phi * Math.PI / 180);
3032
+ const cosPhi = Math.cos(phi * Math.PI / 180);
3033
+ const dTheta = theta2 - theta1;
3034
+ const t = 4 / 3 * Math.tan(dTheta / 4);
3035
+ const x1 = Math.cos(theta1);
3036
+ const y1 = Math.sin(theta1);
3037
+ const x2 = Math.cos(theta2);
3038
+ const y2 = Math.sin(theta2);
3039
+ const dx1 = -t * y1;
3040
+ const dy1 = t * x1;
3041
+ const dx2 = t * y2;
3042
+ const dy2 = -t * x2;
3043
+ const transform = (px, py) => {
3044
+ const x = rx * px;
3045
+ const y = ry * py;
3046
+ return {
3047
+ x: cosPhi * x - sinPhi * y + cx,
3048
+ y: sinPhi * x + cosPhi * y + cy
3049
+ };
3050
+ };
3051
+ const p1 = transform(x1, y1);
3052
+ const cp1 = transform(x1 + dx1, y1 + dy1);
3053
+ const cp2 = transform(x2 + dx2, y2 + dy2);
3054
+ const p2 = transform(x2, y2);
3055
+ return {
3056
+ type: "C",
3057
+ x1: cp1.x,
3058
+ y1: cp1.y,
3059
+ x2: cp2.x,
3060
+ y2: cp2.y,
3061
+ x: p2.x,
3062
+ y: p2.y
3063
+ };
3064
+ }
3065
+ function quadraticToCubic(x0, y0, qx, qy, x, y) {
3066
+ return {
3067
+ type: "C",
3068
+ x1: x0 + 2 / 3 * (qx - x0),
3069
+ y1: y0 + 2 / 3 * (qy - y0),
3070
+ x2: x + 2 / 3 * (qx - x),
3071
+ y2: y + 2 / 3 * (qy - y),
3072
+ x,
3073
+ y
3074
+ };
3075
+ }
3076
+
3077
+ // src/core/svg-import.ts
3078
+ var SVG_ATTRS_REGEX = /<svg([^>]*)>/i;
3079
+ var PATH_TAG_REGEX = /<path([^/>]*)\/?>/gi;
3080
+ var RECT_TAG_REGEX = /<rect([^/>]*)\/?>/gi;
3081
+ var CIRCLE_TAG_REGEX = /<circle([^/>]*)\/?>/gi;
3082
+ var ELLIPSE_TAG_REGEX = /<ellipse([^/>]*)\/?>/gi;
3083
+ var LINE_TAG_REGEX = /<line([^/>]*)\/?>/gi;
3084
+ var POLYLINE_TAG_REGEX = /<polyline([^/>]*)\/?>/gi;
3085
+ var POLYGON_TAG_REGEX = /<polygon([^/>]*)\/?>/gi;
3086
+ function extractAttribute(attrs, name) {
3087
+ const regex = new RegExp(`${name}\\s*=\\s*["']([^"']*)["']`, "i");
3088
+ const match = attrs.match(regex);
3089
+ return match ? match[1] : void 0;
3090
+ }
3091
+ function parseColor(color) {
3092
+ if (!color || color === "none" || color === "transparent") {
3093
+ return void 0;
3094
+ }
3095
+ color = color.trim();
3096
+ if (color.startsWith("#")) {
3097
+ if (color.length === 4) {
3098
+ const r = color[1];
3099
+ const g = color[2];
3100
+ const b = color[3];
3101
+ return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
3102
+ }
3103
+ return color.toUpperCase();
3104
+ }
3105
+ const rgbMatch = color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i);
3106
+ if (rgbMatch) {
3107
+ const r = parseInt(rgbMatch[1], 10);
3108
+ const g = parseInt(rgbMatch[2], 10);
3109
+ const b = parseInt(rgbMatch[3], 10);
3110
+ return rgbToHex(r, g, b);
3111
+ }
3112
+ const rgbaMatch = color.match(/rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)/i);
3113
+ if (rgbaMatch) {
3114
+ const r = parseInt(rgbaMatch[1], 10);
3115
+ const g = parseInt(rgbaMatch[2], 10);
3116
+ const b = parseInt(rgbaMatch[3], 10);
3117
+ return rgbToHex(r, g, b);
3118
+ }
3119
+ const namedColors = {
3120
+ black: "#000000",
3121
+ white: "#FFFFFF",
3122
+ red: "#FF0000",
3123
+ green: "#008000",
3124
+ blue: "#0000FF",
3125
+ yellow: "#FFFF00",
3126
+ cyan: "#00FFFF",
3127
+ magenta: "#FF00FF",
3128
+ gray: "#808080",
3129
+ grey: "#808080",
3130
+ orange: "#FFA500",
3131
+ purple: "#800080",
3132
+ pink: "#FFC0CB",
3133
+ brown: "#A52A2A",
3134
+ navy: "#000080",
3135
+ teal: "#008080",
3136
+ olive: "#808000",
3137
+ maroon: "#800000",
3138
+ aqua: "#00FFFF",
3139
+ lime: "#00FF00",
3140
+ silver: "#C0C0C0",
3141
+ fuchsia: "#FF00FF"
3142
+ };
3143
+ const lowerColor = color.toLowerCase();
3144
+ if (namedColors[lowerColor]) {
3145
+ return namedColors[lowerColor];
3146
+ }
3147
+ return void 0;
3148
+ }
3149
+ function rgbToHex(r, g, b) {
3150
+ const toHex = (n) => {
3151
+ const hex = Math.max(0, Math.min(255, n)).toString(16);
3152
+ return hex.length === 1 ? "0" + hex : hex;
3153
+ };
3154
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
3155
+ }
3156
+ function parseNumber(value) {
3157
+ if (!value) return void 0;
3158
+ const num = parseFloat(value);
3159
+ return isNaN(num) ? void 0 : num;
3160
+ }
3161
+ function parseViewBox(viewBox) {
3162
+ if (!viewBox) return void 0;
3163
+ const parts = viewBox.trim().split(/[\s,]+/).map(parseFloat);
3164
+ if (parts.length === 4 && parts.every((p) => !isNaN(p))) {
3165
+ return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
3166
+ }
3167
+ return void 0;
3168
+ }
3169
+ function rectToPath(attrs) {
3170
+ const x = parseNumber(extractAttribute(attrs, "x")) || 0;
3171
+ const y = parseNumber(extractAttribute(attrs, "y")) || 0;
3172
+ const width = parseNumber(extractAttribute(attrs, "width")) || 0;
3173
+ const height = parseNumber(extractAttribute(attrs, "height")) || 0;
3174
+ const rx = parseNumber(extractAttribute(attrs, "rx")) || 0;
3175
+ const ry = parseNumber(extractAttribute(attrs, "ry")) || rx;
3176
+ if (rx === 0 && ry === 0) {
3177
+ return `M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z`;
3178
+ }
3179
+ const r = Math.min(rx, ry, width / 2, height / 2);
3180
+ return [
3181
+ `M ${x + r} ${y}`,
3182
+ `L ${x + width - r} ${y}`,
3183
+ `Q ${x + width} ${y} ${x + width} ${y + r}`,
3184
+ `L ${x + width} ${y + height - r}`,
3185
+ `Q ${x + width} ${y + height} ${x + width - r} ${y + height}`,
3186
+ `L ${x + r} ${y + height}`,
3187
+ `Q ${x} ${y + height} ${x} ${y + height - r}`,
3188
+ `L ${x} ${y + r}`,
3189
+ `Q ${x} ${y} ${x + r} ${y}`,
3190
+ "Z"
3191
+ ].join(" ");
3192
+ }
3193
+ function circleToPath(attrs) {
3194
+ const cx = parseNumber(extractAttribute(attrs, "cx")) || 0;
3195
+ const cy = parseNumber(extractAttribute(attrs, "cy")) || 0;
3196
+ const r = parseNumber(extractAttribute(attrs, "r")) || 0;
3197
+ if (r === 0) return "";
3198
+ const KAPPA2 = 0.5522847498307936;
3199
+ const k = r * KAPPA2;
3200
+ return [
3201
+ `M ${cx + r} ${cy}`,
3202
+ `C ${cx + r} ${cy + k} ${cx + k} ${cy + r} ${cx} ${cy + r}`,
3203
+ `C ${cx - k} ${cy + r} ${cx - r} ${cy + k} ${cx - r} ${cy}`,
3204
+ `C ${cx - r} ${cy - k} ${cx - k} ${cy - r} ${cx} ${cy - r}`,
3205
+ `C ${cx + k} ${cy - r} ${cx + r} ${cy - k} ${cx + r} ${cy}`,
3206
+ "Z"
3207
+ ].join(" ");
3208
+ }
3209
+ function ellipseToPath(attrs) {
3210
+ const cx = parseNumber(extractAttribute(attrs, "cx")) || 0;
3211
+ const cy = parseNumber(extractAttribute(attrs, "cy")) || 0;
3212
+ const rx = parseNumber(extractAttribute(attrs, "rx")) || 0;
3213
+ const ry = parseNumber(extractAttribute(attrs, "ry")) || 0;
3214
+ if (rx === 0 || ry === 0) return "";
3215
+ const KAPPA2 = 0.5522847498307936;
3216
+ const kx = rx * KAPPA2;
3217
+ const ky = ry * KAPPA2;
3218
+ return [
3219
+ `M ${cx + rx} ${cy}`,
3220
+ `C ${cx + rx} ${cy + ky} ${cx + kx} ${cy + ry} ${cx} ${cy + ry}`,
3221
+ `C ${cx - kx} ${cy + ry} ${cx - rx} ${cy + ky} ${cx - rx} ${cy}`,
3222
+ `C ${cx - rx} ${cy - ky} ${cx - kx} ${cy - ry} ${cx} ${cy - ry}`,
3223
+ `C ${cx + kx} ${cy - ry} ${cx + rx} ${cy - ky} ${cx + rx} ${cy}`,
3224
+ "Z"
3225
+ ].join(" ");
3226
+ }
3227
+ function lineToPath(attrs) {
3228
+ const x1 = parseNumber(extractAttribute(attrs, "x1")) || 0;
3229
+ const y1 = parseNumber(extractAttribute(attrs, "y1")) || 0;
3230
+ const x2 = parseNumber(extractAttribute(attrs, "x2")) || 0;
3231
+ const y2 = parseNumber(extractAttribute(attrs, "y2")) || 0;
3232
+ return `M ${x1} ${y1} L ${x2} ${y2}`;
3233
+ }
3234
+ function polylineToPath(attrs) {
3235
+ const points = extractAttribute(attrs, "points");
3236
+ if (!points) return "";
3237
+ const coords = points.trim().split(/[\s,]+/).map(parseFloat);
3238
+ if (coords.length < 2) return "";
3239
+ const pathParts = [];
3240
+ for (let i = 0; i < coords.length; i += 2) {
3241
+ if (i + 1 < coords.length) {
3242
+ if (i === 0) {
3243
+ pathParts.push(`M ${coords[i]} ${coords[i + 1]}`);
3244
+ } else {
3245
+ pathParts.push(`L ${coords[i]} ${coords[i + 1]}`);
3246
+ }
3247
+ }
3248
+ }
3249
+ return pathParts.join(" ");
3250
+ }
3251
+ function polygonToPath(attrs) {
3252
+ const polyPath = polylineToPath(attrs);
3253
+ if (!polyPath) return "";
3254
+ return polyPath + " Z";
3255
+ }
3256
+ function extractPathAttributes(attrs) {
3257
+ const d = extractAttribute(attrs, "d") || "";
3258
+ const fill = extractAttribute(attrs, "fill");
3259
+ const fillOpacity = parseNumber(extractAttribute(attrs, "fill-opacity"));
3260
+ const stroke = extractAttribute(attrs, "stroke");
3261
+ const strokeWidth = parseNumber(extractAttribute(attrs, "stroke-width"));
3262
+ const strokeOpacity = parseNumber(extractAttribute(attrs, "stroke-opacity"));
3263
+ const opacity = parseNumber(extractAttribute(attrs, "opacity"));
3264
+ const transform = extractAttribute(attrs, "transform");
3265
+ const style = extractAttribute(attrs, "style");
3266
+ let styleFill = fill;
3267
+ let styleStroke = stroke;
3268
+ let styleStrokeWidth = strokeWidth;
3269
+ let styleOpacity = opacity;
3270
+ if (style) {
3271
+ const fillMatch = style.match(/fill\s*:\s*([^;]+)/i);
3272
+ if (fillMatch) styleFill = fillMatch[1].trim();
3273
+ const strokeMatch = style.match(/stroke\s*:\s*([^;]+)/i);
3274
+ if (strokeMatch) styleStroke = strokeMatch[1].trim();
3275
+ const strokeWidthMatch = style.match(/stroke-width\s*:\s*([^;]+)/i);
3276
+ if (strokeWidthMatch) styleStrokeWidth = parseNumber(strokeWidthMatch[1].trim());
3277
+ const opacityMatch = style.match(/opacity\s*:\s*([^;]+)/i);
3278
+ if (opacityMatch) styleOpacity = parseNumber(opacityMatch[1].trim());
3279
+ }
3280
+ return {
3281
+ d,
3282
+ fill: styleFill,
3283
+ fillOpacity,
3284
+ stroke: styleStroke,
3285
+ strokeWidth: styleStrokeWidth,
3286
+ strokeOpacity,
3287
+ opacity: styleOpacity,
3288
+ transform
3289
+ };
3290
+ }
3291
+ function extractShapeAsPath(tagAttrs, convertFn) {
3292
+ const d = convertFn(tagAttrs);
3293
+ if (!d) return null;
3294
+ const pathAttrs = extractPathAttributes(tagAttrs);
3295
+ pathAttrs.d = d;
3296
+ return pathAttrs;
3297
+ }
3298
+ function parseSvgMarkup(svgString) {
3299
+ const svgMatch = svgString.match(SVG_ATTRS_REGEX);
3300
+ const svgAttrs = svgMatch ? svgMatch[1] : "";
3301
+ const widthAttr = extractAttribute(svgAttrs, "width");
3302
+ const heightAttr = extractAttribute(svgAttrs, "height");
3303
+ const viewBoxAttr = extractAttribute(svgAttrs, "viewBox");
3304
+ let width = parseNumber(widthAttr?.replace(/px$/, "")) || 0;
3305
+ let height = parseNumber(heightAttr?.replace(/px$/, "")) || 0;
3306
+ const viewBox = parseViewBox(viewBoxAttr);
3307
+ if (viewBox && (!width || !height)) {
3308
+ width = width || viewBox.width;
3309
+ height = height || viewBox.height;
3310
+ }
3311
+ const paths = [];
3312
+ let match;
3313
+ PATH_TAG_REGEX.lastIndex = 0;
3314
+ while ((match = PATH_TAG_REGEX.exec(svgString)) !== null) {
3315
+ const pathAttrs = extractPathAttributes(match[1]);
3316
+ if (pathAttrs.d) {
3317
+ paths.push(pathAttrs);
3318
+ }
3319
+ }
3320
+ RECT_TAG_REGEX.lastIndex = 0;
3321
+ while ((match = RECT_TAG_REGEX.exec(svgString)) !== null) {
3322
+ const path = extractShapeAsPath(match[1], rectToPath);
3323
+ if (path) paths.push(path);
3324
+ }
3325
+ CIRCLE_TAG_REGEX.lastIndex = 0;
3326
+ while ((match = CIRCLE_TAG_REGEX.exec(svgString)) !== null) {
3327
+ const path = extractShapeAsPath(match[1], circleToPath);
3328
+ if (path) paths.push(path);
3329
+ }
3330
+ ELLIPSE_TAG_REGEX.lastIndex = 0;
3331
+ while ((match = ELLIPSE_TAG_REGEX.exec(svgString)) !== null) {
3332
+ const path = extractShapeAsPath(match[1], ellipseToPath);
3333
+ if (path) paths.push(path);
3334
+ }
3335
+ LINE_TAG_REGEX.lastIndex = 0;
3336
+ while ((match = LINE_TAG_REGEX.exec(svgString)) !== null) {
3337
+ const path = extractShapeAsPath(match[1], lineToPath);
3338
+ if (path) paths.push(path);
3339
+ }
3340
+ POLYLINE_TAG_REGEX.lastIndex = 0;
3341
+ while ((match = POLYLINE_TAG_REGEX.exec(svgString)) !== null) {
3342
+ const path = extractShapeAsPath(match[1], polylineToPath);
3343
+ if (path) paths.push(path);
3344
+ }
3345
+ POLYGON_TAG_REGEX.lastIndex = 0;
3346
+ while ((match = POLYGON_TAG_REGEX.exec(svgString)) !== null) {
3347
+ const path = extractShapeAsPath(match[1], polygonToPath);
3348
+ if (path) paths.push(path);
3349
+ }
3350
+ return {
3351
+ width,
3352
+ height,
3353
+ viewBox,
3354
+ paths
3355
+ };
3356
+ }
3357
+ function svgToAsset(svgString) {
3358
+ const parsed = parseSvgMarkup(svgString);
3359
+ if (parsed.paths.length === 0) {
3360
+ return {
3361
+ type: "svg",
3362
+ shape: {
3363
+ type: "path",
3364
+ d: ""
3365
+ },
3366
+ width: parsed.width || void 0,
3367
+ height: parsed.height || void 0
3368
+ };
3369
+ }
3370
+ if (parsed.paths.length === 1) {
3371
+ return pathToAsset(parsed.paths[0], parsed.width, parsed.height);
3372
+ }
3373
+ return parsed.paths.map((path) => pathToAsset(path, parsed.width, parsed.height));
3374
+ }
3375
+ function pathToAsset(path, width, height) {
3376
+ const asset = {
3377
+ type: "svg",
3378
+ shape: {
3379
+ type: "path",
3380
+ d: path.d
3381
+ }
3382
+ };
3383
+ const fillColor = parseColor(path.fill);
3384
+ if (fillColor) {
3385
+ asset.fill = {
3386
+ type: "solid",
3387
+ color: fillColor
3388
+ };
3389
+ if (path.fillOpacity !== void 0 && path.fillOpacity !== 1) {
3390
+ asset.fill.opacity = path.fillOpacity;
3391
+ }
3392
+ }
3393
+ const strokeColor = parseColor(path.stroke);
3394
+ if (strokeColor && path.strokeWidth && path.strokeWidth > 0) {
3395
+ asset.stroke = {
3396
+ color: strokeColor,
3397
+ width: path.strokeWidth
3398
+ };
3399
+ if (path.strokeOpacity !== void 0 && path.strokeOpacity !== 1) {
3400
+ asset.stroke.opacity = path.strokeOpacity;
3401
+ }
3402
+ }
3403
+ if (path.opacity !== void 0 && path.opacity !== 1) {
3404
+ asset.opacity = path.opacity;
3405
+ }
3406
+ if (width > 0) {
3407
+ asset.width = width;
3408
+ }
3409
+ if (height > 0) {
3410
+ asset.height = height;
3411
+ }
3412
+ return asset;
3413
+ }
3414
+ function svgToSingleAsset(svgString) {
3415
+ const parsed = parseSvgMarkup(svgString);
3416
+ if (parsed.paths.length === 0) {
3417
+ return {
3418
+ type: "svg",
3419
+ shape: {
3420
+ type: "path",
3421
+ d: ""
3422
+ },
3423
+ width: parsed.width || void 0,
3424
+ height: parsed.height || void 0
3425
+ };
3426
+ }
3427
+ const combinedD = parsed.paths.map((p) => p.d).join(" ");
3428
+ const firstPathWithFill = parsed.paths.find((p) => p.fill && p.fill !== "none");
3429
+ const firstPathWithStroke = parsed.paths.find((p) => p.stroke && p.stroke !== "none");
3430
+ const asset = {
3431
+ type: "svg",
3432
+ shape: {
3433
+ type: "path",
3434
+ d: combinedD
3435
+ }
3436
+ };
3437
+ if (firstPathWithFill) {
3438
+ const fillColor = parseColor(firstPathWithFill.fill);
3439
+ if (fillColor) {
3440
+ asset.fill = {
3441
+ type: "solid",
3442
+ color: fillColor
3443
+ };
3444
+ if (firstPathWithFill.fillOpacity !== void 0 && firstPathWithFill.fillOpacity !== 1) {
3445
+ asset.fill.opacity = firstPathWithFill.fillOpacity;
3446
+ }
3447
+ }
3448
+ }
3449
+ if (firstPathWithStroke) {
3450
+ const strokeColor = parseColor(firstPathWithStroke.stroke);
3451
+ if (strokeColor && firstPathWithStroke.strokeWidth && firstPathWithStroke.strokeWidth > 0) {
3452
+ asset.stroke = {
3453
+ color: strokeColor,
3454
+ width: firstPathWithStroke.strokeWidth
3455
+ };
3456
+ if (firstPathWithStroke.strokeOpacity !== void 0 && firstPathWithStroke.strokeOpacity !== 1) {
3457
+ asset.stroke.opacity = firstPathWithStroke.strokeOpacity;
3458
+ }
3459
+ }
3460
+ }
3461
+ const firstPathWithOpacity = parsed.paths.find((p) => p.opacity !== void 0 && p.opacity !== 1);
3462
+ if (firstPathWithOpacity) {
3463
+ asset.opacity = firstPathWithOpacity.opacity;
3464
+ }
3465
+ if (parsed.width > 0) {
3466
+ asset.width = parsed.width;
3467
+ }
3468
+ if (parsed.height > 0) {
3469
+ asset.height = parsed.height;
3470
+ }
3471
+ return asset;
3472
+ }
3473
+ function importSvg(svgString) {
3474
+ return svgToSingleAsset(svgString);
3475
+ }
3476
+
2089
3477
  // src/env/entry.web.ts
2090
3478
  var DEFAULT_ROBOTO_URL = "https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxP.ttf";
2091
3479
  async function createTextEngine(opts = {}) {
@@ -2213,12 +3601,17 @@ async function createTextEngine(opts = {}) {
2213
3601
  const desc = { family: main.family, weight: `${main.weight}` };
2214
3602
  let lines;
2215
3603
  try {
2216
- const emojiDesc = { family: "NotoEmoji", weight: "400" };
2217
- let emojiAvailable = false;
2218
- try {
2219
- await fonts.getFace(emojiDesc);
2220
- emojiAvailable = true;
2221
- } catch {
3604
+ let emojiFallback = void 0;
3605
+ const colorEmojiFamilies = fonts.getColorEmojiFontFamilies();
3606
+ if (colorEmojiFamilies.length > 0) {
3607
+ emojiFallback = { family: colorEmojiFamilies[0], weight: "400" };
3608
+ } else {
3609
+ const notoEmojiDesc = { family: "NotoEmoji", weight: "400" };
3610
+ try {
3611
+ await fonts.getFace(notoEmojiDesc);
3612
+ emojiFallback = notoEmojiDesc;
3613
+ } catch {
3614
+ }
2222
3615
  }
2223
3616
  const padding2 = asset.padding ? typeof asset.padding === "number" ? {
2224
3617
  top: asset.padding,
@@ -2234,7 +3627,7 @@ async function createTextEngine(opts = {}) {
2234
3627
  lineHeight: asset.style?.lineHeight ?? 1.2,
2235
3628
  desc,
2236
3629
  textTransform: asset.style?.textTransform ?? "none",
2237
- emojiFallback: emojiAvailable ? emojiDesc : void 0
3630
+ emojiFallback
2238
3631
  });
2239
3632
  } catch (err) {
2240
3633
  throw new Error(
@@ -2289,7 +3682,8 @@ async function createTextEngine(opts = {}) {
2289
3682
  border: asset.border,
2290
3683
  padding: asset.padding,
2291
3684
  glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2292
- getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
3685
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc),
3686
+ isColorEmojiFont: (family) => fonts.isColorEmojiFont(family)
2293
3687
  });
2294
3688
  } catch (err) {
2295
3689
  throw new Error(
@@ -2342,7 +3736,50 @@ async function createTextEngine(opts = {}) {
2342
3736
  }
2343
3737
  export {
2344
3738
  CanvasRichTextAssetSchema,
3739
+ CanvasSvgAssetSchema,
3740
+ arcToCubicBeziers,
3741
+ centerPath,
3742
+ commandsToPathString,
3743
+ computePathBounds3 as computePathBounds,
2345
3744
  createTextEngine,
3745
+ createWebPainter,
3746
+ generateArrowPath,
3747
+ generateCirclePath,
3748
+ generateCrossPath,
3749
+ generateEllipsePath,
3750
+ generateHeartPath,
3751
+ generateLinePath,
3752
+ generatePolygonPath,
3753
+ generatePolygonPoints,
3754
+ generateRectanglePath,
3755
+ generateRingPath,
3756
+ generateShapePath,
3757
+ generateStarPath,
3758
+ generateStarPoints,
3759
+ generateTrianglePath,
3760
+ importSvg,
2346
3761
  isGlyphFill2 as isGlyphFill,
2347
- isShadowFill2 as isShadowFill
3762
+ isShadowFill2 as isShadowFill,
3763
+ normalizePath,
3764
+ normalizePathString,
3765
+ normalizePathToSize,
3766
+ parseSvgMarkup,
3767
+ parseSvgPath,
3768
+ pointsToPath,
3769
+ quadraticToCubic,
3770
+ reverseWindingOrder,
3771
+ rotatePath,
3772
+ scalePath,
3773
+ svgAssetSchema,
3774
+ svgGradientStopSchema,
3775
+ svgLinearGradientFillSchema,
3776
+ svgRadialGradientFillSchema,
3777
+ svgShadowSchema,
3778
+ svgShapeSchema,
3779
+ svgSolidFillSchema,
3780
+ svgStrokeSchema,
3781
+ svgToAsset,
3782
+ svgToSingleAsset,
3783
+ svgTransformSchema,
3784
+ translatePath
2348
3785
  };