@effing/canvas 0.18.5 → 0.18.6

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
@@ -74,8 +74,28 @@ function getScratchCtx() {
74
74
  }
75
75
  return scratchCtx;
76
76
  }
77
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
78
+ "serif",
79
+ "sans-serif",
80
+ "monospace",
81
+ "cursive",
82
+ "fantasy",
83
+ "system-ui",
84
+ "ui-serif",
85
+ "ui-sans-serif",
86
+ "ui-monospace",
87
+ "ui-rounded",
88
+ "math",
89
+ "emoji",
90
+ "fangsong"
91
+ ]);
92
+ function quoteFontFamily(family) {
93
+ if (!family || GENERIC_FAMILIES.has(family)) return family;
94
+ return `"${family}"`;
95
+ }
77
96
  function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
78
- ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
97
+ const quoted = fontFamily.split(",").map((f) => quoteFontFamily(f.trim())).join(", ");
98
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoted}`;
79
99
  }
80
100
  function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx) {
81
101
  const c = ctx ?? getScratchCtx();
@@ -678,10 +698,7 @@ function drawRect(ctx, x, y, width, height, style) {
678
698
  drawBorders(ctx, x, y, width, height, style, borderRadius);
679
699
  }
680
700
  function resolveRadius(v, width, height) {
681
- if (typeof v === "string" && v.endsWith("%")) {
682
- const pct = parseFloat(v) / 100;
683
- return pct * Math.min(width, height);
684
- }
701
+ if (typeof v === "string") return parseCSSLength(v, Math.min(width, height));
685
702
  return toNumber(v);
686
703
  }
687
704
  function getBorderRadius(style, width, height) {
@@ -793,12 +810,6 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
793
810
  ctx.fill();
794
811
  ctx.restore();
795
812
  }
796
- function toNumber(v) {
797
- if (typeof v === "number") return v;
798
- if (v === void 0 || v === null) return 0;
799
- const n = parseFloat(String(v));
800
- return isNaN(n) ? 0 : n;
801
- }
802
813
 
803
814
  // src/jsx/draw/svg.ts
804
815
  import { Path2D } from "@napi-rs/canvas";
@@ -811,6 +822,97 @@ function mergeStyleIntoProps(props) {
811
822
  if (!style) return props;
812
823
  return { ...props, ...style };
813
824
  }
825
+ function collectDefs(children) {
826
+ const defs = /* @__PURE__ */ new Map();
827
+ for (const child of children) {
828
+ if (child.type !== "defs") continue;
829
+ const defsChildren = normalizeChildren(child);
830
+ for (const def of defsChildren) {
831
+ if (def.type === "clipPath") {
832
+ const id = def.props.id;
833
+ if (id) {
834
+ defs.set(id, normalizeChildren(def));
835
+ }
836
+ }
837
+ }
838
+ }
839
+ return defs;
840
+ }
841
+ function parseUrlRef(value) {
842
+ if (typeof value !== "string") return void 0;
843
+ const m = value.match(/^url\(#(.+)\)$/);
844
+ return m?.[1];
845
+ }
846
+ function normalizeChildren(node) {
847
+ const raw = node.children ?? node.props.children;
848
+ if (raw == null) return [];
849
+ return Array.isArray(raw) ? raw : [raw];
850
+ }
851
+ function buildPath(child) {
852
+ const props = mergeStyleIntoProps(child.props);
853
+ switch (child.type) {
854
+ case "path": {
855
+ const d = props.d;
856
+ if (!d) return void 0;
857
+ return new Path2D(d);
858
+ }
859
+ case "circle": {
860
+ const cx = Number(props.cx ?? 0);
861
+ const cy = Number(props.cy ?? 0);
862
+ const r = Number(props.r ?? 0);
863
+ if (r <= 0) return void 0;
864
+ const p = new Path2D();
865
+ p.arc(cx, cy, r, 0, Math.PI * 2);
866
+ return p;
867
+ }
868
+ case "rect": {
869
+ const rx = Number(props.x ?? 0);
870
+ const ry = Number(props.y ?? 0);
871
+ const w = Number(props.width ?? 0);
872
+ const h = Number(props.height ?? 0);
873
+ if (w <= 0 || h <= 0) return void 0;
874
+ const p = new Path2D();
875
+ p.rect(rx, ry, w, h);
876
+ return p;
877
+ }
878
+ case "ellipse": {
879
+ const cx = Number(props.cx ?? 0);
880
+ const cy = Number(props.cy ?? 0);
881
+ const rx = Number(props.rx ?? 0);
882
+ const ry = Number(props.ry ?? 0);
883
+ if (rx <= 0 || ry <= 0) return void 0;
884
+ const p = new Path2D();
885
+ p.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
886
+ return p;
887
+ }
888
+ case "polygon": {
889
+ const points = parsePoints(props.points);
890
+ if (points.length < 2) return void 0;
891
+ const p = new Path2D();
892
+ p.moveTo(points[0][0], points[0][1]);
893
+ for (let i = 1; i < points.length; i++) {
894
+ p.lineTo(points[i][0], points[i][1]);
895
+ }
896
+ p.closePath();
897
+ return p;
898
+ }
899
+ default:
900
+ return void 0;
901
+ }
902
+ }
903
+ function buildClipPath(shapes) {
904
+ let combined;
905
+ for (const shape of shapes) {
906
+ const p = buildPath(shape);
907
+ if (!p) continue;
908
+ if (!combined) {
909
+ combined = p;
910
+ } else {
911
+ combined.addPath(p);
912
+ }
913
+ }
914
+ return combined;
915
+ }
814
916
  function drawSvgContainer(ctx, node, x, y, width, height) {
815
917
  ctx.save();
816
918
  ctx.translate(x, y);
@@ -831,17 +933,25 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
831
933
  const children = node.props.children;
832
934
  if (children != null) {
833
935
  const childArray = Array.isArray(children) ? children : [children];
834
- for (const child of childArray) {
835
- if (child != null && typeof child === "object") {
836
- drawSvgChild(ctx, child, inheritedFill, color);
837
- }
936
+ const svgChildren = childArray.filter(
937
+ (c) => c != null && typeof c === "object"
938
+ );
939
+ const defs = collectDefs(svgChildren);
940
+ for (const child of svgChildren) {
941
+ drawSvgChild(ctx, child, inheritedFill, color, defs);
838
942
  }
839
943
  }
840
944
  ctx.restore();
841
945
  }
842
- function drawSvgChild(ctx, child, inheritedFill, color) {
946
+ function drawSvgChild(ctx, child, inheritedFill, color, defs = /* @__PURE__ */ new Map()) {
843
947
  const { type } = child;
844
948
  const props = mergeStyleIntoProps(child.props);
949
+ if (type === "defs" || type === "clipPath") return;
950
+ const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
951
+ const clipShapes = clipRef ? defs.get(clipRef) : void 0;
952
+ const clipPath = clipShapes ? buildClipPath(clipShapes) : void 0;
953
+ if (clipPath) ctx.save();
954
+ if (clipPath) ctx.clip(clipPath);
845
955
  switch (type) {
846
956
  case "path":
847
957
  drawPath(ctx, props, inheritedFill, color);
@@ -865,9 +975,10 @@ function drawSvgChild(ctx, child, inheritedFill, color) {
865
975
  drawPolyline(ctx, props, inheritedFill, color);
866
976
  break;
867
977
  case "g":
868
- drawGroup(ctx, child, inheritedFill, color);
978
+ drawGroup(ctx, child, inheritedFill, color, defs);
869
979
  break;
870
980
  }
981
+ if (clipPath) ctx.restore();
871
982
  }
872
983
  function drawPath(ctx, props, inheritedFill, color) {
873
984
  const d = props.d;
@@ -935,15 +1046,14 @@ function drawPolyline(ctx, props, inheritedFill, color) {
935
1046
  }
936
1047
  applyFillAndStroke(ctx, props, path, inheritedFill, color);
937
1048
  }
938
- function drawGroup(ctx, node, inheritedFill, color) {
939
- const children = node.children ?? node.props.children;
940
- if (children == null) return;
1049
+ function drawGroup(ctx, node, inheritedFill, color, defs = /* @__PURE__ */ new Map()) {
1050
+ const children = normalizeChildren(node);
1051
+ if (children.length === 0) return;
941
1052
  const merged = mergeStyleIntoProps(node.props);
942
1053
  const groupFill = resolveCurrentColor(merged.fill, color) ?? inheritedFill;
943
- const childArray = Array.isArray(children) ? children : [children];
944
- for (const child of childArray) {
1054
+ for (const child of children) {
945
1055
  if (child != null && typeof child === "object") {
946
- drawSvgChild(ctx, child, groupFill, color);
1056
+ drawSvgChild(ctx, child, groupFill, color, defs);
947
1057
  }
948
1058
  }
949
1059
  }
@@ -1419,12 +1529,12 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1419
1529
  ctx.strokeRect(x, y, width, height);
1420
1530
  }
1421
1531
  if (node.textContent !== void 0 && node.textContent !== "") {
1422
- const paddingTop = toNumber2(style.paddingTop);
1423
- const paddingLeft = toNumber2(style.paddingLeft);
1424
- const paddingRight = toNumber2(style.paddingRight);
1425
- const borderTopW = toNumber2(style.borderTopWidth);
1426
- const borderLeftW = toNumber2(style.borderLeftWidth);
1427
- const borderRightW = toNumber2(style.borderRightWidth);
1532
+ const paddingTop = toNumber(style.paddingTop);
1533
+ const paddingLeft = toNumber(style.paddingLeft);
1534
+ const paddingRight = toNumber(style.paddingRight);
1535
+ const borderTopW = toNumber(style.borderTopWidth);
1536
+ const borderLeftW = toNumber(style.borderLeftWidth);
1537
+ const borderRightW = toNumber(style.borderRightWidth);
1428
1538
  const contentX = x + paddingLeft + borderLeftW;
1429
1539
  const contentY = y + paddingTop + borderTopW;
1430
1540
  const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
@@ -1445,10 +1555,10 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1445
1555
  );
1446
1556
  }
1447
1557
  if (node.type === "img" && node.props.src) {
1448
- const paddingTop = toNumber2(style.paddingTop);
1449
- const paddingLeft = toNumber2(style.paddingLeft);
1450
- const paddingRight = toNumber2(style.paddingRight);
1451
- const paddingBottom = toNumber2(style.paddingBottom);
1558
+ const paddingTop = toNumber(style.paddingTop);
1559
+ const paddingLeft = toNumber(style.paddingLeft);
1560
+ const paddingRight = toNumber(style.paddingRight);
1561
+ const paddingBottom = toNumber(style.paddingBottom);
1452
1562
  const imgX = x + paddingLeft;
1453
1563
  const imgY = y + paddingTop;
1454
1564
  const imgW = width - paddingLeft - paddingRight;
@@ -1614,12 +1724,12 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1614
1724
  ctx.strokeRect(x, y, width, height);
1615
1725
  }
1616
1726
  if (node.textContent !== void 0 && node.textContent !== "") {
1617
- const paddingTop = toNumber2(style.paddingTop);
1618
- const paddingLeft = toNumber2(style.paddingLeft);
1619
- const paddingRight = toNumber2(style.paddingRight);
1620
- const borderTopW = toNumber2(style.borderTopWidth);
1621
- const borderLeftW = toNumber2(style.borderLeftWidth);
1622
- const borderRightW = toNumber2(style.borderRightWidth);
1727
+ const paddingTop = toNumber(style.paddingTop);
1728
+ const paddingLeft = toNumber(style.paddingLeft);
1729
+ const paddingRight = toNumber(style.paddingRight);
1730
+ const borderTopW = toNumber(style.borderTopWidth);
1731
+ const borderLeftW = toNumber(style.borderLeftWidth);
1732
+ const borderRightW = toNumber(style.borderRightWidth);
1623
1733
  const contentX = x + paddingLeft + borderLeftW;
1624
1734
  const contentY = y + paddingTop + borderTopW;
1625
1735
  const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
@@ -1640,10 +1750,10 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1640
1750
  );
1641
1751
  }
1642
1752
  if (node.type === "img" && node.props.src) {
1643
- const paddingTop = toNumber2(style.paddingTop);
1644
- const paddingLeft = toNumber2(style.paddingLeft);
1645
- const paddingRight = toNumber2(style.paddingRight);
1646
- const paddingBottom = toNumber2(style.paddingBottom);
1753
+ const paddingTop = toNumber(style.paddingTop);
1754
+ const paddingLeft = toNumber(style.paddingLeft);
1755
+ const paddingRight = toNumber(style.paddingRight);
1756
+ const paddingBottom = toNumber(style.paddingBottom);
1647
1757
  const imgX = x + paddingLeft;
1648
1758
  const imgY = y + paddingTop;
1649
1759
  const imgW = width - paddingLeft - paddingRight;
@@ -1674,6 +1784,10 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1674
1784
  }
1675
1785
  ctx.restore();
1676
1786
  }
1787
+ function parseCSSLength(value, referenceSize) {
1788
+ if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
1789
+ return parseFloat(value);
1790
+ }
1677
1791
  function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
1678
1792
  let ox = x + width / 2;
1679
1793
  let oy = y + height / 2;
@@ -1690,8 +1804,11 @@ function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
1690
1804
  case "translate":
1691
1805
  case "translateX":
1692
1806
  case "translateY": {
1693
- const tx = name === "translateY" ? 0 : parseFloat(values[0]);
1694
- const ty = name === "translateX" ? 0 : parseFloat(values[name === "translate" ? 1 : 0] ?? "0");
1807
+ const tx = name === "translateY" ? 0 : parseCSSLength(values[0], width);
1808
+ const ty = name === "translateX" ? 0 : parseCSSLength(
1809
+ values[name === "translate" ? 1 : 0] ?? "0",
1810
+ height
1811
+ );
1695
1812
  ctx.translate(tx, ty);
1696
1813
  break;
1697
1814
  }
@@ -1727,8 +1844,7 @@ function resolveOrigin(value, base, size) {
1727
1844
  if (value === "left" || value === "top") return base;
1728
1845
  if (value === "right" || value === "bottom") return base + size;
1729
1846
  if (value === "center") return base + size / 2;
1730
- if (value.endsWith("%")) return base + parseFloat(value) / 100 * size;
1731
- return base + parseFloat(value);
1847
+ return base + parseCSSLength(value, size);
1732
1848
  }
1733
1849
  function parseAngle(value) {
1734
1850
  if (value.endsWith("deg")) return parseFloat(value) * Math.PI / 180;
@@ -1736,7 +1852,7 @@ function parseAngle(value) {
1736
1852
  if (value.endsWith("turn")) return parseFloat(value) * 2 * Math.PI;
1737
1853
  return parseFloat(value);
1738
1854
  }
1739
- function toNumber2(v) {
1855
+ function toNumber(v) {
1740
1856
  if (typeof v === "number") return v;
1741
1857
  if (v === void 0 || v === null) return 0;
1742
1858
  const n = parseFloat(String(v));
@@ -1791,7 +1907,7 @@ function parseValue(v) {
1791
1907
  if (s !== "" && !isNaN(n)) return n;
1792
1908
  return s;
1793
1909
  }
1794
- function expandStyle(raw) {
1910
+ function expandStyle(raw, fontFamilies) {
1795
1911
  const style = { ...raw };
1796
1912
  if (style.margin !== void 0) {
1797
1913
  const sides = parseSides(String(style.margin));
@@ -1904,7 +2020,14 @@ function expandStyle(raw) {
1904
2020
  if (style.overflowY === void 0) style.overflowY = style.overflow;
1905
2021
  }
1906
2022
  if (typeof style.fontFamily === "string") {
1907
- style.fontFamily = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean).join(", ");
2023
+ const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
2024
+ if (fontFamilies) {
2025
+ const present = new Set(families);
2026
+ for (const name of fontFamilies) {
2027
+ if (!present.has(name)) families.push(name);
2028
+ }
2029
+ }
2030
+ style.fontFamily = families.join(", ");
1908
2031
  }
1909
2032
  return style;
1910
2033
  }
@@ -2075,8 +2198,41 @@ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = DEFAU
2075
2198
  style[prop] = resolved;
2076
2199
  }
2077
2200
  }
2201
+ if (style.transform) {
2202
+ style.transform = resolveTransformUnits(
2203
+ style.transform,
2204
+ viewportWidth,
2205
+ viewportHeight,
2206
+ fontSize,
2207
+ rootFontSize
2208
+ );
2209
+ }
2210
+ if (style.transformOrigin) {
2211
+ style.transformOrigin = resolveTransformUnits(
2212
+ style.transformOrigin,
2213
+ viewportWidth,
2214
+ viewportHeight,
2215
+ fontSize,
2216
+ rootFontSize
2217
+ );
2218
+ }
2078
2219
  return style;
2079
2220
  }
2221
+ function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
2222
+ return transform.replace(
2223
+ /(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
2224
+ (match) => {
2225
+ const resolved = resolveUnit(
2226
+ match,
2227
+ viewportWidth,
2228
+ viewportHeight,
2229
+ fontSize,
2230
+ rootFontSize
2231
+ );
2232
+ return typeof resolved === "number" ? String(resolved) : match;
2233
+ }
2234
+ );
2235
+ }
2080
2236
 
2081
2237
  // src/jsx/yoga.ts
2082
2238
  import Yoga, {
@@ -2265,16 +2421,18 @@ function applyEdgeValue(node, setter, edge, value) {
2265
2421
  }
2266
2422
 
2267
2423
  // src/jsx/layout.ts
2268
- async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled) {
2424
+ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
2269
2425
  const rootYogaNode = createYogaNode();
2426
+ const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
2270
2427
  const rootNode = await buildNode(
2271
2428
  element,
2272
- DEFAULT_STYLE,
2429
+ rootStyle,
2273
2430
  rootYogaNode,
2274
2431
  containerWidth,
2275
2432
  containerHeight,
2276
2433
  ctx,
2277
- emojiEnabled
2434
+ emojiEnabled,
2435
+ fontFamilies
2278
2436
  );
2279
2437
  rootYogaNode.setWidth(containerWidth);
2280
2438
  rootYogaNode.setHeight(containerHeight);
@@ -2283,7 +2441,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
2283
2441
  freeYogaNode(rootYogaNode);
2284
2442
  return layoutTree;
2285
2443
  }
2286
- async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled) {
2444
+ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
2287
2445
  if (element === null || element === void 0 || typeof element === "boolean") {
2288
2446
  return {
2289
2447
  type: "empty",
@@ -2324,7 +2482,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2324
2482
  viewportWidth,
2325
2483
  viewportHeight,
2326
2484
  ctx,
2327
- emojiEnabled
2485
+ emojiEnabled,
2486
+ fontFamilies
2328
2487
  )
2329
2488
  );
2330
2489
  }
@@ -2349,12 +2508,13 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2349
2508
  viewportWidth,
2350
2509
  viewportHeight,
2351
2510
  ctx,
2352
- emojiEnabled
2511
+ emojiEnabled,
2512
+ fontFamilies
2353
2513
  );
2354
2514
  }
2355
2515
  const props = el.props ?? {};
2356
2516
  const rawStyle = props.style ?? {};
2357
- const expanded = expandStyle(rawStyle);
2517
+ const expanded = expandStyle(rawStyle, fontFamilies);
2358
2518
  const style = resolveStyle(expanded, parentStyle);
2359
2519
  resolveUnits(style, viewportWidth, viewportHeight);
2360
2520
  const tagName = String(type);
@@ -2472,7 +2632,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2472
2632
  viewportWidth,
2473
2633
  viewportHeight,
2474
2634
  ctx,
2475
- emojiEnabled
2635
+ emojiEnabled,
2636
+ fontFamilies
2476
2637
  )
2477
2638
  );
2478
2639
  }
@@ -2540,12 +2701,14 @@ async function renderReactElement(ctx, element, options) {
2540
2701
  const width = ctx.canvas.width;
2541
2702
  const height = ctx.canvas.height;
2542
2703
  const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
2704
+ const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
2543
2705
  const layoutTree = await buildLayoutTree(
2544
2706
  element,
2545
2707
  width,
2546
2708
  height,
2547
2709
  ctx,
2548
- !!emojiStyle
2710
+ !!emojiStyle,
2711
+ fontFamilies
2549
2712
  );
2550
2713
  await drawNode(ctx, layoutTree, 0, 0, options.debug ?? false, emojiStyle);
2551
2714
  }