@effing/canvas 0.19.2 → 0.19.3

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
@@ -266,8 +266,7 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
266
266
  processedText = text.replace(/\b\w/g, (c) => c.toUpperCase());
267
267
  }
268
268
  const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
269
- const preserveWhitespace = whiteSpace === "pre" || whiteSpace === "pre-wrap";
270
- const paragraphs = preserveWhitespace ? processedText.split("\n") : processedText.split("\n");
269
+ const paragraphs = processedText.split("\n");
271
270
  const lines = [];
272
271
  for (const paragraph of paragraphs) {
273
272
  if (noWrap) {
@@ -738,10 +737,30 @@ async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
738
737
  }
739
738
  }
740
739
 
740
+ // src/jsx/draw/utils.ts
741
+ function parseCSSLength(value, referenceSize) {
742
+ if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
743
+ return parseFloat(value);
744
+ }
745
+ function toNumber(v) {
746
+ if (typeof v === "number") return v;
747
+ if (v === void 0 || v === null) return 0;
748
+ const n = parseFloat(String(v));
749
+ return isNaN(n) ? 0 : n;
750
+ }
751
+ function resolveBoxValue(v, referenceWidth) {
752
+ if (typeof v === "number") return v;
753
+ if (v === void 0 || v === null) return 0;
754
+ const s = String(v);
755
+ if (s.endsWith("%")) return parseFloat(s) / 100 * referenceWidth;
756
+ const n = parseFloat(s);
757
+ return isNaN(n) ? 0 : n;
758
+ }
759
+
741
760
  // src/jsx/draw/rect.ts
