@effing/canvas 0.23.0 → 0.23.2

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/index.js CHANGED
@@ -3,7 +3,7 @@ import { createCanvas as _createCanvas } from "@napi-rs/canvas";
3
3
  import {
4
4
  Canvas,
5
5
  GlobalFonts as GlobalFonts2,
6
- loadImage as loadImage5,
6
+ loadImage as loadImage3,
7
7
  Image
8
8
  } from "@napi-rs/canvas";
9
9
 
@@ -20,8 +20,17 @@ function renderLottieFrame(ctx, animation, frame) {
20
20
  animation.render(ctx);
21
21
  }
22
22
 
23
- // src/jsx/draw/index.ts
24
- import { loadImage as loadImage3 } from "@napi-rs/canvas";
23
+ // src/jsx/image-cache.ts
24
+ import { loadImage } from "@napi-rs/canvas";
25
+ function cachedLoadImage(cache, src) {
26
+ if (Buffer.isBuffer(src)) return loadImage(src);
27
+ let entry = cache.get(src);
28
+ if (!entry) {
29
+ entry = loadImage(src);
30
+ cache.set(src, entry);
31
+ }
32
+ return entry;
33
+ }
25
34
 
26
35
  // src/jsx/style/compute.ts
27
36
  var ROOT_FONT_SIZE = 16;
@@ -627,6 +636,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
627
636
  const segments = [];
628
637
  let totalHeight = 0;
629
638
  let maxLineWidth = 0;
639
+ const isAutoLineHeight = style.lineHeight === void 0 || style.lineHeight === "normal";
640
+ let lastMetrics;
630
641
  for (let i = 0; i < lines.length; i++) {
631
642
  const line = lines[i];
632
643
  const lineWidth = measure(line);
@@ -644,14 +655,16 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
644
655
  fontStyle,
645
656
  ctx
646
657
  );
647
- let baselineY;
648
- if (fontMetrics) {
649
- const typoAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
650
- const typoDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
651
- baselineY = totalHeight + (lineHeightPx + typoAscent - typoDescent) / 2;
652
- } else {
653
- baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
654
- }
658
+ lastMetrics = metrics;
659
+ const contentHeight = metrics.ascent + metrics.descent;
660
+ let effectiveAscent = metrics.ascent;
661
+ let effectiveDescent = metrics.descent;
662
+ if (isAutoLineHeight && contentHeight > lineHeightPx) {
663
+ const scale = lineHeightPx / contentHeight;
664
+ effectiveAscent = metrics.ascent * scale;
665
+ effectiveDescent = metrics.descent * scale;
666
+ }
667
+ const baselineY = totalHeight + (lineHeightPx + effectiveAscent - effectiveDescent) / 2;
655
668
  segments.push({
656
669
  text: line,
657
670
  x,
@@ -673,6 +686,14 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
673
686
  totalHeight += lineHeightPx;
674
687
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
675
688
  }
689
+ if (isAutoLineHeight && lastMetrics && segments.length > 0) {
690
+ const lastSeg = segments[segments.length - 1];
691
+ const descentOverflow = lastSeg.y + lastMetrics.descent - totalHeight;
692
+ if (descentOverflow > 0) {
693
+ totalHeight += descentOverflow;
694
+ }
695
+ totalHeight = Math.ceil(totalHeight);
696
+ }
676
697
  const textBoxTrim = style.textBoxTrim;
677
698
  if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
678
699
  const textBoxEdge = style.textBoxEdge ?? "text";
@@ -1004,9 +1025,6 @@ function splitGradientArgs(content) {
1004
1025
  return parts;
1005
1026
  }
1006
1027
 
1007
- // src/jsx/draw/image.ts
1008
- import { loadImage } from "@napi-rs/canvas";
1009
-
1010
1028
  // src/jsx/draw/object-fit.ts
1011
1029
  function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
1012
1030
  const imgRatio = imgW / imgH;
@@ -1042,8 +1060,8 @@ function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
1042
1060
  }
1043
1061
 
1044
1062
  // src/jsx/draw/image.ts
