@effing/canvas 0.23.1 → 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,7 +655,16 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
644
655
  fontStyle,
645
656
  ctx
646
657
  );
647
- const baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
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;
648
668
  segments.push({
649
669
  text: line,
650
670
  x,
@@ -666,6 +686,14 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
666
686
  totalHeight += lineHeightPx;
667
687
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
668
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
+ }
669
697
  const textBoxTrim = style.textBoxTrim;
670
698
  if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
671
699
  const textBoxEdge = style.textBoxEdge ?? "text";
@@ -997,9 +1025,6 @@ function splitGradientArgs(content) {
997
1025
  return parts;
998
1026
  }
999
1027
 
1000
- // src/jsx/draw/image.ts
1001
- import { loadImage } from "@napi-rs/canvas";
1002
-
1003
1028
  // src/jsx/draw/object-fit.ts
1004
1029
  function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
1005
1030
  const imgRatio = imgW / imgH;
@@ -1035,8 +1060,8 @@ function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
1035
1060
  }
1036
1061
 
1037
1062
  // src/jsx/draw/image.ts
1038
- async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
1039
- 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);
1040
1065
  const objectFit = style?.objectFit ?? "fill";
1041
1066
  if (objectFit === "fill") {
1042
1067
  ctx.drawImage(image, x, y, width, height);
@@ -1352,9 +1377,7 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1352
1377
  ctx.restore();
1353
1378
  }
1354
1379
 