742
761
  function drawRect(ctx, x, y, width, height, style) {
743
- const borderRadius = getBorderRadius(style, width, height);
744
- const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
762
+ const borderRadius = getBorderRadiusFromStyle(style, width, height);
763
+ const hasRoundedCorners = hasRadius(borderRadius);
745
764
  if (style.boxShadow) {
746
765
  drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
747
766
  }
@@ -771,7 +790,7 @@ function resolveRadius(v, width, height) {
771
790
  if (typeof v === "string") return parseCSSLength(v, Math.min(width, height));
772
791
  return toNumber(v);
773
792
  }
774
- function getBorderRadius(style, width, height) {
793
+ function getBorderRadiusFromStyle(style, width, height) {
775
794
  return {
776
795
  topLeft: resolveRadius(style.borderTopLeftRadius, width, height),
777
796
  topRight: resolveRadius(style.borderTopRightRadius, width, height),
@@ -779,11 +798,8 @@ function getBorderRadius(style, width, height) {
779
798
  bottomLeft: resolveRadius(style.borderBottomLeftRadius, width, height)
780
799
  };
781
800
  }
782
- function getBorderRadiusFromStyle(style, width, height) {
783
- return getBorderRadius(style, width, height);
784
- }
785
801
  function drawBorders(ctx, x, y, width, height, style, borderRadius) {
786
- const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
802
+ const hasRoundedCorners = hasRadius(borderRadius);
787
803
  const tw = resolveBoxValue(style.borderTopWidth, width);
788
804
  const rw = resolveBoxValue(style.borderRightWidth, width);
789
805
  const bw = resolveBoxValue(style.borderBottomWidth, width);
@@ -1573,25 +1589,22 @@ function splitTextIntoRuns(text, measureText2, emojiSize, letterSpacing = 0) {
1573
1589
  const runs = [];
1574
1590
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1575
1591
  let currentText = "";
1592
+ let currentGraphemeCount = 0;
1576
1593
  let currentX = 0;
1577
1594
  let textStartX = 0;
1578
1595
  for (const { segment } of segmenter.segment(text)) {
1579
1596
  if (isEmojiGrapheme(segment)) {
1580
1597
  if (currentText) {
1581
1598
  const textWidth = measureText2(currentText);
1582
- const graphemeCount = [
1583
- ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1584
- currentText
1585
- )
1586
- ].length;
1587
1599
  runs.push({
1588
1600
  kind: "text",
1589
1601
  text: currentText,
1590
1602
  x: textStartX,
1591
- width: textWidth + letterSpacing * graphemeCount
1603
+ width: textWidth + letterSpacing * currentGraphemeCount
1592
1604
  });
1593
- currentX = textStartX + textWidth + letterSpacing * graphemeCount;
1605
+ currentX = textStartX + textWidth + letterSpacing * currentGraphemeCount;
1594
1606
  currentText = "";
1607
+ currentGraphemeCount = 0;
1595
1608
  }
1596
1609
  runs.push({
1597
1610
  kind: "emoji",
@@ -1604,20 +1617,16 @@ function splitTextIntoRuns(text, measureText2, emojiSize, letterSpacing = 0) {
1604
1617
  } else {
1605
1618
  if (!currentText) textStartX = currentX;
1606
1619
  currentText += segment;
1620
+ currentGraphemeCount++;
1607
1621
  }
1608
1622
  }
1609
1623
  if (currentText) {
1610
1624
  const textWidth = measureText2(currentText);
1611
- const graphemeCount = [
1612
- ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
1613
- currentText
1614
- )
1615
- ].length;
1616
1625
  runs.push({
1617
1626
  kind: "text",
1618
1627
  text: currentText,
1619
1628
  x: textStartX,
1620
- width: textWidth + letterSpacing * graphemeCount
1629
+ width: textWidth + letterSpacing * currentGraphemeCount
1621
1630
  });
1622
1631
  }
1623
1632
  return runs;
@@ -1680,15 +1689,12 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
1680
1689
  );
1681
1690
  for (const run of runs) {
1682
1691
  if (run.kind === "text") {
1692
+ if (textShadow) {
1693
+ drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1694
+ }
1683
1695
  if (letterSpacing !== 0) {
1684
- if (textShadow) {
1685
- drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1686
- }
1687
1696
  drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
1688
1697
  } else {
1689
- if (textShadow) {
1690
- drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
1691
- }
1692
1698
  ctx.fillText(run.text, x + run.x, y);
1693
1699
  }
1694
1700
  } else {
@@ -1803,7 +1809,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1803
1809
  const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
1804
1810
  offCtx.save();
1805
1811
  offCtx.scale(qx, qy);
1806
- await drawNodeInner(
1812
+ await drawNodeCore(
1807
1813
  offCtx,
1808
1814
  node,
1809
1815
  parentX,
@@ -1845,182 +1851,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1845
1851
  return;
1846
1852
  }
1847
1853
  }
1848
- ctx.save();
1849
- if (opacity < 1) {
1850
- ctx.globalAlpha *= opacity;
1851
- }
1852
- if (style.filter) {
1853
- ctx.filter = style.filter;
1854
- }
1855
- if (style.transform) {
1856
- applyTransform(
1857
- ctx,
1858
- style.transform,
1859
- x,
1860
- y,
1861
- width,
1862
- height,
1863
- style.transformOrigin
1864
- );
1865
- }
1866
- const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1867
- if (isClipped) {
1868
- const borderRadius = getBorderRadiusFromStyle(style, width, height);
1869
- applyClip(ctx, x, y, width, height, borderRadius);
1870
- }
1871
- if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1872
- drawRect(ctx, x, y, width, height, style);
1873
- }
1874
- if (style.backgroundImage) {
1875
- const layers = splitGradientArgs(style.backgroundImage);
1876
- for (let i = layers.length - 1; i >= 0; i--) {
1877
- const layer = layers[i].trim();
1878
- const gradient = createGradientFromCSS(ctx, layer, x, y, width, height);
1879
- if (gradient) {
1880
- ctx.fillStyle = gradient;
1881
- const borderRadius = getBorderRadiusFromStyle(style, width, height);
1882
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1883
- ctx.beginPath();
1884
- roundedRect(
1885
- ctx,
1886
- x,
1887
- y,
1888
- width,
1889
- height,
1890
- borderRadius.topLeft,
1891
- borderRadius.topRight,
1892
- borderRadius.bottomRight,
1893
- borderRadius.bottomLeft
1894
- );
1895
- ctx.fill();
1896
- } else {
1897
- ctx.fillRect(x, y, width, height);
1898
- }
1899
- } else {
1900
- const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
1901
- if (urlMatch) {
1902
- const borderRadius = getBorderRadiusFromStyle(style, width, height);
1903
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1904
- if (hasRadius2) {
1905
- applyClip(ctx, x, y, width, height, borderRadius);
1906
- }
1907
- const image = await loadImage3(urlMatch[1]);
1908
- const bgSize = style.backgroundSize;
1909
- if (bgSize === "cover") {
1910
- const r = computeCover(
1911
- image.width,
1912
- image.height,
1913
- x,
1914
- y,
1915
- width,
1916
- height
1917
- );
1918
- ctx.drawImage(
1919
- image,
1920
- r.sx,
1921
- r.sy,
1922
- r.sw,
1923
- r.sh,
1924
- r.dx,
1925
- r.dy,
1926
- r.dw,
1927
- r.dh
1928
- );
1929
- } else {
1930
- let tileW, tileH;
1931
- if (bgSize === "contain") {
1932
- const r = computeContain(
1933
- image.width,
1934
- image.height,
1935
- 0,
1936
- 0,
1937
- width,
1938
- height
1939
- );
1940
- tileW = r.dw;
1941
- tileH = r.dh;
1942
- } else if (bgSize === "100% 100%") {
1943
- tileW = width;
1944
- tileH = height;
1945
- } else {
1946
- tileW = image.width;
1947
- tileH = image.height;
1948
- }
1949
- for (let ty = y; ty < y + height; ty += tileH) {
1950
- for (let tx = x; tx < x + width; tx += tileW) {
1951
- ctx.drawImage(image, tx, ty, tileW, tileH);
1952
- }
1953
- }
1954
- }
1955
- }
1956
- }
1957
- }
1958
- }
1959
- if (debug) {
1960
- ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
1961
- ctx.lineWidth = 1;
1962
- ctx.strokeRect(x, y, width, height);
1963
- }
1964
- if (node.textContent !== void 0 && node.textContent !== "") {
1965
- const paddingTop = resolveBoxValue(style.paddingTop, width);
1966
- const paddingLeft = resolveBoxValue(style.paddingLeft, width);
1967
- const paddingRight = resolveBoxValue(style.paddingRight, width);
1968
- const borderTopW = resolveBoxValue(style.borderTopWidth, width);
1969
- const borderLeftW = resolveBoxValue(style.borderLeftWidth, width);
1970
- const borderRightW = resolveBoxValue(style.borderRightWidth, width);
1971
- const contentX = x + paddingLeft + borderLeftW;
1972
- const contentY = y + paddingTop + borderTopW;
1973
- const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
1974
- const textLayout = layoutText(
1975
- node.textContent,
1976
- style,
1977
- contentWidth,
1978
- ctx,
1979
- !!emojiStyle
1980
- );
1981
- await drawText(
1982
- ctx,
1983
- textLayout.segments,
1984
- contentX,
1985
- contentY,
1986
- style.textShadow,
1987
- emojiStyle
1988
- );
1989
- }
1990
- if (node.type === "img" && node.props.src) {
1991
- const paddingTop = resolveBoxValue(style.paddingTop, width);
1992
- const paddingLeft = resolveBoxValue(style.paddingLeft, width);
1993
- const paddingRight = resolveBoxValue(style.paddingRight, width);
1994
- const paddingBottom = resolveBoxValue(style.paddingBottom, width);
1995
- const imgX = x + paddingLeft;
1996
- const imgY = y + paddingTop;
1997
- const imgW = width - paddingLeft - paddingRight;
1998
- const imgH = height - paddingTop - paddingBottom;
1999
- if (!isClipped) {
2000
- const borderRadius = getBorderRadiusFromStyle(style, width, height);
2001
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
2002
- applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
2003
- }
2004
- }
2005
- await drawImage(
2006
- ctx,
2007
- node.props.src,
2008
- imgX,
2009
- imgY,
2010
- imgW,
2011
- imgH,
2012
- style,
2013
- node.props.__loadedImage
2014
- );
2015
- }
2016
- if (node.type === "svg") {
2017
- drawSvgContainer(ctx, node, x, y, width, height);
2018
- } else {
2019
- for (const child of node.children) {
2020
- await drawNode(ctx, child, x, y, debug, emojiStyle);
2021
- }
2022
- }
2023
- ctx.restore();
1854
+ await drawNodeCore(ctx, node, parentX, parentY, 0, 0, debug, emojiStyle);
2024
1855
  }
2025
1856
  function extractScale(transform) {
2026
1857
  const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
@@ -2032,7 +1863,7 @@ function extractScale(transform) {
2032
1863
  const remaining = transform.replace(fullMatch, "").trim();
2033
1864
  return { sx, sy, remaining };
2034
1865
  }
2035
- async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, overrideTransform) {
1866
+ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, overrideTransform) {
2036
1867
  const x = parentX + node.x + offsetX;
2037
1868
  const y = parentY + node.y + offsetY;
2038
1869
  const { width, height, style } = node;
@@ -2074,7 +1905,7 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
2074
1905
  if (gradient) {
2075
1906
  ctx.fillStyle = gradient;
2076
1907
  const borderRadius = getBorderRadiusFromStyle(style, width, height);
2077
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1908
+ if (hasRadius(borderRadius)) {
2078
1909
  ctx.beginPath();
2079
1910
  roundedRect(
2080
1911
  ctx,
@@ -2095,8 +1926,7 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
2095
1926
  const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
2096
1927
  if (urlMatch) {
2097
1928
  const borderRadius = getBorderRadiusFromStyle(style, width, height);
2098
- const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
2099
- if (hasRadius2) {
1929
+ if (hasRadius(borderRadius)) {
2100
1930
  applyClip(ctx, x, y, width, height, borderRadius);
2101
1931
  }
2102
1932
  const image = await loadImage3(urlMatch[1]);
@@ -2193,7 +2023,7 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
2193
2023
  const imgH = height - paddingTop - paddingBottom;
2194
2024
  if (!isClipped) {
2195
2025
  const borderRadius = getBorderRadiusFromStyle(style, width, height);
2196
- if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
2026
+ if (hasRadius(borderRadius)) {
2197
2027
  applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
2198
2028
  }
2199
2029
  }
@@ -2212,15 +2042,25 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
2212
2042
  drawSvgContainer(ctx, node, x, y, width, height);
2213
2043
  } else {
2214
2044
  for (const child of node.children) {
2215
- await drawNodeInner(ctx, child, x, y, 0, 0, debug, emojiStyle, void 0);
2045
+ if (offsetX === 0 && offsetY === 0) {
2046
+ await drawNode(ctx, child, x, y, debug, emojiStyle);
2047
+ } else {
2048
+ await drawNodeCore(
2049
+ ctx,
2050
+ child,
2051
+ x,
2052
+ y,
2053
+ 0,
2054
+ 0,
2055
+ debug,
2056
+ emojiStyle,
2057
+ void 0
2058
+ );
2059
+ }
2216
2060
  }
2217
2061
  }
2218
2062
  ctx.restore();
2219
2063
  }
2220
- function parseCSSLength(value, referenceSize) {
2221
- if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
2222
- return parseFloat(value);
2223
- }
2224
2064
  function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
2225
2065
  let ox = x + width / 2;
2226
2066
  let oy = y + height / 2;
@@ -2285,20 +2125,6 @@ function parseAngle(value) {
2285
2125
  if (value.endsWith("turn")) return parseFloat(value) * 2 * Math.PI;
2286
2126
  return parseFloat(value);
2287
2127
  }
2288
- function toNumber(v) {
2289
- if (typeof v === "number") return v;
2290
- if (v === void 0 || v === null) return 0;
2291
- const n = parseFloat(String(v));
2292
- return isNaN(n) ? 0 : n;
2293
- }
2294
- function resolveBoxValue(v, referenceWidth) {
2295
- if (typeof v === "number") return v;
2296
- if (v === void 0 || v === null) return 0;
2297
- const s = String(v);
2298
- if (s.endsWith("%")) return parseFloat(s) / 100 * referenceWidth;
2299
- const n = parseFloat(s);
2300
- return isNaN(n) ? 0 : n;
2301
- }
2302
2128
 
2303
2129
  // src/jsx/font.ts
2304
2130
  import { GlobalFonts } from "@napi-rs/canvas";