@effing/canvas 0.18.3 → 0.18.5

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
@@ -650,7 +650,7 @@ async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
650
650
 
651
651
  // src/jsx/draw/rect.ts
652
652
  function drawRect(ctx, x, y, width, height, style) {
653
- const borderRadius = getBorderRadius(style);
653
+ const borderRadius = getBorderRadius(style, width, height);
654
654
  const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
655
655
  if (style.boxShadow) {
656
656
  drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
@@ -677,16 +677,23 @@ function drawRect(ctx, x, y, width, height, style) {
677
677
  }
678
678
  drawBorders(ctx, x, y, width, height, style, borderRadius);
679
679
  }
680
- function getBorderRadius(style) {
680
+ 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
+ }
685
+ return toNumber(v);
686
+ }
687
+ function getBorderRadius(style, width, height) {
681
688
  return {
682
- topLeft: toNumber(style.borderTopLeftRadius),
683
- topRight: toNumber(style.borderTopRightRadius),
684
- bottomRight: toNumber(style.borderBottomRightRadius),
685
- bottomLeft: toNumber(style.borderBottomLeftRadius)
689
+ topLeft: resolveRadius(style.borderTopLeftRadius, width, height),
690
+ topRight: resolveRadius(style.borderTopRightRadius, width, height),
691
+ bottomRight: resolveRadius(style.borderBottomRightRadius, width, height),
692
+ bottomLeft: resolveRadius(style.borderBottomLeftRadius, width, height)
686
693
  };
687
694
  }
