@effing/canvas 0.18.4 → 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;
@@ -951,9 +958,14 @@ function parsePoints(value) {
951
958
  }
952
959
  function applyFillAndStroke(ctx, props, path, inheritedFill, color) {
953
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
+ }
954
966
  if (fill !== "none") {
955
967
  ctx.fillStyle = fill;
956
- ctx.fill(path);
968
+ ctx.fill(path, fillRule ?? "nonzero");
957
969
  }
958
970
  applyStroke(ctx, props, path, color);
959
971
  }
@@ -1014,7 +1026,7 @@ async function loadEmoji(type, code) {
1014
1026
  }
1015
1027
 
1016
1028
  // src/jsx/text/emoji-split.ts
1017
- function splitTextIntoRuns(text, measureText2, emojiSize) {
1029
+ function splitTextIntoRuns(text, measureText2, emojiSize, letterSpacing = 0) {
1018
1030
  const runs = [];
1019
1031
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1020
1032
  let currentText = "";
@@ -1024,13 +1036,18 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1024
1036
  if (isEmojiGrapheme(segment)) {
1025
1037
  if (currentText) {
1026
1038
  const textWidth = measureText2(currentText);
1039
+ const graphemeCount = [
1040
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1041
+ currentText
1042
+ )
1043
+ ].length;
1027
1044
  runs.push({
1028
1045
  kind: "text",
1029
1046
  text: currentText,
1030
1047
  x: textStartX,
1031
- width: textWidth
1048
+ width: textWidth + letterSpacing * graphemeCount
1032
1049
  });
1033
- currentX = textStartX + textWidth;
1050
+ currentX = textStartX + textWidth + letterSpacing * graphemeCount;
1034
1051
  currentText = "";
1035
1052
  }
1036
1053
  runs.push({
@@ -1039,7 +1056,7 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1039
1056
  x: currentX,
1040
1057
  width: emojiSize
1041
1058
  });
1042
- currentX += emojiSize;
1059
+ currentX += emojiSize + letterSpacing;
1043
1060
  textStartX = currentX;
1044
1061
  } else {
1045
1062
  if (!currentText) textStartX = currentX;
@@ -1048,11 +1065,16 @@ function splitTextIntoRuns(text, measureText2, emojiSize) {
1048
1065
  }
1049
1066
  if (currentText) {
1050
1067
  const textWidth = measureText2(currentText);
1068
+ const graphemeCount = [
1069
+ ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1070
+ currentText
1071
+ )
1072
+ ].length;
1051
1073
  runs.push({
1052
1074
  kind: "text",
1053
1075
  text: currentText,
1054
1076
  x: textStartX,
1055
- width: textWidth
1077
+ width: textWidth + letterSpacing * graphemeCount
1056
1078
  });
1057
1079
  }
1058
1080
  return runs;
@@ -1103,20 +1125,29 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
1103
1125
  }
1104
1126
  }
1105
1127
  async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
1128
+ const letterSpacing = seg.letterSpacing ?? 0;
1106
1129
  const runs = splitTextIntoRuns(
1107
1130
  seg.text,
1108
1131
  (text) => {
1109
1132
  setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
1110
1133
  return ctx.measureText(text).width;
1111
1134
  },
1112
- seg.fontSize
1135
+ seg.fontSize,
1136
+ letterSpacing
1113
1137
  );
1114
1138
  for (const run of runs) {
1115
1139
  if (run.kind === "text") {
1116
- if (textShadow) {
1117
- 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);
1118
1150
  }
1119
- ctx.fillText(run.text, x + run.x, y);
1120
1151
  } else {
1121
1152
  const img = await loadEmojiImage(emojiStyle, run.char);
1122
1153
  if (img) {
@@ -1291,84 +1322,91 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1291
1322
  }
1292
1323
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1293
1324
  if (isClipped) {
1294
- const borderRadius = getBorderRadiusFromStyle(style);
1325
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1295
1326
  applyClip(ctx, x, y, width, height, borderRadius);
1296
1327
  }
1297
1328
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1298
1329
  drawRect(ctx, x, y, width, height, style);
1299
1330
  }
1300
1331
  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,
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,
1343
1343
  x,
1344
1344
  y,
1345
1345
  width,