1045
- async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
1046
- const image = preloadedImage ?? await loadImage(src);
1063
+ async function drawImage(ctx, src, x, y, width, height, imageCache, style) {
1064
+ const image = await cachedLoadImage(imageCache, src);
1047
1065
  const objectFit = style?.objectFit ?? "fill";
1048
1066
  if (objectFit === "fill") {
1049
1067
  ctx.drawImage(image, x, y, width, height);
@@ -1359,9 +1377,7 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1359
1377
  ctx.restore();
1360
1378
  }
1361
1379
 
1362
- // src/jsx/draw/svg.ts
1363
- import { Path2D } from "@napi-rs/canvas";
1364
- import parseCssColor from "parse-css-color";
1380
+ // src/jsx/draw/svg/style.ts
1365
1381
  function resolveCurrentColor(value, color) {
1366
1382
  if (value?.toLowerCase() === "currentcolor") return color;
1367
1383
  return value;
@@ -1371,6 +1387,37 @@ function mergeStyleIntoProps(props) {
1371
1387
  if (!style) return props;
1372
1388
  return { ...props, ...style };
1373
1389
  }
1390
+ function svgLength(value, reference, fallback = 0) {
1391
+ if (value == null) return fallback;
1392
+ const s = String(value);
1393
+ if (s.endsWith("%")) return parseFloat(s) / 100 * reference;
1394
+ const n = Number(s);
1395
+ return isNaN(n) ? fallback : n;
1396
+ }
1397
+ function parseUrlRef(value) {
1398
+ if (typeof value !== "string") return void 0;
1399
+ const m = value.match(/^url\(#(.+)\)$/);
1400
+ return m?.[1];
1401
+ }
1402
+ function resolveInheritedStyle(merged, color, parent) {
1403
+ return {
1404
+ fill: resolveCurrentColor(merged.fill, color) ?? parent?.fill ?? "black",
1405
+ stroke: resolveCurrentColor(merged.stroke, color) ?? parent?.stroke,
1406
+ strokeWidth: merged.strokeWidth ?? merged["stroke-width"] ?? parent?.strokeWidth,
1407
+ strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"] ?? parent?.strokeLinecap,
1408
+ strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? parent?.strokeLinejoin,
1409
+ strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? parent?.strokeOpacity
1410
+ };
1411
+ }
1412
+
1413
+ // src/jsx/draw/svg/tree.ts
1414
+ function normalizeChildren(node) {
1415
+ const raw = node.children ?? node.props.children;
1416
+ if (raw == null) return [];
1417
+ return Array.isArray(raw) ? raw : [raw];
1418
+ }
1419
+
1420
+ // src/jsx/draw/svg/defs.ts
1374
1421
  function collectDefs(children) {
1375
1422
  const clips = /* @__PURE__ */ new Map();
1376
1423
  const gradients = /* @__PURE__ */ new Map();
@@ -1394,122 +1441,85 @@ function collectDefs(children) {
1394
1441
  }
1395
1442
  return { clips, gradients, masks, filters };
1396
1443
  }
1397
- function parseUrlRef(value) {
1398
- if (typeof value !== "string") return void 0;
1399
- const m = value.match(/^url\(#(.+)\)$/);
1400
- return m?.[1];
1401
- }
1402
- function normalizeChildren(node) {
1403
- const raw = node.children ?? node.props.children;
1404
- if (raw == null) return [];
1405
- return Array.isArray(raw) ? raw : [raw];
1406
- }
1407
- function svgLength(value, reference, fallback = 0) {
1408
- if (value == null) return fallback;
1409
- const s = String(value);
1410
- if (s.endsWith("%")) return parseFloat(s) / 100 * reference;
1411
- const n = Number(s);
1412
- return isNaN(n) ? fallback : n;
1413
- }
1414
- function parseFrac(value, fallback) {
1415
- if (value == null) return fallback;
1416
- const s = String(value);
1417
- if (s.endsWith("%")) return parseFloat(s) / 100;
1418
- return Number(s);
1419
- }
1420
- function applyOpacity(color, opacity) {
1421
- if (opacity >= 1) return color;
1422
- const parsed = parseCssColor(color);
1423
- if (!parsed) return color;
1424
- const [r, g, b] = parsed.values;
1425
- return `rgba(${r}, ${g}, ${b}, ${parsed.alpha * opacity})`;
1426
- }
1427
- function addGradientStops(gradient, def) {
1428
- for (const stop of normalizeChildren(def)) {
1429
- if (stop.type !== "stop") continue;
1430
- const sp = stop.props;
1431
- const offsetRaw = sp.offset ?? 0;
1432
- const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
1433
- const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
1434
- const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
1435
- gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
1436
- }
1437
- }
1438
- function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
1439
- const props = def.props;
1440
- if (def.type === "linearGradient") {
1441
- const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
1442
- const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
1443
- const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
1444
- const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
1445
- const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1446
- addGradientStops(gradient, def);
1447
- ctx.fillStyle = gradient;
1448
- ctx.fill(path, fillRule);
1449
- return true;
1450
- }
1451
- if (def.type === "radialGradient") {
1452
- const cxF = parseFrac(props.cx, 0.5);
1453
- const cyF = parseFrac(props.cy, 0.5);
1454
- const rF = parseFrac(props.r, 0.5);
1455
- const fxF = parseFrac(props.fx, cxF);
1456
- const fyF = parseFrac(props.fy, cyF);
1457
- ctx.save();
1458
- ctx.translate(bbox.x, bbox.y);
1459
- ctx.scale(bbox.width, bbox.height);
1460
- const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
1461
- addGradientStops(gradient, def);
1462
- ctx.fillStyle = gradient;
1463
- const unitPath = new Path2D();
1464
- const invTransform = {
1465
- a: 1 / bbox.width,
1466
- b: 0,
1467
- c: 0,
1468
- d: 1 / bbox.height,
1469
- e: -bbox.x / bbox.width,
1470
- f: -bbox.y / bbox.height
1471
- };
1472
- unitPath.addPath(path, invTransform);
1473
- ctx.fill(unitPath, fillRule);
1474
- ctx.restore();
1475
- return true;
1476
- }
1477
- return false;
1444
+ var EMPTY_DEFS = {
1445
+ clips: /* @__PURE__ */ new Map(),
1446
+ gradients: /* @__PURE__ */ new Map(),
1447
+ masks: /* @__PURE__ */ new Map(),
1448
+ filters: /* @__PURE__ */ new Map()
1449
+ };
1450
+
1451
+ // src/jsx/draw/svg/transform.ts
1452
+ function applyTransformAndOpacity(ctx, transform, opacity) {
1453
+ if (!transform && opacity >= 1) return false;
1454
+ ctx.save();
1455
+ if (transform) applySvgTransform(ctx, transform);
1456
+ if (opacity < 1) ctx.globalAlpha *= opacity;
1457
+ return true;
1478
1458
  }
1479
- function strokeWithSvgGradient(ctx, def, bbox, path) {
1480
- const props = def.props;
1481
- if (def.type === "linearGradient") {
1482
- const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
1483
- const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
1484
- const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
1485
- const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
1486
- const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1487
- addGradientStops(gradient, def);
1488
- ctx.strokeStyle = gradient;
1489
- ctx.stroke(path);
1490
- return true;
1491
- }
1492
- if (def.type === "radialGradient") {
1493
- const cxAbs = bbox.x + parseFrac(props.cx, 0.5) * bbox.width;
1494
- const cyAbs = bbox.y + parseFrac(props.cy, 0.5) * bbox.height;
1495
- const rAbs = parseFrac(props.r, 0.5) * Math.sqrt(bbox.width * bbox.height);
1496
- const fxAbs = bbox.x + parseFrac(props.fx, 0.5) * bbox.width;
1497
- const fyAbs = bbox.y + parseFrac(props.fy, 0.5) * bbox.height;
1498
- const gradient = ctx.createRadialGradient(
1499
- fxAbs,
1500
- fyAbs,
1501
- 0,
1502
- cxAbs,
1503
- cyAbs,
1504
- rAbs
1505
- );
1506
- addGradientStops(gradient, def);
1507
- ctx.strokeStyle = gradient;
1508
- ctx.stroke(path);
1509
- return true;
1459
+ function applySvgTransform(ctx, transform) {
1460
+ const re = /\b(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)/g;
1461
+ let match;
1462
+ while ((match = re.exec(transform)) !== null) {
1463
+ const fn = match[1];
1464
+ const args = match[2].split(/[\s,]+/).filter(Boolean).map(Number);
1465
+ switch (fn) {
1466
+ case "translate":
1467
+ ctx.translate(args[0] ?? 0, args[1] ?? 0);
1468
+ break;
1469
+ case "scale": {
1470
+ const sx = args[0] ?? 1;
1471
+ ctx.scale(sx, args[1] ?? sx);
1472
+ break;
1473
+ }
1474
+ case "rotate": {
1475
+ const angle = (args[0] ?? 0) * Math.PI / 180;
1476
+ if (args.length >= 3) {
1477
+ const cx = args[1];
1478
+ const cy = args[2];
1479
+ ctx.translate(cx, cy);
1480
+ ctx.rotate(angle);
1481
+ ctx.translate(-cx, -cy);
1482
+ } else {
1483
+ ctx.rotate(angle);
1484
+ }
1485
+ break;
1486
+ }
1487
+ case "skewX":
1488
+ ctx.transform(
1489
+ 1,
1490
+ 0,
1491
+ Math.tan((args[0] ?? 0) * Math.PI / 180),
1492
+ 1,
1493
+ 0,
1494
+ 0
1495
+ );
1496
+ break;
1497
+ case "skewY":
1498
+ ctx.transform(
1499
+ 1,
1500
+ Math.tan((args[0] ?? 0) * Math.PI / 180),
1501
+ 0,
1502
+ 1,
1503
+ 0,
1504
+ 0
1505
+ );
1506
+ break;
1507
+ case "matrix":
1508
+ ctx.transform(
1509
+ args[0] ?? 1,
1510
+ args[1] ?? 0,
1511
+ args[2] ?? 0,
1512
+ args[3] ?? 1,
1513
+ args[4] ?? 0,
1514
+ args[5] ?? 0
1515
+ );
1516
+ break;
1517
+ }
1510
1518
  }
1511
- return false;
1512
1519
  }
1520
+
1521
+ // src/jsx/draw/svg/path.ts
1522
+ import { Path2D } from "@napi-rs/canvas";
1513
1523
  function pathBBox(d) {
1514
1524
  let minX = Infinity;
1515
1525
  let minY = Infinity;
@@ -1739,53 +1749,17 @@ function buildClipPath(shapes, vbW, vbH) {
1739
1749
  }
1740
1750
  return combined;
1741
1751
  }
1742
- function drawSvgContainer(ctx, node, x, y, width, height) {
1743
- ctx.save();
1744
- ctx.translate(x, y);
1745
- const viewBox = node.props.viewBox;
1746
- let vbW = width;
1747
- let vbH = height;
1748
- if (viewBox) {
1749
- const parts = viewBox.split(/[\s,]+/).map(Number);
1750
- if (parts.length === 4) {
1751
- const [vbX, vbY, parsedW, parsedH] = parts;
1752
- vbW = parsedW;
1753
- vbH = parsedH;
1754
- const scaleX = width / vbW;
1755
- const scaleY = height / vbH;
1756
- ctx.scale(scaleX, scaleY);
1757
- ctx.translate(-vbX, -vbY);
1758
- }
1759
- }
1760
- const color = node.style.color ?? "black";
1761
- const merged = mergeStyleIntoProps(node.props);
1762
- const inherited = {
1763
- fill: resolveCurrentColor(merged.fill, color) ?? "black",
1764
- stroke: resolveCurrentColor(merged.stroke, color),
1765
- strokeWidth: merged.strokeWidth ?? merged["stroke-width"],
1766
- strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"],
1767
- strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"],
1768
- strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"]
1769
- };
1770
- const children = node.props.children;
1771
- if (children != null) {
1772
- const childArray = Array.isArray(children) ? children : [children];
1773
- const svgChildren = childArray.filter(
1774
- (c) => c != null && typeof c === "object"
1775
- );
1776
- const defs = collectDefs(svgChildren);
1777
- for (const child of svgChildren) {
1778
- drawSvgChild(ctx, child, inherited, color, defs, vbW, vbH);
1779
- }
1752
+ function parsePoints(value) {
1753
+ if (!value) return [];
1754
+ const nums = value.trim().split(/[\s,]+/).map(Number);
1755
+ const result = [];
1756
+ for (let i = 0; i + 1 < nums.length; i += 2) {
1757
+ result.push([nums[i], nums[i + 1]]);
1780
1758
  }
1781
- ctx.restore();
1759
+ return result;
1782
1760
  }
1783
- var EMPTY_DEFS = {
1784
- clips: /* @__PURE__ */ new Map(),
1785
- gradients: /* @__PURE__ */ new Map(),
1786
- masks: /* @__PURE__ */ new Map(),
1787
- filters: /* @__PURE__ */ new Map()
1788
- };
1761
+
1762
+ // src/jsx/draw/svg/filters.ts
1789
1763
  function applyFeOffset(input, dx, dy, w, h) {
1790
1764
  const [canvas, ctx] = acquireOffscreen(w, h);
1791
1765
  ctx.drawImage(input, dx, dy);
@@ -1845,18 +1819,18 @@ function applyFeColorMatrix(input, type, values, w, h) {
1845
1819
  }
1846
1820
  return canvas;
1847
1821
  }
1822
+ var BLEND_MODE_MAP = {
1823
+ normal: "source-over",
1824
+ multiply: "multiply",
1825
+ screen: "screen",
1826
+ darken: "darken",
1827
+ lighten: "lighten",
1828
+ overlay: "overlay"
1829
+ };
1848
1830
  function applyFeBlend(in1, in2, mode, w, h) {
1849
- const modeMap = {
1850
- normal: "source-over",
1851
- multiply: "multiply",
1852
- screen: "screen",
1853
- darken: "darken",
1854
- lighten: "lighten",
1855
- overlay: "overlay"
1856
- };
1857
1831
  const [canvas, ctx] = acquireOffscreen(w, h);
1858
1832
  ctx.drawImage(in2, 0, 0);
1859
- ctx.globalCompositeOperation = modeMap[mode] ?? "source-over";
1833
+ ctx.globalCompositeOperation = BLEND_MODE_MAP[mode] ?? "source-over";
1860
1834
  ctx.drawImage(in1, 0, 0);
1861
1835
  ctx.globalCompositeOperation = "source-over";
1862
1836
  return canvas;
@@ -1932,103 +1906,181 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
1932
1906
  }
1933
1907
  return lastResult;
1934
1908
  }
1935
- function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
1936
- const { type } = child;
1937
- const props = mergeStyleIntoProps(child.props);
1938
- if (type === "defs" || type === "clipPath" || type === "mask" || type === "filter")
1939
- return;
1940
- const transform = type !== "g" ? props.transform : void 0;
1941
- const opacity = type !== "g" ? Number(props.opacity ?? 1) : 1;
1942
- if (transform || opacity < 1) {
1909
+
1910
+ // src/jsx/draw/svg/shapes.ts
1911
+ import { Path2D as Path2D3 } from "@napi-rs/canvas";
1912
+
1913
+ // src/jsx/draw/svg/gradients.ts
1914
+ import { Path2D as Path2D2 } from "@napi-rs/canvas";
1915
+ import parseCssColor from "parse-css-color";
1916
+ function parseFrac(value, fallback) {
1917
+ if (value == null) return fallback;
1918
+ const s = String(value);
1919
+ if (s.endsWith("%")) return parseFloat(s) / 100;
1920
+ return Number(s);
1921
+ }
1922
+ function applyOpacity(color, opacity) {
1923
+ if (opacity >= 1) return color;
1924
+ const parsed = parseCssColor(color);
1925
+ if (!parsed) return color;
1926
+ const [r, g, b] = parsed.values;
1927
+ return `rgba(${r}, ${g}, ${b}, ${parsed.alpha * opacity})`;
1928
+ }
1929
+ function addGradientStops(gradient, def) {
1930
+ for (const stop of normalizeChildren(def)) {
1931
+ if (stop.type !== "stop") continue;
1932
+ const sp = stop.props;
1933
+ const offsetRaw = sp.offset ?? 0;
1934
+ const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
1935
+ const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
1936
+ const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
1937
+ gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
1938
+ }
1939
+ }
1940
+ function createLinearGradientFromBBox(ctx, props, bbox, def) {
1941
+ const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
1942
+ const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
1943
+ const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
1944
+ const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
1945
+ const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1946
+ addGradientStops(gradient, def);
1947
+ return gradient;
1948
+ }
1949
+ function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
1950
+ const props = def.props;
1951
+ if (def.type === "linearGradient") {
1952
+ ctx.fillStyle = createLinearGradientFromBBox(ctx, props, bbox, def);
1953
+ ctx.fill(path, fillRule);
1954
+ return true;
1955
+ }
1956
+ if (def.type === "radialGradient") {
1957
+ const cxF = parseFrac(props.cx, 0.5);
1958
+ const cyF = parseFrac(props.cy, 0.5);
1959
+ const rF = parseFrac(props.r, 0.5);
1960
+ const fxF = parseFrac(props.fx, cxF);
1961
+ const fyF = parseFrac(props.fy, cyF);
1943
1962
  ctx.save();
1944
- if (transform) applySvgTransform(ctx, transform);
1945
- if (opacity < 1) ctx.globalAlpha *= opacity;
1963
+ ctx.translate(bbox.x, bbox.y);
1964
+ ctx.scale(bbox.width, bbox.height);
1965
+ const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
1966
+ addGradientStops(gradient, def);
1967
+ ctx.fillStyle = gradient;
1968
+ const unitPath = new Path2D2();
1969
+ const invTransform = {
1970
+ a: 1 / bbox.width,
1971
+ b: 0,
1972
+ c: 0,
1973
+ d: 1 / bbox.height,
1974
+ e: -bbox.x / bbox.width,
1975
+ f: -bbox.y / bbox.height
1976
+ };
1977
+ unitPath.addPath(path, invTransform);
1978
+ ctx.fill(unitPath, fillRule);
1979
+ ctx.restore();
1980
+ return true;
1946
1981
  }
1947
- const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
1948
- const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
1949
- const clipPath = clipShapes ? buildClipPath(clipShapes, vbW, vbH) : void 0;
1950
- const maskRef = parseUrlRef(props.mask);
1951
- const maskShapes = maskRef ? defs.masks.get(maskRef) : void 0;
1952
- const filterRef = parseUrlRef(props.filter);
1953
- const filterDef = filterRef ? defs.filters.get(filterRef) : void 0;
1954
- if (clipPath) ctx.save();
1955
- if (clipPath) ctx.clip(clipPath);
1956
- const needsOffscreen = maskShapes || filterDef;
1957
- let elemCanvas;
1958
- const targetCtx = needsOffscreen ? (() => {
1959
- const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
1960
- elemCanvas = c;
1961
- return cCtx;
1962
- })() : ctx;
1963
- switch (type) {
1964
- case "path":
1965
- drawPath(targetCtx, props, inherited, color, defs);
1966
- break;
1967
- case "circle":
1968
- drawCircle(targetCtx, props, inherited, color, defs, vbW, vbH);
1969
- break;
1970
- case "rect":
1971
- drawSvgRect(targetCtx, props, inherited, color, defs, vbW, vbH);
1972
- break;
1973
- case "line":
1974
- drawLine(targetCtx, props, inherited, color, defs, vbW, vbH);
1975
- break;
1976
- case "ellipse":
1977
- drawEllipse(targetCtx, props, inherited, color, defs, vbW, vbH);
1978
- break;
1979
- case "polygon":
1980
- drawPolygon(targetCtx, props, inherited, color, defs);
1981
- break;
1982
- case "polyline":
1983
- drawPolyline(targetCtx, props, inherited, color, defs);
1984
- break;
1985
- case "g":
1986
- drawGroup(targetCtx, child, inherited, color, defs, vbW, vbH);
1987
- break;
1982
+ return false;
1983
+ }
1984
+ function strokeWithSvgGradient(ctx, def, bbox, path) {
1985
+ const props = def.props;
1986
+ if (def.type === "linearGradient") {
1987
+ ctx.strokeStyle = createLinearGradientFromBBox(ctx, props, bbox, def);
1988
+ ctx.stroke(path);
1989
+ return true;
1988
1990
  }
1989
- if (needsOffscreen && elemCanvas) {
1990
- if (maskShapes) {
1991
- const [maskCanvas, maskCtx] = acquireOffscreen(
1992
- Math.ceil(vbW),
1993
- Math.ceil(vbH)
1994
- );
1995
- for (const shape of maskShapes) {
1996
- drawSvgChild(
1997
- maskCtx,
1998
- shape,
1999
- { fill: "white" },
2000
- "white",
2001
- defs,
2002
- vbW,
2003
- vbH
1991
+ if (def.type === "radialGradient") {
1992
+ const cxAbs = bbox.x + parseFrac(props.cx, 0.5) * bbox.width;
1993
+ const cyAbs = bbox.y + parseFrac(props.cy, 0.5) * bbox.height;
1994
+ const rAbs = parseFrac(props.r, 0.5) * Math.sqrt(bbox.width * bbox.height);
1995
+ const fxAbs = bbox.x + parseFrac(props.fx, 0.5) * bbox.width;
1996
+ const fyAbs = bbox.y + parseFrac(props.fy, 0.5) * bbox.height;
1997
+ const gradient = ctx.createRadialGradient(
1998
+ fxAbs,
1999
+ fyAbs,
2000
+ 0,
2001
+ cxAbs,
2002
+ cyAbs,
2003
+ rAbs
2004
+ );
2005
+ addGradientStops(gradient, def);
2006
+ ctx.strokeStyle = gradient;
2007
+ ctx.stroke(path);
2008
+ return true;
2009
+ }
2010
+ return false;
2011
+ }
2012
+
2013
+ // src/jsx/draw/svg/paint.ts
2014
+ function applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox) {
2015
+ const fill = resolveCurrentColor(props.fill, color) ?? inherited.fill;
2016
+ const fillRule = props.fillRule ?? props["fill-rule"];
2017
+ const fillOpacity = Number(props.fillOpacity ?? props["fill-opacity"] ?? 1);
2018
+ const fillRef = parseUrlRef(fill);
2019
+ if (fillRef) {
2020
+ const gradientDef = defs.gradients.get(fillRef);
2021
+ if (gradientDef) {
2022
+ if (fillOpacity < 1) {
2023
+ ctx.save();
2024
+ ctx.globalAlpha *= fillOpacity;
2025
+ fillWithSvgGradient(
2026
+ ctx,
2027
+ gradientDef,
2028
+ bbox,
2029
+ path,
2030
+ fillRule ?? "nonzero"
2031
+ );
2032
+ ctx.restore();
2033
+ } else {
2034
+ fillWithSvgGradient(
2035
+ ctx,
2036
+ gradientDef,
2037
+ bbox,
2038
+ path,
2039
+ fillRule ?? "nonzero"
2004
2040
  );
2005
2041
  }
2006
- targetCtx.globalCompositeOperation = "destination-in";
2007
- targetCtx.drawImage(maskCanvas, 0, 0);
2008
- targetCtx.globalCompositeOperation = "source-over";
2009
- releaseOffscreen(maskCanvas);
2010
- }
2011
- if (filterDef) {
2012
- const filteredCanvas = applyFilter(
2013
- filterDef,
2014
- elemCanvas,
2015
- Math.ceil(vbW),
2016
- Math.ceil(vbH)
2017
- );
2018
- ctx.drawImage(filteredCanvas, 0, 0);
2019
- if (filteredCanvas !== elemCanvas) releaseOffscreen(filteredCanvas);
2020
- } else {
2021
- ctx.drawImage(elemCanvas, 0, 0);
2022
2042
  }
2023
- releaseOffscreen(elemCanvas);
2043
+ } else if (fill !== "none") {
2044
+ ctx.fillStyle = applyOpacity(fill, fillOpacity);
2045
+ ctx.fill(path, fillRule ?? "nonzero");
2024
2046
  }
2025
- if (clipPath) ctx.restore();
2026
- if (transform || opacity < 1) ctx.restore();
2047
+ applyStroke(ctx, props, path, inherited, color, defs, bbox);
2048
+ }
2049
+ function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
2050
+ const stroke = resolveCurrentColor(props.stroke, color) ?? inherited.stroke;
2051
+ if (!stroke || stroke === "none") return;
2052
+ const strokeOpacity = Number(
2053
+ props.strokeOpacity ?? props["stroke-opacity"] ?? inherited.strokeOpacity ?? 1
2054
+ );
2055
+ ctx.lineWidth = Number(
2056
+ props.strokeWidth ?? props["stroke-width"] ?? inherited.strokeWidth ?? 1
2057
+ );
2058
+ ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? inherited.strokeLinecap ?? "butt";
2059
+ ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? inherited.strokeLinejoin ?? "miter";
2060
+ const strokeRef = parseUrlRef(stroke);
2061
+ if (strokeRef) {
2062
+ const gradientDef = defs.gradients.get(strokeRef);
2063
+ if (gradientDef) {
2064
+ if (strokeOpacity < 1) {
2065
+ ctx.save();
2066
+ ctx.globalAlpha *= strokeOpacity;
2067
+ strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2068
+ ctx.restore();
2069
+ } else {
2070
+ strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2071
+ }
2072
+ return;
2073
+ }
2074
+ }
2075
+ ctx.strokeStyle = applyOpacity(stroke, strokeOpacity);
2076
+ ctx.stroke(path);
2027
2077
  }
2078
+
2079
+ // src/jsx/draw/svg/shapes.ts
2028
2080
  function drawPath(ctx, props, inherited, color, defs) {
2029
2081
  const d = props.d;
2030
2082
  if (!d) return;
2031
- const path = new Path2D(d);
2083
+ const path = new Path2D3(d);
2032
2084
  const bbox = pathBBox(d);
2033
2085
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2034
2086
  }
@@ -2037,7 +2089,7 @@ function drawCircle(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2037
2089
  const cy = svgLength(props.cy, vbH);
2038
2090
  const r = svgLength(props.r, Math.min(vbW, vbH));
2039
2091
  if (r <= 0) return;
2040
- const path = new Path2D();
2092
+ const path = new Path2D3();
2041
2093
  path.arc(cx, cy, r, 0, Math.PI * 2);
2042
2094
  const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
2043
2095
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
@@ -2054,7 +2106,7 @@ function drawSvgRect(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2054
2106
  let ry = ryRaw >= 0 ? ryRaw : rx;
2055
2107
  rx = Math.min(rx, w / 2);
2056
2108
  ry = Math.min(ry, h / 2);
2057
- const path = new Path2D();
2109
+ const path = new Path2D3();
2058
2110
  if (rx > 0 || ry > 0) {
2059
2111
  path.roundRect(x, y, w, h, [rx]);
2060
2112
  } else {
@@ -2068,7 +2120,7 @@ function drawLine(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2068
2120
  const y1 = svgLength(props.y1, vbH);
2069
2121
  const x2 = svgLength(props.x2, vbW);
2070
2122
  const y2 = svgLength(props.y2, vbH);
2071
- const path = new Path2D();
2123
+ const path = new Path2D3();
2072
2124
  path.moveTo(x1, y1);
2073
2125
  path.lineTo(x2, y2);
2074
2126
  const bbox = {
@@ -2085,7 +2137,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2085
2137
  const rx = svgLength(props.rx, vbW);
2086
2138
  const ry = svgLength(props.ry, vbH);
2087
2139
  if (rx <= 0 || ry <= 0) return;
2088
- const path = new Path2D();
2140
+ const path = new Path2D3();
2089
2141
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
2090
2142
  const bbox = {
2091
2143
  x: cx - rx,
@@ -2098,7 +2150,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2098
2150
  function drawPolygon(ctx, props, inherited, color, defs) {
2099
2151
  const points = parsePoints(props.points);
2100
2152
  if (points.length < 2) return;
2101
- const path = new Path2D();
2153
+ const path = new Path2D3();
2102
2154
  path.moveTo(points[0][0], points[0][1]);
2103
2155
  for (let i = 1; i < points.length; i++) {
2104
2156
  path.lineTo(points[i][0], points[i][1]);
@@ -2110,7 +2162,7 @@ function drawPolygon(ctx, props, inherited, color, defs) {
2110
2162
  function drawPolyline(ctx, props, inherited, color, defs) {
2111
2163
  const points = parsePoints(props.points);
2112
2164
  if (points.length < 2) return;
2113
- const path = new Path2D();
2165
+ const path = new Path2D3();
2114
2166
  path.moveTo(points[0][0], points[0][1]);
2115
2167
  for (let i = 1; i < points.length; i++) {
2116
2168
  path.lineTo(points[i][0], points[i][1]);
@@ -2118,167 +2170,156 @@ function drawPolyline(ctx, props, inherited, color, defs) {
2118
2170
  const bbox = pointsBBox(points);
2119
2171
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2120
2172
  }
2121
- function applySvgTransform(ctx, transform) {
2122
- const re = /\b(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)/g;
2123
- let match;
2124
- while ((match = re.exec(transform)) !== null) {
2125
- const fn = match[1];
2126
- const args = match[2].split(/[\s,]+/).filter(Boolean).map(Number);
2127
- switch (fn) {
2128
- case "translate":
2129
- ctx.translate(args[0] ?? 0, args[1] ?? 0);
2130
- break;
2131
- case "scale": {
2132
- const sx = args[0] ?? 1;
2133
- ctx.scale(sx, args[1] ?? sx);
2134
- break;
2135
- }
2136
- case "rotate": {
2137
- const angle = (args[0] ?? 0) * Math.PI / 180;
2138
- if (args.length >= 3) {
2139
- const cx = args[1];
2140
- const cy = args[2];
2141
- ctx.translate(cx, cy);
2142
- ctx.rotate(angle);
2143
- ctx.translate(-cx, -cy);
2144
- } else {
2145
- ctx.rotate(angle);
2146
- }
2147
- break;
2148
- }
2149
- case "skewX":
2150
- ctx.transform(
2151
- 1,
2152
- 0,
2153
- Math.tan((args[0] ?? 0) * Math.PI / 180),
2154
- 1,
2155
- 0,
2156
- 0
2157
- );
2158
- break;
2159
- case "skewY":
2160
- ctx.transform(
2161
- 1,
2162
- Math.tan((args[0] ?? 0) * Math.PI / 180),
2163
- 0,
2164
- 1,
2165
- 0,
2166
- 0
2167
- );
2168
- break;
2169
- case "matrix":
2170
- ctx.transform(
2171
- args[0] ?? 1,
2172
- args[1] ?? 0,
2173
- args[2] ?? 0,
2174
- args[3] ?? 1,
2175
- args[4] ?? 0,
2176
- args[5] ?? 0
2177
- );
2178
- break;
2179
- }
2173
+
2174
+ // src/jsx/draw/svg/traverse.ts
2175
+ function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
2176
+ const { type } = child;
2177
+ const props = mergeStyleIntoProps(child.props);
2178
+ if (type === "defs" || type === "clipPath" || type === "mask" || type === "filter")
2179
+ return;
2180
+ const transform = type !== "g" ? props.transform : void 0;
2181
+ const opacity = type !== "g" ? Number(props.opacity ?? 1) : 1;
2182
+ const savedTransform = applyTransformAndOpacity(ctx, transform, opacity);
2183
+ const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
2184
+ const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
2185
+ const clipPath = clipShapes ? buildClipPath(clipShapes, vbW, vbH) : void 0;
2186
+ const maskRef = parseUrlRef(props.mask);
2187
+ const maskShapes = maskRef ? defs.masks.get(maskRef) : void 0;
2188
+ const filterRef = parseUrlRef(props.filter);
2189
+ const filterDef = filterRef ? defs.filters.get(filterRef) : void 0;
2190
+ if (clipPath) {
2191
+ ctx.save();
2192
+ ctx.clip(clipPath);
2193
+ }
2194
+ const needsOffscreen = maskShapes || filterDef;
2195
+ let elemCanvas;
2196
+ let targetCtx;
2197
+ if (needsOffscreen) {
2198
+ const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
2199
+ elemCanvas = c;
2200
+ targetCtx = cCtx;
2201
+ } else {
2202
+ targetCtx = ctx;
2203
+ }
2204
+ switch (type) {
2205
+ case "path":
2206
+ drawPath(targetCtx, props, inherited, color, defs);
2207
+ break;
2208
+ case "circle":
2209
+ drawCircle(targetCtx, props, inherited, color, defs, vbW, vbH);
2210
+ break;
2211
+ case "rect":
2212
+ drawSvgRect(targetCtx, props, inherited, color, defs, vbW, vbH);
2213
+ break;
2214
+ case "line":
2215
+ drawLine(targetCtx, props, inherited, color, defs, vbW, vbH);
2216
+ break;
2217
+ case "ellipse":
2218
+ drawEllipse(targetCtx, props, inherited, color, defs, vbW, vbH);
2219
+ break;
2220
+ case "polygon":
2221
+ drawPolygon(targetCtx, props, inherited, color, defs);
2222
+ break;
2223
+ case "polyline":
2224
+ drawPolyline(targetCtx, props, inherited, color, defs);
2225
+ break;
2226
+ case "g":
2227
+ drawGroup(targetCtx, child, props, inherited, color, defs, vbW, vbH);
2228
+ break;
2229
+ }
2230
+ if (needsOffscreen && elemCanvas) {
2231
+ applyOffscreenEffects(
2232
+ ctx,
2233
+ elemCanvas,
2234
+ targetCtx,
2235
+ maskShapes,
2236
+ filterDef,
2237
+ color,
2238
+ defs,
2239
+ vbW,
2240
+ vbH
2241
+ );
2180
2242
  }
2243
+ if (clipPath) ctx.restore();
2244
+ if (savedTransform) ctx.restore();
2181
2245
  }
2182
- function drawGroup(ctx, node, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
2246
+ function drawGroup(ctx, node, mergedProps, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
2183
2247
  const children = normalizeChildren(node);
2184
2248
  if (children.length === 0) return;
2185
- const merged = mergeStyleIntoProps(node.props);
2186
- const groupInherited = {
2187
- fill: resolveCurrentColor(merged.fill, color) ?? inherited.fill,
2188
- stroke: resolveCurrentColor(merged.stroke, color) ?? inherited.stroke,
2189
- strokeWidth: merged.strokeWidth ?? merged["stroke-width"] ?? inherited.strokeWidth,
2190
- strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"] ?? inherited.strokeLinecap,
2191
- strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? inherited.strokeLinejoin,
2192
- strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? inherited.strokeOpacity
2193
- };
2194
- const opacity = Number(merged.opacity ?? 1);
2195
- const transform = merged.transform;
2196
- if (transform || opacity < 1) {
2197
- ctx.save();
2198
- if (transform) applySvgTransform(ctx, transform);
2199
- if (opacity < 1) ctx.globalAlpha *= opacity;
2200
- }
2249
+ const groupInherited = resolveInheritedStyle(mergedProps, color, inherited);
2250
+ const opacity = Number(mergedProps.opacity ?? 1);
2251
+ const transform = mergedProps.transform;
2252
+ const saved = applyTransformAndOpacity(ctx, transform, opacity);
2201
2253
  for (const child of children) {
2202
2254
  if (child != null && typeof child === "object") {
2203
2255
  drawSvgChild(ctx, child, groupInherited, color, defs, vbW, vbH);
2204
2256
  }
2205
2257
  }
2206
- if (transform || opacity < 1) {
2207
- ctx.restore();
2208
- }
2258
+ if (saved) ctx.restore();
2209
2259
  }
2210
- function parsePoints(value) {
2211
- if (!value) return [];
2212
- const nums = value.trim().split(/[\s,]+/).map(Number);
2213
- const result = [];
2214
- for (let i = 0; i + 1 < nums.length; i += 2) {
2215
- result.push([nums[i], nums[i + 1]]);
2260
+ function applyOffscreenEffects(ctx, elemCanvas, targetCtx, maskShapes, filterDef, color, defs, vbW, vbH) {
2261
+ if (maskShapes) {
2262
+ const [maskCanvas, maskCtx] = acquireOffscreen(
2263
+ Math.ceil(vbW),
2264
+ Math.ceil(vbH)
2265
+ );
2266
+ for (const shape of maskShapes) {
2267
+ drawSvgChild(maskCtx, shape, { fill: "white" }, "white", defs, vbW, vbH);
2268
+ }
2269
+ targetCtx.globalCompositeOperation = "destination-in";
2270
+ targetCtx.drawImage(maskCanvas, 0, 0);
2271
+ targetCtx.globalCompositeOperation = "source-over";
2272
+ releaseOffscreen(maskCanvas);
2273
+ }
2274
+ if (filterDef) {
2275
+ const filteredCanvas = applyFilter(
2276
+ filterDef,
2277
+ elemCanvas,
2278
+ Math.ceil(vbW),
2279
+ Math.ceil(vbH)
2280
+ );
2281
+ ctx.drawImage(filteredCanvas, 0, 0);
2282
+ if (filteredCanvas !== elemCanvas) releaseOffscreen(filteredCanvas);
2283
+ } else {
2284
+ ctx.drawImage(elemCanvas, 0, 0);
2216
2285
  }
2217
- return result;
2286
+ releaseOffscreen(elemCanvas);
2218
2287
  }
2219
- function applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox) {
2220
- const fill = resolveCurrentColor(props.fill, color) ?? inherited.fill;
2221
- const fillRule = props.fillRule ?? props["fill-rule"];
2222
- const fillOpacity = Number(props.fillOpacity ?? props["fill-opacity"] ?? 1);
2223
- const fillRef = parseUrlRef(fill);
2224
- if (fillRef) {
2225
- const gradientDef = defs.gradients.get(fillRef);
2226
- if (gradientDef) {
2227
- if (fillOpacity < 1) {
2228
- ctx.save();
2229
- ctx.globalAlpha *= fillOpacity;
2230
- fillWithSvgGradient(
2231
- ctx,
2232
- gradientDef,
2233
- bbox,
2234
- path,
2235
- fillRule ?? "nonzero"
2236
- );
2237
- ctx.restore();
2238
- } else {
2239
- fillWithSvgGradient(
2240
- ctx,
2241
- gradientDef,
2242
- bbox,
2243
- path,
2244
- fillRule ?? "nonzero"
2245
- );
2246
- }
2288
+
2289
+ // src/jsx/draw/svg/index.ts
2290
+ function drawSvgContainer(ctx, node, x, y, width, height) {
2291
+ ctx.save();
2292
+ ctx.translate(x, y);
2293
+ const viewBox = node.props.viewBox;
2294
+ let vbW = width;
2295
+ let vbH = height;
2296
+ if (viewBox) {
2297
+ const parts = viewBox.split(/[\s,]+/).map(Number);
2298
+ if (parts.length === 4) {
2299
+ const [vbX, vbY, parsedW, parsedH] = parts;
2300
+ vbW = parsedW;
2301
+ vbH = parsedH;
2302
+ const scaleX = width / vbW;
2303
+ const scaleY = height / vbH;
2304
+ ctx.scale(scaleX, scaleY);
2305
+ ctx.translate(-vbX, -vbY);
2247
2306
  }
2248
- } else if (fill !== "none") {
2249
- ctx.fillStyle = applyOpacity(fill, fillOpacity);
2250
- ctx.fill(path, fillRule ?? "nonzero");
2251
2307
  }
2252
- applyStroke(ctx, props, path, inherited, color, defs, bbox);
2253
- }
2254
- function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
2255
- const stroke = resolveCurrentColor(props.stroke, color) ?? inherited.stroke;
2256
- if (!stroke || stroke === "none") return;
2257
- const strokeOpacity = Number(
2258
- props.strokeOpacity ?? props["stroke-opacity"] ?? inherited.strokeOpacity ?? 1
2259
- );
2260
- ctx.lineWidth = Number(
2261
- props.strokeWidth ?? props["stroke-width"] ?? inherited.strokeWidth ?? 1
2262
- );
2263
- ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? inherited.strokeLinecap ?? "butt";
2264
- ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? inherited.strokeLinejoin ?? "miter";
2265
- const strokeRef = parseUrlRef(stroke);
2266
- if (strokeRef) {
2267
- const gradientDef = defs.gradients.get(strokeRef);
2268
- if (gradientDef) {
2269
- if (strokeOpacity < 1) {
2270
- ctx.save();
2271
- ctx.globalAlpha *= strokeOpacity;
2272
- strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2273
- ctx.restore();
2274
- } else {
2275
- strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2276
- }
2277
- return;
2308
+ const color = node.style.color ?? "black";
2309
+ const merged = mergeStyleIntoProps(node.props);
2310
+ const inherited = resolveInheritedStyle(merged, color);
2311
+ const children = node.props.children;
2312
+ if (children != null) {
2313
+ const childArray = Array.isArray(children) ? children : [children];
2314
+ const svgChildren = childArray.filter(
2315
+ (c) => c != null && typeof c === "object"
2316
+ );
2317
+ const defs = collectDefs(svgChildren);
2318
+ for (const child of svgChildren) {
2319
+ drawSvgChild(ctx, child, inherited, color, defs, vbW, vbH);
2278
2320
  }
2279
2321
  }
2280
- ctx.strokeStyle = applyOpacity(stroke, strokeOpacity);
2281
- ctx.stroke(path);
2322
+ ctx.restore();
2282
2323
  }
2283
2324
 
2284
2325
  // src/jsx/draw/text.ts
@@ -2565,7 +2606,7 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
2565
2606
  }
2566
2607
 
2567
2608
  // src/jsx/draw/index.ts
2568
- async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2609
+ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle, imageCache) {
2569
2610
  const x = parentX + node.x;
2570
2611
  const y = parentY + node.y;
2571
2612
  const { width, height, style } = node;
@@ -2585,6 +2626,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2585
2626
  const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
2586
2627
  offCtx.save();
2587
2628
  offCtx.scale(qx, qy);
2629
+ const cache2 = imageCache ?? /* @__PURE__ */ new Map();
2588
2630
  await drawNodeCore(
2589
2631
  offCtx,
2590
2632
  node,
@@ -2594,6 +2636,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2594
2636
  1 - y,
2595
2637
  debug,
2596
2638
  emojiStyle,
2639
+ cache2,
2597
2640
  transformWithoutScale
2598
2641
  );
2599
2642
  offCtx.restore();
@@ -2627,7 +2670,18 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2627
2670
  return;
2628
2671
  }
2629
2672
  }
2630
- await drawNodeCore(ctx, node, parentX, parentY, 0, 0, debug, emojiStyle);
2673
+ const cache = imageCache ?? /* @__PURE__ */ new Map();
2674
+ await drawNodeCore(
2675
+ ctx,
2676
+ node,
2677
+ parentX,
2678
+ parentY,
2679
+ 0,
2680
+ 0,
2681
+ debug,
2682
+ emojiStyle,
2683
+ cache
2684
+ );
2631
2685
  }
2632
2686
  function extractScale(transform) {
2633
2687
  const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
@@ -2639,7 +2693,7 @@ function extractScale(transform) {
2639
2693
  const remaining = transform.replace(fullMatch, "").trim();
2640
2694
  return { sx, sy, remaining };
2641
2695
  }
2642
- async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, overrideTransform) {
2696
+ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, imageCache, overrideTransform) {
2643
2697
  const x = parentX + node.x + offsetX;
2644
2698
  const y = parentY + node.y + offsetY;
2645
2699
  const { width, height, style } = node;
@@ -2705,7 +2759,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2705
2759
  if (hasRadius(borderRadius)) {
2706
2760
  applyClip(ctx, x, y, width, height, borderRadius);
2707
2761
  }
2708
- const image = await loadImage3(urlMatch[1]);
2762
+ const image = await cachedLoadImage(imageCache, urlMatch[1]);
2709
2763
  const bgSize = style.backgroundSize;
2710
2764
  if (bgSize === "cover") {
2711
2765
  const r = computeCover(
@@ -2814,8 +2868,8 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2814
2868
  imgY,
2815
2869
  imgW,
2816
2870
  imgH,
2817
- style,
2818
- node.props.__loadedImage
2871
+ imageCache,
2872
+ style
2819
2873
  );
2820
2874
  }
2821
2875
  if (node.type === "svg") {
@@ -2823,7 +2877,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2823
2877
  } else {
2824
2878
  for (const child of node.children) {
2825
2879
  if (offsetX === 0 && offsetY === 0) {
2826
- await drawNode(ctx, child, x, y, debug, emojiStyle);
2880
+ await drawNode(ctx, child, x, y, debug, emojiStyle, imageCache);
2827
2881
  } else {
2828
2882
  await drawNodeCore(
2829
2883
  ctx,
@@ -2834,7 +2888,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2834
2888
  0,
2835
2889
  debug,
2836
2890
  emojiStyle,
2837
- void 0
2891
+ imageCache
2838
2892
  );
2839
2893
  }
2840
2894
  }
@@ -2906,9 +2960,6 @@ function parseAngle(value) {
2906
2960
  return parseFloat(value);
2907
2961
  }
2908
2962
 
2909
- // src/jsx/layout.ts
2910
- import { loadImage as loadImage4 } from "@napi-rs/canvas";
2911
-
2912
2963
  // src/jsx/style/expand.ts
2913
2964
  var SIDES = ["Top", "Right", "Bottom", "Left"];
2914
2965
  function parseSides(value) {
@@ -3295,6 +3346,7 @@ function applyEdgeValue(node, setter, edge, value) {
3295
3346
 
3296
3347
  // src/jsx/layout.ts
3297
3348
  async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
3349
+ const imageCache = /* @__PURE__ */ new Map();
3298
3350
  const elementYogaNode = createYogaNode();
3299
3351
  const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
3300
3352
  const elementNode = await buildNode(
@@ -3305,7 +3357,8 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3305
3357
  containerHeight,
3306
3358
  ctx,
3307
3359
  emojiEnabled,
3308
- fontFamilies
3360
+ fontFamilies,
3361
+ imageCache
3309
3362
  );
3310
3363
  const rootYogaNode = createYogaNode();
3311
3364
  rootYogaNode.setWidth(containerWidth);
@@ -3315,7 +3368,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3315
3368
  rootYogaNode.calculateLayout(containerWidth, containerHeight);
3316
3369
  const elementLayout = extractLayout(elementNode, elementYogaNode);
3317
3370
  freeYogaNode(rootYogaNode);
3318
- return {
3371
+ const tree = {
3319
3372
  type: "div",
3320
3373
  style: rootStyle,
3321
3374
  children: [elementLayout],
@@ -3325,8 +3378,9 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3325
3378
  width: containerWidth,
3326
3379
  height: containerHeight
3327
3380
  };
3381
+ return { tree, imageCache };
3328
3382
  }
3329
- async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
3383
+ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies, imageCache) {
3330
3384
  if (element === null || element === void 0 || typeof element === "boolean") {
3331
3385
  return {
3332
3386
  type: "empty",
@@ -3364,7 +3418,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3364
3418
  viewportHeight,
3365
3419
  ctx,
3366
3420
  emojiEnabled,
3367
- fontFamilies
3421
+ fontFamilies,
3422
+ imageCache
3368
3423
  );
3369
3424
  }
3370
3425
  const props = el.props ?? {};
@@ -3424,7 +3479,7 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3424
3479
  const src = props.src;
3425
3480
  if (src) {
3426
3481
  try {
3427
- const image = await loadImage4(src);
3482
+ const image = await cachedLoadImage(imageCache ?? /* @__PURE__ */ new Map(), src);
3428
3483
  const naturalW = image.width;
3429
3484
  const naturalH = image.height;
3430
3485
  const wSet = style.width !== void 0;
@@ -3434,7 +3489,6 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3434
3489
  } else if (hSet && !wSet && typeof style.height === "number" && naturalH > 0) {
3435
3490
  style.width = style.height * (naturalW / naturalH);
3436
3491
  }
3437
- props.__loadedImage = image;
3438
3492
  } catch {
3439
3493
  }
3440
3494
  }
@@ -3512,7 +3566,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3512
3566
  viewportHeight,
3513
3567
  ctx,
3514
3568
  emojiEnabled,
3515
- fontFamilies
3569
+ fontFamilies,
3570
+ imageCache
3516
3571
  )
3517
3572
  );
3518
3573
  }
@@ -3586,7 +3641,7 @@ async function renderReactElement(ctx, element, options) {
3586
3641
  const height = options.height ?? ctx.canvas.height;
3587
3642
  const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
3588
3643
  const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
3589
- const layoutTree = await buildLayoutTree(
3644
+ const { tree: layoutTree, imageCache } = await buildLayoutTree(
3590
3645
  element,
3591
3646
  width,
3592
3647
  height,
@@ -3594,7 +3649,15 @@ async function renderReactElement(ctx, element, options) {
3594
3649
  !!emojiStyle,
3595
3650
  fontFamilies
3596
3651
  );
3597
- await drawNode(ctx, layoutTree, 0, 0, options.debug ?? false, emojiStyle);
3652
+ await drawNode(
3653
+ ctx,
3654
+ layoutTree,
3655
+ 0,
3656
+ 0,
3657
+ options.debug ?? false,
3658
+ emojiStyle,
3659
+ imageCache
3660
+ );
3598
3661
  }
3599
3662
 
3600
3663
  // src/index.ts
@@ -3610,7 +3673,7 @@ export {
3610
3673
  Image,
3611
3674
  LottieAnimation,
3612
3675
  createCanvas3 as createCanvas,
3613
- loadImage5 as loadImage,
3676
+ loadImage3 as loadImage,
3614
3677
  loadLottie,
3615
3678
  registerFont,
3616
3679
  registerFontFromPath,