688
- function getBorderRadiusFromStyle(style) {
689
- return getBorderRadius(style);
695
+ function getBorderRadiusFromStyle(style, width, height) {
696
+ return getBorderRadius(style, width, height);
690
697
  }
691
698
  function drawBorders(ctx, x, y, width, height, style, borderRadius) {
692
699
  const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
@@ -795,6 +802,10 @@ function toNumber(v) {
795
802
 
796
803
  // src/jsx/draw/svg.ts
797
804
  import { Path2D } from "@napi-rs/canvas";
805
+ function resolveCurrentColor(value, color) {
806
+ if (value?.toLowerCase() === "currentcolor") return color;
807
+ return value;
808
+ }
798
809
  function mergeStyleIntoProps(props) {
799
810
  const style = props.style;
800
811
  if (!style) return props;
@@ -814,65 +825,66 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
814
825
  ctx.translate(-vbX, -vbY);
815
826
  }
816
827
  }
828
+ const color = node.style.color ?? "black";
817
829
  const merged = mergeStyleIntoProps(node.props);
818
- const inheritedFill = merged.fill ?? "black";
830
+ const inheritedFill = resolveCurrentColor(merged.fill, color) ?? "black";
819
831
  const children = node.props.children;
820
832
  if (children != null) {
821
833
  const childArray = Array.isArray(children) ? children : [children];
822
834
  for (const child of childArray) {
823
835
  if (child != null && typeof child === "object") {
824
- drawSvgChild(ctx, child, inheritedFill);
836
+ drawSvgChild(ctx, child, inheritedFill, color);
825
837
  }
826
838
  }
827
839
  }
828
840
  ctx.restore();
829
841
  }
830
- function drawSvgChild(ctx, child, inheritedFill) {
842
+ function drawSvgChild(ctx, child, inheritedFill, color) {
831
843
  const { type } = child;
832
844
  const props = mergeStyleIntoProps(child.props);
833
845
  switch (type) {
834
846
  case "path":
835
- drawPath(ctx, props, inheritedFill);
847
+ drawPath(ctx, props, inheritedFill, color);
836
848
  break;
837
849
  case "circle":
838
- drawCircle(ctx, props, inheritedFill);
850
+ drawCircle(ctx, props, inheritedFill, color);
839
851
  break;
840
852
  case "rect":
841
- drawSvgRect(ctx, props, inheritedFill);
853
+ drawSvgRect(ctx, props, inheritedFill, color);
842
854
  break;
843
855
  case "line":
844
- drawLine(ctx, props);
856
+ drawLine(ctx, props, color);
845
857
  break;
846
858
  case "ellipse":
847
- drawEllipse(ctx, props, inheritedFill);
859
+ drawEllipse(ctx, props, inheritedFill, color);
848
860
  break;
849
861
  case "polygon":
850
- drawPolygon(ctx, props, inheritedFill);
862
+ drawPolygon(ctx, props, inheritedFill, color);
851
863
  break;
852
864
  case "polyline":
853
- drawPolyline(ctx, props, inheritedFill);
865
+ drawPolyline(ctx, props, inheritedFill, color);
854
866
  break;
855
867
  case "g":
856
- drawGroup(ctx, child, inheritedFill);
868
+ drawGroup(ctx, child, inheritedFill, color);
857
869
  break;
858
870
  }
859
871
  }
860
- function drawPath(ctx, props, inheritedFill) {
872
+ function drawPath(ctx, props, inheritedFill, color) {
861
873
  const d = props.d;
862
874
  if (!d) return;
863
875
  const path = new Path2D(d);
864
- applyFillAndStroke(ctx, props, path, inheritedFill);
876
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
865
877
  }
866
- function drawCircle(ctx, props, inheritedFill) {
878
+ function drawCircle(ctx, props, inheritedFill, color) {
867
879
  const cx = Number(props.cx ?? 0);
868
880
  const cy = Number(props.cy ?? 0);
869
881
  const r = Number(props.r ?? 0);
870
882
  if (r <= 0) return;
871
883
  const path = new Path2D();
872
884
  path.arc(cx, cy, r, 0, Math.PI * 2);
873
- applyFillAndStroke(ctx, props, path, inheritedFill);
885
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
874
886
  }
875
- function drawSvgRect(ctx, props, inheritedFill) {
887
+ function drawSvgRect(ctx, props, inheritedFill, color) {
876
888
  const rx = Number(props.x ?? 0);
877
889
  const ry = Number(props.y ?? 0);
878
890
  const w = Number(props.width ?? 0);
@@ -880,9 +892,9 @@ function drawSvgRect(ctx, props, inheritedFill) {
880
892
  if (w <= 0 || h <= 0) return;
881
893
  const path = new Path2D();
882
894
  path.rect(rx, ry, w, h);
883
- applyFillAndStroke(ctx, props, path, inheritedFill);
895
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
884
896
  }
885
- function drawLine(ctx, props) {
897
+ function drawLine(ctx, props, color) {
886
898
  const x1 = Number(props.x1 ?? 0);
887
899
  const y1 = Number(props.y1 ?? 0);
888
900
  const x2 = Number(props.x2 ?? 0);
@@ -890,9 +902,9 @@ function drawLine(ctx, props) {
890
902
  const path = new Path2D();
891
903
  path.moveTo(x1, y1);
892
904
  path.lineTo(x2, y2);
893
- applyStroke(ctx, props, path);
905
+ applyStroke(ctx, props, path, color);
894
906
  }
895
- function drawEllipse(ctx, props, inheritedFill) {
907
+ function drawEllipse(ctx, props, inheritedFill, color) {
896
908
  const cx = Number(props.cx ?? 0);
897
909
  const cy = Number(props.cy ?? 0);
898
910
  const rx = Number(props.rx ?? 0);
@@ -900,9 +912,9 @@ function drawEllipse(ctx, props, inheritedFill) {
900
912
  if (rx <= 0 || ry <= 0) return;
901
913
  const path = new Path2D();
902
914
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
903
- applyFillAndStroke(ctx, props, path, inheritedFill);
915
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
904
916
  }
905
- function drawPolygon(ctx, props, inheritedFill) {
917
+ function drawPolygon(ctx, props, inheritedFill, color) {
906
918
  const points = parsePoints(props.points);
907
919
  if (points.length < 2) return;
908
920
  const path = new Path2D();
@@ -911,9 +923,9 @@ function drawPolygon(ctx, props, inheritedFill) {
911
923
  path.lineTo(points[i][0], points[i][1]);
912
924
  }
913
925
  path.closePath();
914
- applyFillAndStroke(ctx, props, path, inheritedFill);
926
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
915
927
  }
916
- function drawPolyline(ctx, props, inheritedFill) {
928
+ function drawPolyline(ctx, props, inheritedFill, color) {
917
929
  const points = parsePoints(props.points);
918
930
  if (points.length < 2) return;
919
931
  const path = new Path2D();
@@ -921,17 +933,17 @@ function drawPolyline(ctx, props, inheritedFill) {
921
933
  for (let i = 1; i < points.length; i++) {
922
934
  path.lineTo(points[i][0], points[i][1]);
923
935
  }
924
- applyFillAndStroke(ctx, props, path, inheritedFill);
936
+ applyFillAndStroke(ctx, props, path, inheritedFill, color);
925
937
  }
926
- function drawGroup(ctx, node, inheritedFill) {
938
+ function drawGroup(ctx, node, inheritedFill, color) {
927
939
  const children = node.children ?? node.props.children;
928
940
  if (children == null) return;
929
941
  const merged = mergeStyleIntoProps(node.props);
930
- const groupFill = merged.fill ?? inheritedFill;
942
+ const groupFill = resolveCurrentColor(merged.fill, color) ?? inheritedFill;
931
943
  const childArray = Array.isArray(children) ? children : [children];
932
944
  for (const child of childArray) {
933
945
  if (child != null && typeof child === "object") {
934
- drawSvgChild(ctx, child, groupFill);
946
+ drawSvgChild(ctx, child, groupFill, color);
935
947
  }
936
948
  }
937
949
  }
@@ -944,16 +956,21 @@ function parsePoints(value) {
944
956
  }
945
957
  return result;
946
958
  }
947
- function applyFillAndStroke(ctx, props, path, inheritedFill) {
948
- const fill = props.fill ?? inheritedFill;
959
+ function applyFillAndStroke(ctx, props, path, inheritedFill, color) {
960
+ const fill = resolveCurrentColor(props.fill, color) ?? inheritedFill;
961
+ const fillRule = props.fillRule ?? props["fill-rule"];
962
+ const clipRule = props.clipRule ?? props["clip-rule"];
963
+ if (clipRule) {
964
+ ctx.clip(path, clipRule);
965
+ }
949
966
  if (fill !== "none") {
950
967
  ctx.fillStyle = fill;
951
- ctx.fill(path);
968
+ ctx.fill(path, fillRule ?? "nonzero");
952
969
  }
953
- applyStroke(ctx, props, path);
970
+ applyStroke(ctx, props, path, color);
954
971
  }
955
- function applyStroke(ctx, props, path) {
956
- const stroke = props.stroke;
972
+ function applyStroke(ctx, props, path, color) {
973
+ const stroke = resolveCurrentColor(props.stroke, color);
957
974
  if (!stroke || stroke === "none") return;
958
975
  ctx.strokeStyle = stroke;
959
976
  ctx.lineWidth = Number(props.strokeWidth ?? props["stroke-width"] ?? 1);
@@ -1009,7 +1026,7 @@ async function loadEmoji(type, code) {
1009
1026
  }
1010
1027
 
1011
1028
  // src/jsx/text/emoji-split.ts
1012
- function splitTextIntoRuns(text, measureText2, emojiSize) {
1029
+ function splitTextIntoRuns(text, measureText2, emojiSize, letterSpacing = 0) {
1013
1030
  const runs = [];
1014
1031
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1015
1032
  let currentText = "";
@@ -1019,13 +1036,18 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1019
1036
  if (isEmojiGrapheme(segment)) {
1020
1037
  if (currentText) {
1021
1038
  const textWidth = measureText2(currentText);
1039
+ const graphemeCount = [
1040
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1041
+ currentText
1042
+ )
1043
+ ].length;
1022
1044
  runs.push({
1023
1045
  kind: "text",
1024
1046
  text: currentText,
1025
1047
  x: textStartX,
1026
- width: textWidth
1048
+ width: textWidth + letterSpacing * graphemeCount
1027
1049
  });
1028
- currentX = textStartX + textWidth;
1050
+ currentX = textStartX + textWidth + letterSpacing * graphemeCount;
1029
1051
  currentText = "";
1030
1052
  }
1031
1053
  runs.push({
@@ -1034,7 +1056,7 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1034
1056
  x: currentX,
1035
1057
  width: emojiSize
1036
1058
  });
1037
- currentX += emojiSize;
1059
+ currentX += emojiSize + letterSpacing;
1038
1060
  textStartX = currentX;
1039
1061
  } else {
1040
1062
  if (!currentText) textStartX = currentX;
@@ -1043,11 +1065,16 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1043
1065
  }
1044
1066
  if (currentText) {
1045
1067
  const textWidth = measureText2(currentText);
1068
+ const graphemeCount = [
1069
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1070
+ currentText
1071
+ )
1072
+ ].length;
1046
1073
  runs.push({
1047
1074
  kind: "text",
1048
1075
  text: currentText,
1049
1076
  x: textStartX,
1050
- width: textWidth
1077
+ width: textWidth + letterSpacing * graphemeCount
1051
1078
  });
1052
1079
  }
1053
1080
  return runs;
@@ -1098,20 +1125,29 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
1098
1125
  }
1099
1126
  }
1100
1127
  async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
1128
+ const letterSpacing = seg.letterSpacing ?? 0;
1101
1129
  const runs = splitTextIntoRuns(
1102
1130
  seg.text,
1103
1131
  (text) => {
1104
1132
  setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
1105
1133
  return ctx.measureText(text).width;
1106
1134
  },
1107
- seg.fontSize
1135
+ seg.fontSize,
1136
+ letterSpacing
1108
1137
  );
1109
1138
  for (const run of runs) {
1110
1139
  if (run.kind === "text") {
1111
- if (textShadow) {
1112
- drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1140
+ if (letterSpacing !== 0) {
1141
+ if (textShadow) {
1142
+ drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1143
+ }
1144
+ drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
1145
+ } else {
1146
+ if (textShadow) {
1147
+ drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1148
+ }
1149
+ ctx.fillText(run.text, x + run.x, y);
1113
1150
  }
1114
- ctx.fillText(run.text, x + run.x, y);
1115
1151
  } else {
1116
1152
  const img = await loadEmojiImage(emojiStyle, run.char);
1117
1153
  if (img) {
@@ -1286,84 +1322,91 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1286
1322
  }
1287
1323
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1288
1324
  if (isClipped) {
1289
- const borderRadius = getBorderRadiusFromStyle(style);
1325
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1290
1326
  applyClip(ctx, x, y, width, height, borderRadius);
1291
1327
  }
1292
1328
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1293
1329
  drawRect(ctx, x, y, width, height, style);
1294
1330
  }
1295
1331
  if (style.backgroundImage) {
1296
- const gradient = createGradientFromCSS(
1297
- ctx,
1298
- style.backgroundImage,
1299
- x,
1300
- y,
1301
- width,
1302
- height
1303
- );
1304
- if (gradient) {
1305
- ctx.fillStyle = gradient;
1306
- const borderRadius = getBorderRadiusFromStyle(style);
1307
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1308
- ctx.beginPath();
1309
- roundedRect(
1310
- ctx,
1311
- x,
1312
- y,
1313
- width,
1314
- height,
1315
- borderRadius.topLeft,
1316
- borderRadius.topRight,
1317
- borderRadius.bottomRight,
1318
- borderRadius.bottomLeft
1319
- );
1320
- ctx.fill();
1321
- } else {
1322
- ctx.fillRect(x, y, width, height);
1323
- }
1324
- } else {
1325
- const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
1326
- if (urlMatch) {
1327
- const borderRadius = getBorderRadiusFromStyle(style);
1328
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1329
- if (hasRadius2) {
1330
- applyClip(ctx, x, y, width, height, borderRadius);
1331
- }
1332
- const image = await loadImage3(urlMatch[1]);
1333
- const bgSize = style.backgroundSize;
1334
- if (bgSize === "cover") {
1335
- const r = computeCover(
1336
- image.width,
1337
- image.height,
1332
+ const layers = splitGradientArgs(style.backgroundImage);
1333
+ for (let i = layers.length - 1; i >= 0; i--) {
1334
+ const layer = layers[i].trim();
1335
+ const gradient = createGradientFromCSS(ctx, layer, x, y, width, height);
1336
+ if (gradient) {
1337
+ ctx.fillStyle = gradient;
1338
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1339
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1340
+ ctx.beginPath();
1341
+ roundedRect(
1342
+ ctx,
1338
1343
  x,
1339
1344
  y,
1340
1345
  width,
1341
- height
1346
+ height,
1347
+ borderRadius.topLeft,
1348
+ borderRadius.topRight,
1349
+ borderRadius.bottomRight,
1350
+ borderRadius.bottomLeft
1342
1351
  );
1343
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1352
+ ctx.fill();
1344
1353
  } else {
1345
- let tileW, tileH;
1346
- if (bgSize === "contain") {
1347
- const r = computeContain(
1354
+ ctx.fillRect(x, y, width, height);
1355
+ }
1356
+ } else {
1357
+ const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
1358
+ if (urlMatch) {
1359
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1360
+ const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1361
+ if (hasRadius2) {
1362
+ applyClip(ctx, x, y, width, height, borderRadius);
1363
+ }
1364
+ const image = await loadImage3(urlMatch[1]);
1365
+ const bgSize = style.backgroundSize;
1366
+ if (bgSize === "cover") {
1367
+ const r = computeCover(
1348
1368
  image.width,
1349
1369
  image.height,
1350
- 0,
1351
- 0,
1370
+ x,
1371
+ y,
1352
1372
  width,
1353
1373
  height
1354
1374
  );
1355
- tileW = r.dw;
1356
- tileH = r.dh;
1357
- } else if (bgSize === "100% 100%") {
1358
- tileW = width;
1359
- tileH = height;
1375
+ ctx.drawImage(
1376
+ image,
1377
+ r.sx,
1378
+ r.sy,
1379
+ r.sw,
1380
+ r.sh,
1381
+ r.dx,
1382
+ r.dy,
1383
+ r.dw,
1384
+ r.dh
1385
+ );
1360
1386
  } else {
1361
- tileW = image.width;
1362
- tileH = image.height;
1363
- }
1364
- for (let ty = y; ty < y + height; ty += tileH) {
1365
- for (let tx = x; tx < x + width; tx += tileW) {
1366
- ctx.drawImage(image, tx, ty, tileW, tileH);
1387
+ let tileW, tileH;
1388
+ if (bgSize === "contain") {
1389
+ const r = computeContain(
1390
+ image.width,
1391
+ image.height,
1392
+ 0,
1393
+ 0,
1394
+ width,
1395
+ height
1396
+ );
1397
+ tileW = r.dw;
1398
+ tileH = r.dh;
1399
+ } else if (bgSize === "100% 100%") {
1400
+ tileW = width;
1401
+ tileH = height;
1402
+ } else {
1403
+ tileW = image.width;
1404
+ tileH = image.height;
1405
+ }
1406
+ for (let ty = y; ty < y + height; ty += tileH) {
1407
+ for (let tx = x; tx < x + width; tx += tileW) {
1408
+ ctx.drawImage(image, tx, ty, tileW, tileH);
1409
+ }
1367
1410
  }
1368
1411
  }
1369
1412
  }
@@ -1411,7 +1454,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1411
1454
  const imgW = width - paddingLeft - paddingRight;
1412
1455
  const imgH = height - paddingTop - paddingBottom;
1413
1456
  if (!isClipped) {
1414
- const borderRadius = getBorderRadiusFromStyle(style);
1457
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1415
1458
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1416
1459
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1417
1460
  }
@@ -1474,84 +1517,91 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1474
1517
  }
1475
1518
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1476
1519
  if (isClipped) {
1477
- const borderRadius = getBorderRadiusFromStyle(style);
1520
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1478
1521
  applyClip(ctx, x, y, width, height, borderRadius);
1479
1522
  }
1480
1523
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1481
1524
  drawRect(ctx, x, y, width, height, style);
1482
1525
  }
1483
1526
  if (style.backgroundImage) {
1484
- const gradient = createGradientFromCSS(
1485
- ctx,
1486
- style.backgroundImage,
1487
- x,
1488
- y,
1489
- width,
1490
- height
1491
- );
1492
- if (gradient) {
1493
- ctx.fillStyle = gradient;
1494
- const borderRadius = getBorderRadiusFromStyle(style);
1495
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1496
- ctx.beginPath();
1497
- roundedRect(
1498
- ctx,
1499
- x,
1500
- y,
1501
- width,
1502
- height,
1503
- borderRadius.topLeft,
1504
- borderRadius.topRight,
1505
- borderRadius.bottomRight,
1506
- borderRadius.bottomLeft
1507
- );
1508
- ctx.fill();
1509
- } else {
1510
- ctx.fillRect(x, y, width, height);
1511
- }
1512
- } else {
1513
- const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
1514
- if (urlMatch) {
1515
- const borderRadius = getBorderRadiusFromStyle(style);
1516
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1517
- if (hasRadius2) {
1518
- applyClip(ctx, x, y, width, height, borderRadius);
1519
- }
1520
- const image = await loadImage3(urlMatch[1]);
1521
- const bgSize = style.backgroundSize;
1522
- if (bgSize === "cover") {
1523
- const r = computeCover(
1524
- image.width,
1525
- image.height,
1527
+ const layers = splitGradientArgs(style.backgroundImage);
1528
+ for (let i = layers.length - 1; i >= 0; i--) {
1529
+ const layer = layers[i].trim();
1530
+ const gradient = createGradientFromCSS(ctx, layer, x, y, width, height);
1531
+ if (gradient) {
1532
+ ctx.fillStyle = gradient;
1533
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1534
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1535
+ ctx.beginPath();
1536
+ roundedRect(
1537
+ ctx,
1526
1538
  x,
1527
1539
  y,
1528
1540
  width,
1529
- height
1541
+ height,
1542
+ borderRadius.topLeft,
1543
+ borderRadius.topRight,
1544
+ borderRadius.bottomRight,
1545
+ borderRadius.bottomLeft
1530
1546
  );
1531
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1547
+ ctx.fill();
1532
1548
  } else {
1533
- let tileW, tileH;
1534
- if (bgSize === "contain") {
1535
- const r = computeContain(
1549
+ ctx.fillRect(x, y, width, height);
1550
+ }
1551
+ } else {
1552
+ const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
1553
+ if (urlMatch) {
1554
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1555
+ const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1556
+ if (hasRadius2) {
1557
+ applyClip(ctx, x, y, width, height, borderRadius);
1558
+ }
1559
+ const image = await loadImage3(urlMatch[1]);
1560
+ const bgSize = style.backgroundSize;
1561
+ if (bgSize === "cover") {
1562
+ const r = computeCover(
1536
1563
  image.width,
1537
1564
  image.height,
1538
- 0,
1539
- 0,
1565
+ x,
1566
+ y,
1540
1567
  width,
1541
1568
  height
1542
1569
  );
1543
- tileW = r.dw;
1544
- tileH = r.dh;
1545
- } else if (bgSize === "100% 100%") {
1546
- tileW = width;
1547
- tileH = height;
1570
+ ctx.drawImage(
1571
+ image,
1572
+ r.sx,
1573
+ r.sy,
1574
+ r.sw,
1575
+ r.sh,
1576
+ r.dx,
1577
+ r.dy,
1578
+ r.dw,
1579
+ r.dh
1580
+ );
1548
1581
  } else {
1549
- tileW = image.width;
1550
- tileH = image.height;
1551
- }
1552
- for (let ty = y; ty < y + height; ty += tileH) {
1553
- for (let tx = x; tx < x + width; tx += tileW) {
1554
- ctx.drawImage(image, tx, ty, tileW, tileH);
1582
+ let tileW, tileH;
1583
+ if (bgSize === "contain") {
1584
+ const r = computeContain(
1585
+ image.width,
1586
+ image.height,
1587
+ 0,
1588
+ 0,
1589
+ width,
1590
+ height
1591
+ );
1592
+ tileW = r.dw;
1593
+ tileH = r.dh;
1594
+ } else if (bgSize === "100% 100%") {
1595
+ tileW = width;
1596
+ tileH = height;
1597
+ } else {
1598
+ tileW = image.width;
1599
+ tileH = image.height;
1600
+ }
1601
+ for (let ty = y; ty < y + height; ty += tileH) {
1602
+ for (let tx = x; tx < x + width; tx += tileW) {
1603
+ ctx.drawImage(image, tx, ty, tileW, tileH);
1604
+ }
1555
1605
  }
1556
1606
  }
1557
1607
  }
@@ -1599,7 +1649,7 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1599
1649
  const imgW = width - paddingLeft - paddingRight;
1600
1650
  const imgH = height - paddingTop - paddingBottom;
1601
1651
  if (!isClipped) {
1602
- const borderRadius = getBorderRadiusFromStyle(style);
1652
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1603
1653
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1604
1654
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1605
1655
  }
@@ -1737,8 +1787,8 @@ function parseValue(v) {
1737
1787
  if (v === void 0 || v === null) return void 0;
1738
1788
  const s = String(v);
1739
1789
  if (s === "auto") return "auto";
1740
- const n = parseFloat(s);
1741
- if (!isNaN(n)) return n;
1790
+ const n = Number(s);
1791
+ if (s !== "" && !isNaN(n)) return n;
1742
1792
  return s;
1743
1793
  }
1744
1794
  function expandStyle(raw) {
@@ -1947,7 +1997,15 @@ var DIMENSION_PROPS = [
1947
1997
  "paddingLeft",
1948
1998
  "rowGap",
1949
1999
  "columnGap",
1950
- "flexBasis"
2000
+ "flexBasis",
2001
+ "borderTopWidth",
2002
+ "borderRightWidth",
2003
+ "borderBottomWidth",
2004
+ "borderLeftWidth",
2005
+ "borderTopLeftRadius",
2006
+ "borderTopRightRadius",
2007
+ "borderBottomRightRadius",
2008
+ "borderBottomLeftRadius"
1951
2009
  ];
1952
2010
  function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
1953
2011
  if (value.endsWith("%") || value === "auto") return value;
@@ -2301,10 +2359,14 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2301
2359
  resolveUnits(style, viewportWidth, viewportHeight);
2302
2360
  const tagName = String(type);
2303
2361
  if (tagName === "svg") {
2304
- if (props.width != null && style.width === void 0)
2305
- style.width = Number(props.width);
2306
- if (props.height != null && style.height === void 0)
2307
- style.height = Number(props.height);
2362
+ if (props.width != null && style.width === void 0) {
2363
+ const v = props.width;
2364
+ style.width = typeof v === "string" && v.endsWith("%") ? v : Number(v);
2365
+ }
2366
+ if (props.height != null && style.height === void 0) {
2367
+ const v = props.height;
2368
+ style.height = typeof v === "string" && v.endsWith("%") ? v : Number(v);
2369
+ }
2308
2370
  const viewBox = props.viewBox;
2309
2371
  if (viewBox) {
2310
2372
  const parts = viewBox.split(/[\s,]+/).map(Number);
@@ -2326,10 +2388,14 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2326
2388
  }
2327
2389
  }
2328
2390
  if (tagName === "img") {
2329
- if (props.width != null && style.width === void 0)
2330
- style.width = Number(props.width);
2331
- if (props.height != null && style.height === void 0)
2332
- style.height = Number(props.height);
2391
+ if (props.width != null && style.width === void 0) {
2392
+ const v = props.width;
2393
+ style.width = typeof v === "string" && v.endsWith("%") ? v : Number(v);
2394
+ }
2395
+ if (props.height != null && style.height === void 0) {
2396
+ const v = props.height;
2397
+ style.height = typeof v === "string" && v.endsWith("%") ? v : Number(v);
2398
+ }
2333
2399
  const src = props.src;
2334
2400
  if (src) {
2335
2401
  try {
@@ -2369,6 +2435,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
2369
2435
  emojiEnabled
2370
2436
  );
2371
2437
  childYogaNode.setMeasureFunc(measureFunc);
2438
+ childYogaNode.setFlexGrow(1);
2439
+ childYogaNode.setFlexShrink(1);
2372
2440
  yogaNode.insertChild(childYogaNode, 0);
2373
2441
  return {
2374
2442
  type: tagName,