1346
- height
1346
+ height,
1347
+ borderRadius.topLeft,
1348
+ borderRadius.topRight,
1349
+ borderRadius.bottomRight,
1350
+ borderRadius.bottomLeft
1347
1351
  );
1348
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1352
+ ctx.fill();
1349
1353
  } else {
1350
- let tileW, tileH;
1351
- if (bgSize === "contain") {
1352
- 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(
1353
1368
  image.width,
1354
1369
  image.height,
1355
- 0,
1356
- 0,
1370
+ x,
1371
+ y,
1357
1372
  width,
1358
1373
  height
1359
1374
  );
1360
- tileW = r.dw;
1361
- tileH = r.dh;
1362
- } else if (bgSize === "100% 100%") {
1363
- tileW = width;
1364
- 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
+ );
1365
1386
  } 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);
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
+ }
1372
1410
  }
1373
1411
  }
1374
1412
  }
@@ -1416,7 +1454,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1416
1454
  const imgW = width - paddingLeft - paddingRight;
1417
1455
  const imgH = height - paddingTop - paddingBottom;
1418
1456
  if (!isClipped) {
1419
- const borderRadius = getBorderRadiusFromStyle(style);
1457
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1420
1458
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1421
1459
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1422
1460
  }
@@ -1479,84 +1517,91 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1479
1517
  }
1480
1518
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1481
1519
  if (isClipped) {
1482
- const borderRadius = getBorderRadiusFromStyle(style);
1520
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1483
1521
  applyClip(ctx, x, y, width, height, borderRadius);
1484
1522
  }
1485
1523
  if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1486
1524
  drawRect(ctx, x, y, width, height, style);
1487
1525
  }
1488
1526
  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,
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,
1531
1538
  x,
1532
1539
  y,
1533
1540
  width,
1534
- height
1541
+ height,
1542
+ borderRadius.topLeft,
1543
+ borderRadius.topRight,
1544
+ borderRadius.bottomRight,
1545
+ borderRadius.bottomLeft
1535
1546
  );
1536
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1547
+ ctx.fill();
1537
1548
  } else {
1538
- let tileW, tileH;
1539
- if (bgSize === "contain") {
1540
- 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(
1541
1563
  image.width,
1542
1564
  image.height,
1543
- 0,
1544
- 0,
1565
+ x,
1566
+ y,
1545
1567
  width,
1546
1568
  height
1547
1569
  );
1548
- tileW = r.dw;
1549
- tileH = r.dh;
1550
- } else if (bgSize === "100% 100%") {
1551
- tileW = width;
1552
- 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
+ );
1553
1581
  } 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);
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
+ }
1560
1605
  }
1561
1606
  }
1562
1607
  }
@@ -1604,7 +1649,7 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
1604
1649
  const imgW = width - paddingLeft - paddingRight;
1605
1650
  const imgH = height - paddingTop - paddingBottom;
1606
1651
  if (!isClipped) {
1607
- const borderRadius = getBorderRadiusFromStyle(style);
1652
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
1608
1653
  if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1609
1654
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1610
1655
  }
@@ -1742,8 +1787,8 @@ function parseValue(v) {
1742
1787
  if (v === void 0 || v === null) return void 0;
1743
1788
  const s = String(v);
1744
1789
  if (s === "auto") return "auto";
1745
- const n = parseFloat(s);
1746
- if (!isNaN(n)) return n;
1790
+ const n = Number(s);
1791
+ if (s !== "" && !isNaN(n)) return n;
1747
1792
  return s;
1748
1793
  }
1749
1794
  function expandStyle(raw) {
@@ -1952,7 +1997,15 @@ var DIMENSION_PROPS = [
1952
1997
  "paddingLeft",
1953
1998
  "rowGap",
1954
1999
  "columnGap",
1955
- "flexBasis"
2000
+ "flexBasis",
2001
+ "borderTopWidth",
2002
+ "borderRightWidth",
2003
+ "borderBottomWidth",
2004
+ "borderLeftWidth",
2005
+ "borderTopLeftRadius",
2006
+ "borderTopRightRadius",
2007
+ "borderBottomRightRadius",
2008
+ "borderBottomLeftRadius"
1956
2009
  ];
1957
2010
  function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
1958
2011
  if (value.endsWith("%") || value === "auto") return value;