1355
- // src/jsx/draw/svg.ts
1356
- import { Path2D } from "@napi-rs/canvas";
1357
- import parseCssColor from "parse-css-color";
1380
+ // src/jsx/draw/svg/style.ts
1358
1381
  function resolveCurrentColor(value, color) {
1359
1382
  if (value?.toLowerCase() === "currentcolor") return color;
1360
1383
  return value;
@@ -1364,6 +1387,37 @@ function mergeStyleIntoProps(props) {
1364
1387
  if (!style) return props;
1365
1388
  return { ...props, ...style };
1366
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
1367
1421
  function collectDefs(children) {
1368
1422
  const clips = /* @__PURE__ */ new Map();
1369
1423
  const gradients = /* @__PURE__ */ new Map();
@@ -1387,122 +1441,85 @@ function collectDefs(children) {
1387
1441
  }
1388
1442
  return { clips, gradients, masks, filters };
1389
1443
  }
1390
- function parseUrlRef(value) {
1391
- if (typeof value !== "string") return void 0;
1392
- const m = value.match(/^url\(#(.+)\)$/);
1393
- return m?.[1];
1394
- }
1395
- function normalizeChildren(node) {
1396
- const raw = node.children ?? node.props.children;
1397
- if (raw == null) return [];
1398
- return Array.isArray(raw) ? raw : [raw];
1399
- }
1400
- function svgLength(value, reference, fallback = 0) {
1401
- if (value == null) return fallback;
1402
- const s = String(value);
1403
- if (s.endsWith("%")) return parseFloat(s) / 100 * reference;
1404
- const n = Number(s);
1405
- return isNaN(n) ? fallback : n;
1406
- }
1407
- function parseFrac(value, fallback) {
1408
- if (value == null) return fallback;
1409
- const s = String(value);
1410
- if (s.endsWith("%")) return parseFloat(s) / 100;
1411
- return Number(s);
1412
- }
1413
- function applyOpacity(color, opacity) {
1414
- if (opacity >= 1) return color;
1415
- const parsed = parseCssColor(color);
1416
- if (!parsed) return color;
1417
- const [r, g, b] = parsed.values;
1418
- return `rgba(${r}, ${g}, ${b}, ${parsed.alpha * opacity})`;
1419
- }
1420
- function addGradientStops(gradient, def) {
1421
- for (const stop of normalizeChildren(def)) {
1422
- if (stop.type !== "stop") continue;
1423
- const sp = stop.props;
1424
- const offsetRaw = sp.offset ?? 0;
1425
- const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
1426
- const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
1427
- const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
1428
- gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
1429
- }
1430
- }
1431
- function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
1432
- const props = def.props;
1433
- if (def.type === "linearGradient") {
1434
- const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
1435
- const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
1436
- const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
1437
- const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
1438
- const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1439
- addGradientStops(gradient, def);
1440
- ctx.fillStyle = gradient;
1441
- ctx.fill(path, fillRule);
1442
- return true;
1443
- }
1444
- if (def.type === "radialGradient") {
1445
- const cxF = parseFrac(props.cx, 0.5);
1446
- const cyF = parseFrac(props.cy, 0.5);
1447
- const rF = parseFrac(props.r, 0.5);
1448
- const fxF = parseFrac(props.fx, cxF);
1449
- const fyF = parseFrac(props.fy, cyF);
1450
- ctx.save();
1451
- ctx.translate(bbox.x, bbox.y);
1452
- ctx.scale(bbox.width, bbox.height);
1453
- const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
1454
- addGradientStops(gradient, def);
1455
- ctx.fillStyle = gradient;
1456
- const unitPath = new Path2D();
1457
- const invTransform = {
1458
- a: 1 / bbox.width,
1459
- b: 0,
1460
- c: 0,
1461
- d: 1 / bbox.height,
1462
- e: -bbox.x / bbox.width,
1463
- f: -bbox.y / bbox.height
1464
- };
1465
- unitPath.addPath(path, invTransform);
1466
- ctx.fill(unitPath, fillRule);
1467
- ctx.restore();
1468
- return true;
1469
- }
1470
- 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;
1471
1458
  }
1472
- function strokeWithSvgGradient(ctx, def, bbox, path) {
1473
- const props = def.props;
1474
- if (def.type === "linearGradient") {
1475
- const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
1476
- const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
1477
- const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
1478
- const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
1479
- const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1480
- addGradientStops(gradient, def);
1481
- ctx.strokeStyle = gradient;
1482
- ctx.stroke(path);
1483
- return true;
1484
- }
1485
- if (def.type === "radialGradient") {
1486
- const cxAbs = bbox.x + parseFrac(props.cx, 0.5) * bbox.width;
1487
- const cyAbs = bbox.y + parseFrac(props.cy, 0.5) * bbox.height;
1488
- const rAbs = parseFrac(props.r, 0.5) * Math.sqrt(bbox.width * bbox.height);
1489
- const fxAbs = bbox.x + parseFrac(props.fx, 0.5) * bbox.width;
1490
- const fyAbs = bbox.y + parseFrac(props.fy, 0.5) * bbox.height;
1491
- const gradient = ctx.createRadialGradient(
1492
- fxAbs,
1493
- fyAbs,
1494
- 0,
1495
- cxAbs,
1496
- cyAbs,
1497
- rAbs
1498
- );
1499
- addGradientStops(gradient, def);
1500
- ctx.strokeStyle = gradient;
1501
- ctx.stroke(path);
1502
- 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
+ }
1503
1518
  }
1504
- return false;
1505
1519
  }
1520
+
1521
+ // src/jsx/draw/svg/path.ts
1522
+ import { Path2D } from "@napi-rs/canvas";
1506
1523
  function pathBBox(d) {
1507
1524
  let minX = Infinity;
1508
1525
  let minY = Infinity;
@@ -1732,53 +1749,17 @@ function buildClipPath(shapes, vbW, vbH) {
1732
1749
  }
1733
1750
  return combined;
1734
1751
  }
1735
- function drawSvgContainer(ctx, node, x, y, width, height) {
1736
- ctx.save();
1737
- ctx.translate(x, y);
1738
- const viewBox = node.props.viewBox;
1739
- let vbW = width;
1740
- let vbH = height;
1741
- if (viewBox) {
1742
- const parts = viewBox.split(/[\s,]+/).map(Number);
1743
- if (parts.length === 4) {
1744
- const [vbX, vbY, parsedW, parsedH] = parts;
1745
- vbW = parsedW;
1746
- vbH = parsedH;
1747
- const scaleX = width / vbW;
1748
- const scaleY = height / vbH;
1749
- ctx.scale(scaleX, scaleY);
1750
- ctx.translate(-vbX, -vbY);
1751
- }
1752
- }
1753
- const color = node.style.color ?? "black";
1754
- const merged = mergeStyleIntoProps(node.props);
1755
- const inherited = {
1756
- fill: resolveCurrentColor(merged.fill, color) ?? "black",
1757
- stroke: resolveCurrentColor(merged.stroke, color),
1758
- strokeWidth: merged.strokeWidth ?? merged["stroke-width"],
1759
- strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"],
1760
- strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"],
1761
- strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"]
1762
- };
1763
- const children = node.props.children;
1764
- if (children != null) {
1765
- const childArray = Array.isArray(children) ? children : [children];
1766
- const svgChildren = childArray.filter(
1767
- (c) => c != null && typeof c === "object"
1768
- );
1769
- const defs = collectDefs(svgChildren);
1770
- for (const child of svgChildren) {
1771
- drawSvgChild(ctx, child, inherited, color, defs, vbW, vbH);
1772
- }
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]]);
1773
1758
  }
