@effing/canvas 0.18.4 → 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();
@@ -650,7 +670,7 @@ async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
650
670
 
651
671
  // src/jsx/draw/rect.ts
652
672
  function drawRect(ctx, x, y, width, height, style) {
653
- const borderRadius = getBorderRadius(style);
673
+ const borderRadius = getBorderRadius(style, width, height);
654
674
  const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
655
675
  if (style.boxShadow) {
656
676
  drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
@@ -677,16 +697,20 @@ function drawRect(ctx, x, y, width, height, style) {
677
697
  }
678
698
  drawBorders(ctx, x, y, width, height, style, borderRadius);
679
699
  }
680
- function getBorderRadius(style) {
700
+ function resolveRadius(v, width, height) {
701
+ if (typeof v === "string") return parseCSSLength(v, Math.min(width, height));
702
+ return toNumber(v);
703
+ }
704
+ function getBorderRadius(style, width, height) {
681
705
  return {
682
- topLeft: toNumber(style.borderTopLeftRadius),
683
- topRight: toNumber(style.borderTopRightRadius),
684
- bottomRight: toNumber(style.borderBottomRightRadius),
685
- bottomLeft: toNumber(style.borderBottomLeftRadius)
706
+ topLeft: resolveRadius(style.borderTopLeftRadius, width, height),
707
+ topRight: resolveRadius(style.borderTopRightRadius, width, height),
708
+ bottomRight: resolveRadius(style.borderBottomRightRadius, width, height),
709
+ bottomLeft: resolveRadius(style.borderBottomLeftRadius, width, height)
686
710
  };
687
711
  }
688
- function getBorderRadiusFromStyle(style) {
689
- return getBorderRadius(style);
712
+ function getBorderRadiusFromStyle(style, width, height) {
713
+ return getBorderRadius(style, width, height);
690
714
  }
691
715
  function drawBorders(ctx, x, y, width, height, style, borderRadius) {
692
716
  const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
@@ -786,12 +810,6 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
786
810
  ctx.fill();
787
811
  ctx.restore();
788
812
  }
789
- function toNumber(v) {
790
- if (typeof v === "number") return v;
791
- if (v === void 0 || v === null) return 0;
792
- const n = parseFloat(String(v));
793
- return isNaN(n) ? 0 : n;
794
- }
795
813
 
796
814
  // src/jsx/draw/svg.ts
797
815
  import { Path2D } from "@napi-rs/canvas";
@@ -804,6 +822,97 @@ function mergeStyleIntoProps(props) {
804
822
  if (!style) return props;
805
823
  return { ...props, ...style };
806
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
+ }
807
916
  function drawSvgContainer(ctx, node, x, y, width, height) {
808
917
  ctx.save();
809
918
  ctx.translate(x, y);
@@ -824,17 +933,25 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
824
933
  const children = node.props.children;
825
934
  if (children != null) {
826
935
  const childArray = Array.isArray(children) ? children : [children];
827
- for (const child of childArray) {
828
- if (child != null && typeof child === "object") {
829
- drawSvgChild(ctx, child, inheritedFill, color);
830
- }
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);
831
942
  }
832
943
  }
833
944
  ctx.restore();
834
945
  }
835
- function drawSvgChild(ctx, child, inheritedFill, color) {
946
+ function drawSvgChild(ctx, child, inheritedFill, color, defs = /* @__PURE__ */ new Map()) {
836
947
  const { type } = child;
837
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);
838
955
  switch (type) {
839
956
  case "path":
840
957
  drawPath(ctx, props, inheritedFill, color);
@@ -858,9 +975,10 @@ function drawSvgChild(ctx, child, inheritedFill, color) {
858
975
  drawPolyline(ctx, props, inheritedFill, color);
859
976
  break;
860
977
  case "g":
861
- drawGroup(ctx, child, inheritedFill, color);
978
+ drawGroup(ctx, child, inheritedFill, color, defs);
862
979
  break;
863
980
  }
981
+ if (clipPath) ctx.restore();
864
982
  }
865
983
  function drawPath(ctx, props, inheritedFill, color) {
866
984
  const d = props.d;
@@ -928,15 +1046,14 @@ function drawPolyline(ctx, props, inheritedFill, color) {
928
1046
  }
929
1047
  applyFillAndStroke(ctx, props, path, inheritedFill, color);
930
1048
  }
931
- function drawGroup(ctx, node, inheritedFill, color) {
932
- const children = node.children ?? node.props.children;
933
- 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;
934
1052
  const merged = mergeStyleIntoProps(node.props);
935
1053
  const groupFill = resolveCurrentColor(merged.fill, color) ?? inheritedFill;
936
- const childArray = Array.isArray(children) ? children : [children];
937
- for (const child of childArray) {
1054
+ for (const child of children) {
938
1055
  if (child != null && typeof child === "object") {
939
- drawSvgChild(ctx, child, groupFill, color);
1056
+ drawSvgChild(ctx, child, groupFill, color, defs);
940
1057
  }
941
1058
  }
942
1059
  }
@@ -951,9 +1068,14 @@ function parsePoints(value) {
951
1068
  }
952
1069
  function applyFillAndStroke(ctx, props, path, inheritedFill, color) {
953
1070
  const fill = resolveCurrentColor(props.fill, color) ?? inheritedFill;
1071
+ const fillRule = props.fillRule ?? props["fill-rule"];
1072
+ const clipRule = props.clipRule ?? props["clip-rule"];
1073
+ if (clipRule) {
1074
+ ctx.clip(path, clipRule);
1075
+ }
954
1076
  if (fill !== "none") {
955
1077
  ctx.fillStyle = fill;
956
- ctx.fill(path);
1078
+ ctx.fill(path, fillRule ?? "nonzero");
957
1079
  }
958
1080
  applyStroke(ctx, props, path, color);
959
1081
  }
@@ -1014,7 +1136,7 @@ async function loadEmoji(type, code) {
1014
1136
  }
1015
1137
 
1016
1138
  // src/jsx/text/emoji-split.ts
1017
- function splitTextIntoRuns(text, measureText2, emojiSize) {
1139
+ function splitTextIntoRuns(text, measureText2, emojiSize, letterSpacing = 0) {
1018
1140
  const runs = [];
1019
1141
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1020
1142
  let currentText = "";
@@ -1024,13 +1146,18 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1024
1146
  if (isEmojiGrapheme(segment)) {
1025
1147
  if (currentText) {
1026
1148
  const textWidth = measureText2(currentText);
1149
+ const graphemeCount = [
1150
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1151
+ currentText
1152
+ )
1153
+ ].length;
1027
1154
  runs.push({
1028
1155
  kind: "text",
1029
1156
  text: currentText,
1030
1157
  x: textStartX,
1031
- width: textWidth
1158
+ width: textWidth + letterSpacing * graphemeCount
1032
1159
  });
1033
- currentX = textStartX + textWidth;
1160
+ currentX = textStartX + textWidth + letterSpacing * graphemeCount;
1034
1161
  currentText = "";
1035
1162
  }
1036
1163
  runs.push({
@@ -1039,7 +1166,7 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1039
1166
  x: currentX,
1040
1167
  width: emojiSize
1041
1168
  });
1042
- currentX += emojiSize;
1169
+ currentX += emojiSize + letterSpacing;
1043
1170
  textStartX = currentX;
1044
1171
  } else {
1045
1172
  if (!currentText) textStartX = currentX;
@@ -1048,11 +1175,16 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1048
1175
  }
1049
1176
  if (currentText) {
1050
1177
  const textWidth = measureText2(currentText);
1178
+ const graphemeCount = [
1179
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1180
+ currentText
1181
+ )
1182
+ ].length;
1051
1183
  runs.push({
1052
1184
  kind: "text",
1053
1185
  text: currentText,
1054
1186
  x: textStartX,
1055
- width: textWidth
1187
+ width: textWidth + letterSpacing * graphemeCount
1056
1188
  });
1057
1189
  }
1058
1190
  return runs;
@@ -1103,20 +1235,29 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
1103
1235
  }
1104
1236
  }
1105
1237
  async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
1238
+ const letterSpacing = seg.letterSpacing ?? 0;
1106
1239
  const runs = splitTextIntoRuns(
1107
1240
  seg.text,
1108
1241
  (text) => {
1109
1242
  setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
1110
1243
  return ctx.measureText(text).width;
1111
1244
  },
1112
- seg.fontSize
1245
+ seg.fontSize,
1246
+ letterSpacing
1113
1247
  );
1114
1248
  for (const run of runs) {
1115
1249
  if (run.kind === "text") {
1116
- if (textShadow) {
1117
- drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1250
+ if (letterSpacing !== 0) {
1251
+ if (textShadow) {
1252
+ drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1253
+ }
1254
+ drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
1255
+ } else {
1256
+ if (textShadow) {
1257
+ drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1258
+ }
1259
+ ctx.fillText(run.text, x + run.x, y);
1118
1260
  }
1119
- ctx.fillText(run.text, x + run.x, y);
1120
1261
  } else {
1121
1262
  const img = await loadEmojiImage(emojiStyle, run.char);
1122
1263
  if (img) {
@@ -1291,84 +1432,91 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1291
1432
  }
1292
1433
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1293
1434
  if (isClipped) {
1294
- const borderRadius = getBorderRadiusFromStyle(style);
1435
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1295
1436
  applyClip(ctx, x, y, width, height, borderRadius);
1296
1437
  }
1297
1438
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1298
1439
  drawRect(ctx, x, y, width, height, style);
1299
1440
  }
1300
1441
  if (style.backgroundImage) {
1301
- const gradient = createGradientFromCSS(
1302
- ctx,
1303
- style.backgroundImage,
1304
- x,
1305
- y,
1306
- width,
1307
- height
1308
- );
1309
- if (gradient) {
1310
- ctx.fillStyle = gradient;
1311
- const borderRadius = getBorderRadiusFromStyle(style);
1312
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1313
- ctx.beginPath();
1314
- roundedRect(
1315
- ctx,
1316
- x,
1317
- y,
1318
- width,
1319
- height,
1320
- borderRadius.topLeft,
1321
- borderRadius.topRight,
1322
- borderRadius.bottomRight,
1323
- borderRadius.bottomLeft
1324
- );
1325
- ctx.fill();
1326
- } else {
1327
- ctx.fillRect(x, y, width, height);
1328
- }
1329
- } else {
1330
- const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
1331
- if (urlMatch) {
1332
- const borderRadius = getBorderRadiusFromStyle(style);
1333
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1334
- if (hasRadius2) {
1335
- applyClip(ctx, x, y, width, height, borderRadius);
1336
- }
1337
- const image = await loadImage3(urlMatch[1]);
1338
- const bgSize = style.backgroundSize;
1339
- if (bgSize === "cover") {
1340
- const r = computeCover(
1341
- image.width,
1342
- image.height,
1442
+ const layers = splitGradientArgs(style.backgroundImage);
1443
+ for (let i = layers.length - 1; i >= 0; i--) {
1444
+ const layer = layers[i].trim();
1445
+ const gradient = createGradientFromCSS(ctx, layer, x, y, width, height);
1446
+ if (gradient) {
1447
+ ctx.fillStyle = gradient;
1448
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1449
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1450
+ ctx.beginPath();
1451
+ roundedRect(
1452
+ ctx,
1343
1453
  x,
1344
1454
  y,
1345
1455
  width,
1346
- height
1456
+ height,
1457
+ borderRadius.topLeft,
1458
+ borderRadius.topRight,
1459
+ borderRadius.bottomRight,
1460
+ borderRadius.bottomLeft
1347
1461
  );
1348
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1462
+ ctx.fill();
1349
1463
  } else {
1350
- let tileW, tileH;
1351
- if (bgSize === "contain") {
1352
- const r = computeContain(
1464
+ ctx.fillRect(x, y, width, height);
1465
+ }
1466
+ } else {
1467
+ const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
1468
+ if (urlMatch) {
1469
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1470
+ const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1471
+ if (hasRadius2) {
1472
+ applyClip(ctx, x, y, width, height, borderRadius);
1473
+ }
1474
+ const image = await loadImage3(urlMatch[1]);
1475
+ const bgSize = style.backgroundSize;
1476
+ if (bgSize === "cover") {
1477
+ const r = computeCover(
1353
1478
  image.width,
1354
1479
  image.height,
1355
- 0,
1356
- 0,
1480
+ x,
1481
+ y,
1357
1482
  width,
1358
1483
  height
1359
1484
  );
1360
- tileW = r.dw;
1361
- tileH = r.dh;
1362
- } else if (bgSize === "100% 100%") {
1363
- tileW = width;
1364
- tileH = height;
1485
+ ctx.drawImage(
1486
+ image,
1487
+ r.sx,
1488
+ r.sy,
1489
+ r.sw,
1490
+ r.sh,
1491
+ r.dx,
1492
+ r.dy,
1493
+ r.dw,
1494
+ r.dh
1495
+ );
1365
1496
  } else {
1366
- tileW = image.width;
1367
- tileH = image.height;
1368
- }
1369
- for (let ty = y; ty < y + height; ty += tileH) {
1370
- for (let tx = x; tx < x + width; tx += tileW) {
1371
- ctx.drawImage(image, tx, ty, tileW, tileH);
1497
+ let tileW, tileH;
1498
+ if (bgSize === "contain") {
1499
+ const r = computeContain(
1500
+ image.width,
1501
+ image.height,
1502
+ 0,
1503
+ 0,
1504
+ width,
1505
+ height
1506
+ );
1507
+ tileW = r.dw;
1508
+ tileH = r.dh;
1509
+ } else if (bgSize === "100% 100%") {
1510
+ tileW = width;
1511
+ tileH = height;
1512
+ } else {
1513
+ tileW = image.width;
1514
+ tileH = image.height;
1515
+ }
1516
+ for (let ty = y; ty < y + height; ty += tileH) {
1517
+ for (let tx = x; tx < x + width; tx += tileW) {
1518
+ ctx.drawImage(image, tx, ty, tileW, tileH);
1519
+ }
1372
1520
  }
1373
1521
  }
1374
1522
  }
@@ -1381,12 +1529,12 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1381
1529
  ctx.strokeRect(x, y, width, height);
1382
1530
  }
1383
1531
  if (node.textContent !== void 0 && node.textContent !== "") {
1384
- const paddingTop = toNumber2(style.paddingTop);
1385
- const paddingLeft = toNumber2(style.paddingLeft);
1386
- const paddingRight = toNumber2(style.paddingRight);
1387
- const borderTopW = toNumber2(style.borderTopWidth);
1388
- const borderLeftW = toNumber2(style.borderLeftWidth);
1389
- 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);
1390
1538
  const contentX = x + paddingLeft + borderLeftW;
1391
1539
  const contentY = y + paddingTop + borderTopW;
1392
1540
  const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
@@ -1407,16 +1555,16 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1407
1555
  );
1408
1556
  }
1409
1557
  if (node.type === "img" && node.props.src) {
1410
- const paddingTop = toNumber2(style.paddingTop);
1411
- const paddingLeft = toNumber2(style.paddingLeft);
1412
- const paddingRight = toNumber2(style.paddingRight);
1413
- 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);
1414
1562
  const imgX = x + paddingLeft;
1415
1563
  const imgY = y + paddingTop;
1416
1564
  const imgW = width - paddingLeft - paddingRight;
1417
1565
  const imgH = height - paddingTop - paddingBottom;
1418
1566
  if (!isClipped) {
1419
- const borderRadius = getBorderRadiusFromStyle(style);
1567
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1420
1568
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1421
1569
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1422
1570
  }
@@ -1479,84 +1627,91 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1479
1627
  }
1480
1628
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1481
1629
  if (isClipped) {
1482
- const borderRadius = getBorderRadiusFromStyle(style);
1630
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1483
1631
  applyClip(ctx, x, y, width, height, borderRadius);
1484
1632
  }
1485
1633
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1486
1634
  drawRect(ctx, x, y, width, height, style);
1487
1635
  }
1488
1636
  if (style.backgroundImage) {
1489
- const gradient = createGradientFromCSS(
1490
- ctx,
1491
- style.backgroundImage,
1492
- x,
1493
- y,
1494
- width,
1495
- height
1496
- );
1497
- if (gradient) {
1498
- ctx.fillStyle = gradient;
1499
- const borderRadius = getBorderRadiusFromStyle(style);
1500
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1501
- ctx.beginPath();
1502
- roundedRect(
1503
- ctx,
1504
- x,
1505
- y,
1506
- width,
1507
- height,
1508
- borderRadius.topLeft,
1509
- borderRadius.topRight,
1510
- borderRadius.bottomRight,
1511
- borderRadius.bottomLeft
1512
- );
1513
- ctx.fill();
1514
- } else {
1515
- ctx.fillRect(x, y, width, height);
1516
- }
1517
- } else {
1518
- const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
1519
- if (urlMatch) {
1520
- const borderRadius = getBorderRadiusFromStyle(style);
1521
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1522
- if (hasRadius2) {
1523
- applyClip(ctx, x, y, width, height, borderRadius);
1524
- }
1525
- const image = await loadImage3(urlMatch[1]);
1526
- const bgSize = style.backgroundSize;
1527
- if (bgSize === "cover") {
1528
- const r = computeCover(
1529
- image.width,
1530
- image.height,
1637
+ const layers = splitGradientArgs(style.backgroundImage);
1638
+ for (let i = layers.length - 1; i >= 0; i--) {
1639
+ const layer = layers[i].trim();
1640
+ const gradient = createGradientFromCSS(ctx, layer, x, y, width, height);
1641
+ if (gradient) {
1642
+ ctx.fillStyle = gradient;
1643
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1644
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1645
+ ctx.beginPath();
1646
+ roundedRect(
1647
+ ctx,
1531
1648
  x,
1532
1649
  y,
1533
1650
  width,
1534
- height
1651
+ height,
1652
+ borderRadius.topLeft,
1653
+ borderRadius.topRight,
1654
+ borderRadius.bottomRight,
1655
+ borderRadius.bottomLeft
1535
1656
  );
1536
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1657
+ ctx.fill();
1537
1658
  } else {
1538
- let tileW, tileH;
1539
- if (bgSize === "contain") {
1540
- const r = computeContain(
1659
+ ctx.fillRect(x, y, width, height);
1660
+ }
1661
+ } else {
1662
+ const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
1663
+ if (urlMatch) {
1664
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1665
+ const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1666
+ if (hasRadius2) {
1667
+ applyClip(ctx, x, y, width, height, borderRadius);
1668
+ }
1669
+ const image = await loadImage3(urlMatch[1]);
1670
+ const bgSize = style.backgroundSize;
1671
+ if (bgSize === "cover") {
1672
+ const r = computeCover(
1541
1673
  image.width,
1542
1674
  image.height,
1543
- 0,
1544
- 0,
1675
+ x,
1676
+ y,
1545
1677
  width,
1546
1678
  height
1547
1679
  );
1548
- tileW = r.dw;
1549
- tileH = r.dh;
1550
- } else if (bgSize === "100% 100%") {
1551
- tileW = width;
1552
- tileH = height;
1680
+ ctx.drawImage(
1681
+ image,
1682
+ r.sx,
1683
+ r.sy,
1684
+ r.sw,
1685
+ r.sh,
1686
+ r.dx,
1687
+ r.dy,
1688
+ r.dw,
1689
+ r.dh
1690
+ );
1553
1691
  } else {
1554
- tileW = image.width;
1555
- tileH = image.height;
1556
- }
1557
- for (let ty = y; ty < y + height; ty += tileH) {
1558
- for (let tx = x; tx < x + width; tx += tileW) {
1559
- ctx.drawImage(image, tx, ty, tileW, tileH);
1692
+ let tileW, tileH;
1693
+ if (bgSize === "contain") {
1694
+ const r = computeContain(
1695
+ image.width,
1696
+ image.height,
1697
+ 0,
1698
+ 0,
1699
+ width,
1700
+ height
1701
+ );
1702
+ tileW = r.dw;
1703
+ tileH = r.dh;
1704
+ } else if (bgSize === "100% 100%") {
1705
+ tileW = width;
1706
+ tileH = height;
1707
+ } else {
1708
+ tileW = image.width;
1709
+ tileH = image.height;
1710
+ }
1711
+ for (let ty = y; ty < y + height; ty += tileH) {
1712
+ for (let tx = x; tx < x + width; tx += tileW) {
1713
+ ctx.drawImage(image, tx, ty, tileW, tileH);
1714
+ }
1560
1715
  }
1561
1716
  }
1562
1717
  }
@@ -1569,12 +1724,12 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1569
1724
  ctx.strokeRect(x, y, width, height);
1570
1725
  }
1571
1726
  if (node.textContent !== void 0 && node.textContent !== "") {
1572
- const paddingTop = toNumber2(style.paddingTop);
1573
- const paddingLeft = toNumber2(style.paddingLeft);
1574
- const paddingRight = toNumber2(style.paddingRight);
1575
- const borderTopW = toNumber2(style.borderTopWidth);
1576
- const borderLeftW = toNumber2(style.borderLeftWidth);
1577
- 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);
1578
1733
  const contentX = x + paddingLeft + borderLeftW;
1579
1734
  const contentY = y + paddingTop + borderTopW;
1580
1735
  const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
@@ -1595,16 +1750,16 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1595
1750
  );
1596
1751
  }
1597
1752
  if (node.type === "img" && node.props.src) {
1598
- const paddingTop = toNumber2(style.paddingTop);
1599
- const paddingLeft = toNumber2(style.paddingLeft);
1600
- const paddingRight = toNumber2(style.paddingRight);
1601
- 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);
1602
1757
  const imgX = x + paddingLeft;
1603
1758
  const imgY = y + paddingTop;
1604
1759
  const imgW = width - paddingLeft - paddingRight;
1605
1760
  const imgH = height - paddingTop - paddingBottom;
1606
1761
  if (!isClipped) {
1607
- const borderRadius = getBorderRadiusFromStyle(style);
1762
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1608
1763
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1609
1764
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1610
1765
  }
@@ -1629,6 +1784,10 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1629
1784
  }
1630
1785
  ctx.restore();
1631
1786
  }
1787
+ function parseCSSLength(value, referenceSize) {
1788
+ if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
1789
+ return parseFloat(value);
1790
+ }
1632
1791
  function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
1633
1792
  let ox = x + width / 2;
1634
1793
  let oy = y + height / 2;
@@ -1645,8 +1804,11 @@ function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
1645
1804
  case "translate":
1646
1805
  case "translateX":
1647
1806
  case "translateY": {
1648
- const tx = name === "translateY" ? 0 : parseFloat(values[0]);
1649
- 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
+ );
1650
1812
  ctx.translate(tx, ty);
1651
1813
  break;
1652
1814
  }
@@ -1682,8 +1844,7 @@ function resolveOrigin(value, base, size) {
1682
1844
  if (value === "left" || value === "top") return base;
1683
1845
  if (value === "right" || value === "bottom") return base + size;
1684
1846
  if (value === "center") return base + size / 2;
1685
- if (value.endsWith("%")) return base + parseFloat(value) / 100 * size;
1686
- return base + parseFloat(value);
1847
+ return base + parseCSSLength(value, size);
1687
1848
  }
1688
1849
  function parseAngle(value) {
1689
1850
  if (value.endsWith("deg")) return parseFloat(value) * Math.PI / 180;
@@ -1691,7 +1852,7 @@ function parseAngle(value) {
1691
1852
  if (value.endsWith("turn")) return parseFloat(value) * 2 * Math.PI;
1692
1853
  return parseFloat(value);
1693
1854
  }
1694
- function toNumber2(v) {
1855
+ function toNumber(v) {
1695
1856
  if (typeof v === "number") return v;
1696
1857
  if (v === void 0 || v === null) return 0;
1697
1858
  const n = parseFloat(String(v));
@@ -1742,11 +1903,11 @@ function parseValue(v) {
1742
1903
  if (v === void 0 || v === null) return void 0;
1743
1904
  const s = String(v);
1744
1905
  if (s === "auto") return "auto";
1745
- const n = parseFloat(s);
1746
- if (!isNaN(n)) return n;
1906
+ const n = Number(s);
1907
+ if (s !== "" && !isNaN(n)) return n;
1747
1908
  return s;
1748
1909
  }
1749
- function expandStyle(raw) {
1910
+ function expandStyle(raw, fontFamilies) {
1750
1911
  const style = { ...raw };
1751
1912
  if (style.margin !== void 0) {
1752
1913
  const sides = parseSides(String(style.margin));
@@ -1859,7 +2020,14 @@ function expandStyle(raw) {
1859
2020
  if (style.overflowY === void 0) style.overflowY = style.overflow;
1860
2021
  }
1861
2022
  if (typeof style.fontFamily === "string") {
1862
- 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(", ");
1863
2031
  }
1864
2032
  return style;
1865
2033
  }
@@ -1952,7 +2120,15 @@ var DIMENSION_PROPS = [
1952
2120
  "paddingLeft",
1953
2121
  "rowGap",
1954
2122
  "columnGap",
1955
- "flexBasis"
2123
+ "flexBasis",
2124
+ "borderTopWidth",
2125
+ "borderRightWidth",
2126
+ "borderBottomWidth",
2127
+ "borderLeftWidth",
2128
+ "borderTopLeftRadius",
2129
+ "borderTopRightRadius",
2130
+ "borderBottomRightRadius",
2131
+ "borderBottomLeftRadius"
1956
2132
  ];
1957
2133
  function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
1958
2134
  if (value.endsWith("%") || value === "auto") return value;
@@ -2022,8 +2198,41 @@ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = DEFAU
2022
2198
  style[prop] = resolved;
2023
2199
  }
2024
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
+ }
2025
2219
  return style;
2026
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
+ }
2027
2236
 
2028
2237
  // src/jsx/yoga.ts
2029
2238
  import Yoga, {
@@ -2212,16 +2421,18 @@ function applyEdgeValue(node, setter, edge, value) {
2212
2421
  }
2213
2422
 
2214
2423
  // src/jsx/layout.ts
2215
- async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled) {
2424
+ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
2216
2425
  const rootYogaNode = createYogaNode();
2426
+ const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
2217
2427
  const rootNode = await buildNode(
2218
2428
  element,
2219
- DEFAULT_STYLE,
2429
+ rootStyle,
2220
2430
  rootYogaNode,
2221
2431
  containerWidth,
2222
2432
  containerHeight,
2223
2433
  ctx,
2224
- emojiEnabled
2434
+ emojiEnabled,
2435
+ fontFamilies
2225
2436
  );
2226
2437
  rootYogaNode.setWidth(containerWidth);
2227
2438
  rootYogaNode.setHeight(containerHeight);
@@ -2230,7 +2441,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
2230
2441
  freeYogaNode(rootYogaNode);
2231
2442
  return layoutTree;
2232
2443
  }
2233
- async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled) {
2444
+ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
2234
2445
  if (element === null || element === void 0 || typeof element === "boolean") {
2235
2446
  return {
2236
2447
  type: "empty",
@@ -2271,7 +2482,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2271
2482
  viewportWidth,
2272
2483
  viewportHeight,
2273
2484
  ctx,
2274
- emojiEnabled
2485
+ emojiEnabled,
2486
+ fontFamilies
2275
2487
  )
2276
2488
  );
2277
2489
  }
@@ -2296,12 +2508,13 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2296
2508
  viewportWidth,
2297
2509
  viewportHeight,
2298
2510
  ctx,
2299
- emojiEnabled
2511
+ emojiEnabled,
2512
+ fontFamilies
2300
2513
  );
2301
2514
  }
2302
2515
  const props = el.props ?? {};
2303
2516
  const rawStyle = props.style ?? {};
2304
- const expanded = expandStyle(rawStyle);
2517
+ const expanded = expandStyle(rawStyle, fontFamilies);
2305
2518
  const style = resolveStyle(expanded, parentStyle);
2306
2519
  resolveUnits(style, viewportWidth, viewportHeight);
2307
2520
  const tagName = String(type);
@@ -2419,7 +2632,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2419
2632
  viewportWidth,
2420
2633
  viewportHeight,
2421
2634
  ctx,
2422
- emojiEnabled
2635
+ emojiEnabled,
2636
+ fontFamilies
2423
2637
  )
2424
2638
  );
2425
2639
  }
@@ -2487,12 +2701,14 @@ async function renderReactElement(ctx, element, options) {
2487
2701
  const width = ctx.canvas.width;
2488
2702
  const height = ctx.canvas.height;
2489
2703
  const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
2704
+ const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
2490
2705
  const layoutTree = await buildLayoutTree(
2491
2706
  element,
2492
2707
  width,
2493
2708
  height,
2494
2709
  ctx,
2495
- !!emojiStyle
2710
+ !!emojiStyle,
2711
+ fontFamilies
2496
2712
  );
2497
2713
  await drawNode(ctx, layoutTree, 0, 0, options.debug ?? false, emojiStyle);
2498
2714
  }