1774
- ctx.restore();
1759
+ return result;
1775
1760
  }
1776
- var EMPTY_DEFS = {
1777
- clips: /* @__PURE__ */ new Map(),
1778
- gradients: /* @__PURE__ */ new Map(),
1779
- masks: /* @__PURE__ */ new Map(),
1780
- filters: /* @__PURE__ */ new Map()
1781
- };
1761
+
1762
+ // src/jsx/draw/svg/filters.ts
1782
1763
  function applyFeOffset(input, dx, dy, w, h) {
1783
1764
  const [canvas, ctx] = acquireOffscreen(w, h);
1784
1765
  ctx.drawImage(input, dx, dy);
@@ -1838,18 +1819,18 @@ function applyFeColorMatrix(input, type, values, w, h) {
1838
1819
  }
1839
1820
  return canvas;
1840
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
+ };
1841
1830
  function applyFeBlend(in1, in2, mode, w, h) {
1842
- const modeMap = {
1843
- normal: "source-over",
1844
- multiply: "multiply",
1845
- screen: "screen",
1846
- darken: "darken",
1847
- lighten: "lighten",
1848
- overlay: "overlay"
1849
- };
1850
1831
  const [canvas, ctx] = acquireOffscreen(w, h);
1851
1832
  ctx.drawImage(in2, 0, 0);
1852
- ctx.globalCompositeOperation = modeMap[mode] ?? "source-over";
1833
+ ctx.globalCompositeOperation = BLEND_MODE_MAP[mode] ?? "source-over";
1853
1834
  ctx.drawImage(in1, 0, 0);
1854
1835
  ctx.globalCompositeOperation = "source-over";
1855
1836
  return canvas;
@@ -1925,103 +1906,181 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
1925
1906
  }
1926
1907
  return lastResult;
1927
1908
  }
1928
- function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
1929
- const { type } = child;
1930
- const props = mergeStyleIntoProps(child.props);
1931
- if (type === "defs" || type === "clipPath" || type === "mask" || type === "filter")
1932
- return;
1933
- const transform = type !== "g" ? props.transform : void 0;
1934
- const opacity = type !== "g" ? Number(props.opacity ?? 1) : 1;
1935
- 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);
1936
1962
  ctx.save();
1937
- if (transform) applySvgTransform(ctx, transform);
1938
- 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;
1939
1981
  }
1940
- const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
1941
- const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
1942
- const clipPath = clipShapes ? buildClipPath(clipShapes, vbW, vbH) : void 0;
1943
- const maskRef = parseUrlRef(props.mask);
1944
- const maskShapes = maskRef ? defs.masks.get(maskRef) : void 0;
1945
- const filterRef = parseUrlRef(props.filter);
1946
- const filterDef = filterRef ? defs.filters.get(filterRef) : void 0;
1947
- if (clipPath) ctx.save();
1948
- if (clipPath) ctx.clip(clipPath);
1949
- const needsOffscreen = maskShapes || filterDef;
1950
- let elemCanvas;
1951
- const targetCtx = needsOffscreen ? (() => {
1952
- const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
1953
- elemCanvas = c;
1954
- return cCtx;
1955
- })() : ctx;
1956
- switch (type) {
1957
- case "path":
1958
- drawPath(targetCtx, props, inherited, color, defs);
1959
- break;
1960
- case "circle":
1961
- drawCircle(targetCtx, props, inherited, color, defs, vbW, vbH);
1962
- break;
1963
- case "rect":
1964
- drawSvgRect(targetCtx, props, inherited, color, defs, vbW, vbH);
1965
- break;
1966
- case "line":
1967
- drawLine(targetCtx, props, inherited, color, defs, vbW, vbH);
1968
- break;
1969
- case "ellipse":
1970
- drawEllipse(targetCtx, props, inherited, color, defs, vbW, vbH);
1971
- break;
1972
- case "polygon":
1973
- drawPolygon(targetCtx, props, inherited, color, defs);
1974
- break;
1975
- case "polyline":
1976
- drawPolyline(targetCtx, props, inherited, color, defs);
1977
- break;
1978
- case "g":
1979
- drawGroup(targetCtx, child, inherited, color, defs, vbW, vbH);
1980
- 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;
1981
1990
  }
1982
- if (needsOffscreen && elemCanvas) {
1983
- if (maskShapes) {
1984
- const [maskCanvas, maskCtx] = acquireOffscreen(
1985
- Math.ceil(vbW),
1986
- Math.ceil(vbH)
1987
- );
1988
- for (const shape of maskShapes) {
1989
- drawSvgChild(
1990
- maskCtx,
1991
- shape,
1992
- { fill: "white" },
1993
- "white",
1994
- defs,
1995
- vbW,
1996
- 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"
1997
2040
  );
1998
2041
  }
1999
- targetCtx.globalCompositeOperation = "destination-in";
2000
- targetCtx.drawImage(maskCanvas, 0, 0);
2001
- targetCtx.globalCompositeOperation = "source-over";
2002
- releaseOffscreen(maskCanvas);
2003
- }
2004
- if (filterDef) {
2005
- const filteredCanvas = applyFilter(
2006
- filterDef,
2007
- elemCanvas,
2008
- Math.ceil(vbW),
2009
- Math.ceil(vbH)
2010
- );
2011
- ctx.drawImage(filteredCanvas, 0, 0);
2012
- if (filteredCanvas !== elemCanvas) releaseOffscreen(filteredCanvas);
2013
- } else {
2014
- ctx.drawImage(elemCanvas, 0, 0);
2015
2042
  }
2016
- releaseOffscreen(elemCanvas);
2043
+ } else if (fill !== "none") {
2044
+ ctx.fillStyle = applyOpacity(fill, fillOpacity);
2045
+ ctx.fill(path, fillRule ?? "nonzero");
2046
+ }
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
+ }
2017
2074
  }
2018
- if (clipPath) ctx.restore();
2019
- if (transform || opacity < 1) ctx.restore();
2075
+ ctx.strokeStyle = applyOpacity(stroke, strokeOpacity);
2076
+ ctx.stroke(path);
2020
2077
  }
2078
+
2079
+ // src/jsx/draw/svg/shapes.ts
2021
2080
  function drawPath(ctx, props, inherited, color, defs) {
2022
2081
  const d = props.d;
2023
2082
  if (!d) return;
2024
- const path = new Path2D(d);
2083
+ const path = new Path2D3(d);
2025
2084
  const bbox = pathBBox(d);
2026
2085
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2027
2086
  }
@@ -2030,7 +2089,7 @@ function drawCircle(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2030
2089
  const cy = svgLength(props.cy, vbH);
2031
2090
  const r = svgLength(props.r, Math.min(vbW, vbH));
2032
2091
  if (r <= 0) return;
2033
- const path = new Path2D();
2092
+ const path = new Path2D3();
2034
2093
  path.arc(cx, cy, r, 0, Math.PI * 2);
2035
2094
  const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
2036
2095
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
@@ -2047,7 +2106,7 @@ function drawSvgRect(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2047
2106
  let ry = ryRaw >= 0 ? ryRaw : rx;
2048
2107
  rx = Math.min(rx, w / 2);
2049
2108
  ry = Math.min(ry, h / 2);
2050
- const path = new Path2D();
2109
+ const path = new Path2D3();
2051
2110
  if (rx > 0 || ry > 0) {
2052
2111
  path.roundRect(x, y, w, h, [rx]);
2053
2112
  } else {
@@ -2061,7 +2120,7 @@ function drawLine(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2061
2120
  const y1 = svgLength(props.y1, vbH);
2062
2121
  const x2 = svgLength(props.x2, vbW);
2063
2122
  const y2 = svgLength(props.y2, vbH);
2064
- const path = new Path2D();
2123
+ const path = new Path2D3();
2065
2124
  path.moveTo(x1, y1);
2066
2125
  path.lineTo(x2, y2);
2067
2126
  const bbox = {
@@ -2078,7 +2137,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2078
2137
  const rx = svgLength(props.rx, vbW);
2079
2138
  const ry = svgLength(props.ry, vbH);
2080
2139
  if (rx <= 0 || ry <= 0) return;
2081
- const path = new Path2D();
2140
+ const path = new Path2D3();
2082
2141
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
2083
2142
  const bbox = {
2084
2143
  x: cx - rx,
@@ -2091,7 +2150,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2091
2150
  function drawPolygon(ctx, props, inherited, color, defs) {
2092
2151
  const points = parsePoints(props.points);
2093
2152
  if (points.length < 2) return;
2094
- const path = new Path2D();
2153
+ const path = new Path2D3();
2095
2154
  path.moveTo(points[0][0], points[0][1]);
2096
2155
  for (let i = 1; i < points.length; i++) {
2097
2156
  path.lineTo(points[i][0], points[i][1]);
@@ -2103,7 +2162,7 @@ function drawPolygon(ctx, props, inherited, color, defs) {
2103
2162
  function drawPolyline(ctx, props, inherited, color, defs) {
2104
2163
  const points = parsePoints(props.points);
2105
2164
  if (points.length < 2) return;
2106
- const path = new Path2D();
2165
+ const path = new Path2D3();
2107
2166
  path.moveTo(points[0][0], points[0][1]);
2108
2167
  for (let i = 1; i < points.length; i++) {
2109
2168
  path.lineTo(points[i][0], points[i][1]);
@@ -2111,167 +2170,156 @@ function drawPolyline(ctx, props, inherited, color, defs) {
2111
2170
  const bbox = pointsBBox(points);
2112
2171
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2113
2172
  }
2114
- function applySvgTransform(ctx, transform) {
2115
- const re = /\b(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)/g;
2116
- let match;
2117
- while ((match = re.exec(transform)) !== null) {
2118
- const fn = match[1];
2119
- const args = match[2].split(/[\s,]+/).filter(Boolean).map(Number);
2120
- switch (fn) {
2121
- case "translate":
2122
- ctx.translate(args[0] ?? 0, args[1] ?? 0);
2123
- break;
2124
- case "scale": {
2125
- const sx = args[0] ?? 1;
2126
- ctx.scale(sx, args[1] ?? sx);
2127
- break;
2128
- }
2129
- case "rotate": {
2130
- const angle = (args[0] ?? 0) * Math.PI / 180;
2131
- if (args.length >= 3) {
2132
- const cx = args[1];
2133
- const cy = args[2];
2134
- ctx.translate(cx, cy);
2135
- ctx.rotate(angle);
2136
- ctx.translate(-cx, -cy);
2137
- } else {
2138
- ctx.rotate(angle);
2139
- }
2140
- break;
2141
- }
2142
- case "skewX":
2143
- ctx.transform(
2144
- 1,
2145
- 0,
2146
- Math.tan((args[0] ?? 0) * Math.PI / 180),
2147
- 1,
2148
- 0,
2149
- 0
2150
- );
2151
- break;
2152
- case "skewY":
2153
- ctx.transform(
2154
- 1,
2155
- Math.tan((args[0] ?? 0) * Math.PI / 180),
2156
- 0,
2157
- 1,
2158
- 0,
2159
- 0
2160
- );
2161
- break;
2162
- case "matrix":
2163
- ctx.transform(
2164
- args[0] ?? 1,
2165
- args[1] ?? 0,
2166
- args[2] ?? 0,
2167
- args[3] ?? 1,
2168
- args[4] ?? 0,
2169
- args[5] ?? 0
2170
- );
2171
- break;
2172
- }
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
+ );
2173
2242
  }
2243
+ if (clipPath) ctx.restore();
2244
+ if (savedTransform) ctx.restore();
2174
2245
  }
2175
- 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) {
2176
2247
  const children = normalizeChildren(node);
2177
2248
  if (children.length === 0) return;
2178
- const merged = mergeStyleIntoProps(node.props);
2179
- const groupInherited = {
2180
- fill: resolveCurrentColor(merged.fill, color) ?? inherited.fill,
2181
- stroke: resolveCurrentColor(merged.stroke, color) ?? inherited.stroke,
2182
- strokeWidth: merged.strokeWidth ?? merged["stroke-width"] ?? inherited.strokeWidth,
2183
- strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"] ?? inherited.strokeLinecap,
2184
- strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? inherited.strokeLinejoin,
2185
- strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? inherited.strokeOpacity
2186
- };
2187
- const opacity = Number(merged.opacity ?? 1);
2188
- const transform = merged.transform;
2189
- if (transform || opacity < 1) {
2190
- ctx.save();
2191
- if (transform) applySvgTransform(ctx, transform);
2192
- if (opacity < 1) ctx.globalAlpha *= opacity;
2193
- }
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);
2194
2253
  for (const child of children) {
2195
2254
  if (child != null && typeof child === "object") {
2196
2255
  drawSvgChild(ctx, child, groupInherited, color, defs, vbW, vbH);
2197
2256
  }
2198
2257
  }
2199
- if (transform || opacity < 1) {
2200
- ctx.restore();
2201
- }
2258
+ if (saved) ctx.restore();
2202
2259
  }
2203
- function parsePoints(value) {
2204
- if (!value) return [];
2205
- const nums = value.trim().split(/[\s,]+/).map(Number);
2206
- const result = [];
2207
- for (let i = 0; i + 1 < nums.length; i += 2) {
2208
- 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);
2209
2285
  }
2210
- return result;
2286
+ releaseOffscreen(elemCanvas);
2211
2287
  }
2212
- function applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox) {
2213
- const fill = resolveCurrentColor(props.fill, color) ?? inherited.fill;
2214
- const fillRule = props.fillRule ?? props["fill-rule"];
2215
- const fillOpacity = Number(props.fillOpacity ?? props["fill-opacity"] ?? 1);
2216
- const fillRef = parseUrlRef(fill);
2217
- if (fillRef) {
2218
- const gradientDef = defs.gradients.get(fillRef);
2219
- if (gradientDef) {
2220
- if (fillOpacity < 1) {
2221
- ctx.save();
2222
- ctx.globalAlpha *= fillOpacity;
2223
- fillWithSvgGradient(
2224
- ctx,
2225
- gradientDef,
2226
- bbox,
2227
- path,
2228
- fillRule ?? "nonzero"
2229
- );
2230
- ctx.restore();
2231
- } else {
2232
- fillWithSvgGradient(
2233
- ctx,
2234
- gradientDef,
2235
- bbox,
2236
- path,
2237
- fillRule ?? "nonzero"
2238
- );
2239
- }
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);
2240
2306
  }
2241
- } else if (fill !== "none") {
2242
- ctx.fillStyle = applyOpacity(fill, fillOpacity);
2243
- ctx.fill(path, fillRule ?? "nonzero");
2244
2307
  }
2245
- applyStroke(ctx, props, path, inherited, color, defs, bbox);
2246
- }
2247
- function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
2248
- const stroke = resolveCurrentColor(props.stroke, color) ?? inherited.stroke;
2249
- if (!stroke || stroke === "none") return;
2250
- const strokeOpacity = Number(
2251
- props.strokeOpacity ?? props["stroke-opacity"] ?? inherited.strokeOpacity ?? 1
2252
- );
2253
- ctx.lineWidth = Number(
2254
- props.strokeWidth ?? props["stroke-width"] ?? inherited.strokeWidth ?? 1
2255
- );
2256
- ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? inherited.strokeLinecap ?? "butt";
2257
- ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? inherited.strokeLinejoin ?? "miter";
2258
- const strokeRef = parseUrlRef(stroke);
2259
- if (strokeRef) {
2260
- const gradientDef = defs.gradients.get(strokeRef);
2261
- if (gradientDef) {
2262
- if (strokeOpacity < 1) {
2263
- ctx.save();
2264
- ctx.globalAlpha *= strokeOpacity;
2265
- strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2266
- ctx.restore();
2267
- } else {
2268
- strokeWithSvgGradient(ctx, gradientDef, bbox, path);
2269
- }
2270
- 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);
2271
2320
  }
2272
2321
  }
2273
- ctx.strokeStyle = applyOpacity(stroke, strokeOpacity);
2274
- ctx.stroke(path);
2322
+ ctx.restore();
2275
2323
  }
2276
2324
 
2277
2325
  // src/jsx/draw/text.ts
@@ -2558,7 +2606,7 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
2558
2606
  }
2559
2607
 
2560
2608
  // src/jsx/draw/index.ts
2561
- async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2609
+ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle, imageCache) {
2562
2610
  const x = parentX + node.x;
2563
2611
  const y = parentY + node.y;
2564
2612
  const { width, height, style } = node;
@@ -2578,6 +2626,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2578
2626
  const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
2579
2627
  offCtx.save();
2580
2628
  offCtx.scale(qx, qy);
2629
+ const cache2 = imageCache ?? /* @__PURE__ */ new Map();
2581
2630
  await drawNodeCore(
2582
2631
  offCtx,
2583
2632
  node,
@@ -2587,6 +2636,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2587
2636
  1 - y,
2588
2637
  debug,
2589
2638
  emojiStyle,
2639
+ cache2,
2590
2640
  transformWithoutScale
2591
2641
  );
2592
2642
  offCtx.restore();
@@ -2620,7 +2670,18 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
2620
2670
  return;
2621
2671
  }
2622
2672
  }
2623
- 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
+ );
2624
2685
  }
2625
2686
  function extractScale(transform) {
2626
2687
  const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
@@ -2632,7 +2693,7 @@ function extractScale(transform) {
2632
2693
  const remaining = transform.replace(fullMatch, "").trim();
2633
2694
  return { sx, sy, remaining };
2634
2695
  }
2635
- 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) {
2636
2697
  const x = parentX + node.x + offsetX;
2637
2698
  const y = parentY + node.y + offsetY;
2638
2699
  const { width, height, style } = node;
@@ -2698,7 +2759,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2698
2759
  if (hasRadius(borderRadius)) {
2699
2760
  applyClip(ctx, x, y, width, height, borderRadius);
2700
2761
  }
2701
- const image = await loadImage3(urlMatch[1]);
2762
+ const image = await cachedLoadImage(imageCache, urlMatch[1]);
2702
2763
  const bgSize = style.backgroundSize;
2703
2764
  if (bgSize === "cover") {
2704
2765
  const r = computeCover(
@@ -2807,8 +2868,8 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2807
2868
  imgY,
2808
2869
  imgW,
2809
2870
  imgH,
2810
- style,
2811
- node.props.__loadedImage
2871
+ imageCache,
2872
+ style
2812
2873
  );
2813
2874
  }
2814
2875
  if (node.type === "svg") {
@@ -2816,7 +2877,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2816
2877
  } else {
2817
2878
  for (const child of node.children) {
2818
2879
  if (offsetX === 0 && offsetY === 0) {
2819
- await drawNode(ctx, child, x, y, debug, emojiStyle);
2880
+ await drawNode(ctx, child, x, y, debug, emojiStyle, imageCache);
2820
2881
  } else {
2821
2882
  await drawNodeCore(
2822
2883
  ctx,
@@ -2827,7 +2888,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
2827
2888
  0,
2828
2889
  debug,
2829
2890
  emojiStyle,
2830
- void 0
2891
+ imageCache
2831
2892
  );
2832
2893
  }
2833
2894
  }
@@ -2899,9 +2960,6 @@ function parseAngle(value) {
2899
2960
  return parseFloat(value);
2900
2961
  }
2901
2962
 
2902
- // src/jsx/layout.ts
2903
- import { loadImage as loadImage4 } from "@napi-rs/canvas";
2904
-
2905
2963
  // src/jsx/style/expand.ts
2906
2964
  var SIDES = ["Top", "Right", "Bottom", "Left"];
2907
2965
  function parseSides(value) {
@@ -3288,6 +3346,7 @@ function applyEdgeValue(node, setter, edge, value) {
3288
3346
 
3289
3347
  // src/jsx/layout.ts
3290
3348
  async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
3349
+ const imageCache = /* @__PURE__ */ new Map();
3291
3350
  const elementYogaNode = createYogaNode();
3292
3351
  const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
3293
3352
  const elementNode = await buildNode(
@@ -3298,7 +3357,8 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3298
3357
  containerHeight,
3299
3358
  ctx,
3300
3359
  emojiEnabled,
3301
- fontFamilies
3360
+ fontFamilies,
3361
+ imageCache
3302
3362
  );
3303
3363
  const rootYogaNode = createYogaNode();
3304
3364
  rootYogaNode.setWidth(containerWidth);
@@ -3308,7 +3368,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3308
3368
  rootYogaNode.calculateLayout(containerWidth, containerHeight);
3309
3369
  const elementLayout = extractLayout(elementNode, elementYogaNode);
3310
3370
  freeYogaNode(rootYogaNode);
3311
- return {
3371
+ const tree = {
3312
3372
  type: "div",
3313
3373
  style: rootStyle,
3314
3374
  children: [elementLayout],
@@ -3318,8 +3378,9 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
3318
3378
  width: containerWidth,
3319
3379
  height: containerHeight
3320
3380
  };
3381
+ return { tree, imageCache };
3321
3382
  }
3322
- async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
3383
+ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies, imageCache) {
3323
3384
  if (element === null || element === void 0 || typeof element === "boolean") {
3324
3385
  return {
3325
3386
  type: "empty",
@@ -3357,7 +3418,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3357
3418
  viewportHeight,
3358
3419
  ctx,
3359
3420
  emojiEnabled,
3360
- fontFamilies
3421
+ fontFamilies,
3422
+ imageCache
3361
3423
  );
3362
3424
  }
3363
3425
  const props = el.props ?? {};
@@ -3417,7 +3479,7 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3417
3479
  const src = props.src;
3418
3480
  if (src) {
3419
3481
  try {
3420
- const image = await loadImage4(src);
3482
+ const image = await cachedLoadImage(imageCache ?? /* @__PURE__ */ new Map(), src);
3421
3483
  const naturalW = image.width;
3422
3484
  const naturalH = image.height;
3423
3485
  const wSet = style.width !== void 0;
@@ -3427,7 +3489,6 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3427
3489
  } else if (hSet && !wSet && typeof style.height === "number" && naturalH > 0) {
3428
3490
  style.width = style.height * (naturalW / naturalH);
3429
3491
  }
3430
- props.__loadedImage = image;
3431
3492
  } catch {
3432
3493
  }
3433
3494
  }
@@ -3505,7 +3566,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3505
3566
  viewportHeight,
3506
3567
  ctx,
3507
3568
  emojiEnabled,
3508
- fontFamilies
3569
+ fontFamilies,
3570
+ imageCache
3509
3571
  )
3510
3572
  );
3511
3573
  }
@@ -3579,7 +3641,7 @@ async function renderReactElement(ctx, element, options) {
3579
3641
  const height = options.height ?? ctx.canvas.height;
3580
3642
  const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
3581
3643
  const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
3582
- const layoutTree = await buildLayoutTree(
3644
+ const { tree: layoutTree, imageCache } = await buildLayoutTree(
3583
3645
  element,
3584
3646
  width,
3585
3647
  height,
@@ -3587,7 +3649,15 @@ async function renderReactElement(ctx, element, options) {
3587
3649
  !!emojiStyle,
3588
3650
  fontFamilies
3589
3651
  );
3590
- 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
+ );
3591
3661
  }
3592
3662
 
3593
3663
  // src/index.ts
@@ -3603,7 +3673,7 @@ export {
3603
3673
  Image,
3604
3674
  LottieAnimation,
3605
3675
  createCanvas3 as createCanvas,
3606
- loadImage5 as loadImage,
3676
+ loadImage3 as loadImage,
3607
3677
  loadLottie,
3608
3678
  registerFont,
3609
3679
  registerFontFromPath,