@effing/canvas 0.1.0 → 0.18.1

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
@@ -21,7 +21,7 @@ function renderLottieFrame(ctx, animation, frame) {
21
21
  }
22
22
 
23
23
  // src/jsx/draw/index.ts
24
- import { loadImage as loadImage3 } from "@napi-rs/canvas";
24
+ import { createCanvas as createCanvas2, loadImage as loadImage3 } from "@napi-rs/canvas";
25
25
 
26
26
  // src/jsx/language.ts
27
27
  function isEmoji(char) {
@@ -948,9 +948,9 @@ function applyStroke(ctx, props, path) {
948
948
  const stroke = props.stroke;
949
949
  if (!stroke || stroke === "none") return;
950
950
  ctx.strokeStyle = stroke;
951
- ctx.lineWidth = Number(props.strokeWidth ?? 1);
952
- ctx.lineCap = props.strokeLinecap ?? "butt";
953
- ctx.lineJoin = props.strokeLinejoin ?? "miter";
951
+ ctx.lineWidth = Number(props.strokeWidth ?? props["stroke-width"] ?? 1);
952
+ ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? "butt";
953
+ ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? "miter";
954
954
  ctx.stroke(path);
955
955
  }
956
956
 
@@ -1168,6 +1168,34 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
1168
1168
  }
1169
1169
 
1170
1170
  // src/jsx/draw/index.ts
1171
+ var canvasPool = /* @__PURE__ */ new Map();
1172
+ function acquireOffscreen(w, h) {
1173
+ const key = `${w}x${h}`;
1174
+ const stack = canvasPool.get(key);
1175
+ if (stack) {
1176
+ while (stack.length > 0) {
1177
+ const ref = stack.pop();
1178
+ const canvas2 = ref.deref();
1179
+ if (canvas2) {
1180
+ const ctx = canvas2.getContext("2d");
1181
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1182
+ ctx.clearRect(0, 0, w, h);
1183
+ return [canvas2, ctx];
1184
+ }
1185
+ }
1186
+ }
1187
+ const canvas = createCanvas2(w, h);
1188
+ return [canvas, canvas.getContext("2d")];
1189
+ }
1190
+ function releaseOffscreen(canvas) {
1191
+ const key = `${canvas.width}x${canvas.height}`;
1192
+ let stack = canvasPool.get(key);
1193
+ if (!stack) {
1194
+ stack = [];
1195
+ canvasPool.set(key, stack);
1196
+ }
1197
+ stack.push(new WeakRef(canvas));
1198
+ }
1171
1199
  async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1172
1200
  const x = parentX + node.x;
1173
1201
  const y = parentY + node.y;
@@ -1175,6 +1203,61 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1175
1203
  if (style.display === "none") return;
1176
1204
  const opacity = style.opacity ?? 1;
1177
1205
  if (opacity <= 0) return;
1206
+ const scaleInfo = style.transform ? extractScale(style.transform) : null;
1207
+ if (scaleInfo && (scaleInfo.sx !== 1 || scaleInfo.sy !== 1)) {
1208
+ const sx = scaleInfo.sx;
1209
+ const sy = scaleInfo.sy;
1210
+ const transformWithoutScale = scaleInfo.remaining;
1211
+ const qx = Math.max(1, Math.ceil(Math.abs(sx)));
1212
+ const qy = Math.max(1, Math.ceil(Math.abs(sy)));
1213
+ const bufW = Math.ceil((width + 2) * qx);
1214
+ const bufH = Math.ceil((height + 2) * qy);
1215
+ if (bufW > 0 && bufH > 0) {
1216
+ const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
1217
+ offCtx.save();
1218
+ offCtx.scale(qx, qy);
1219
+ await drawNodeInner(
1220
+ offCtx,
1221
+ node,
1222
+ parentX,
1223
+ parentY,
1224
+ 1 - x,
1225
+ 1 - y,
1226
+ debug,
1227
+ emojiStyle,
1228
+ transformWithoutScale
1229
+ );
1230
+ offCtx.restore();
1231
+ ctx.save();
1232
+ if (opacity < 1) {
1233
+ ctx.globalAlpha *= opacity;
1234
+ }
1235
+ let ox = x + width / 2;
1236
+ let oy = y + height / 2;
1237
+ if (style.transformOrigin) {
1238
+ const parts = style.transformOrigin.split(/\s+/);
1239
+ ox = resolveOrigin(parts[0], x, width);
1240
+ oy = resolveOrigin(parts[1], y, height);
1241
+ }
1242
+ ctx.translate(ox, oy);
1243
+ ctx.scale(sx, sy);
1244
+ ctx.translate(-ox, -oy);
1245
+ ctx.drawImage(
1246
+ offscreen,
1247
+ 0,
1248
+ 0,
1249
+ bufW,
1250
+ bufH,
1251
+ x - 1,
1252
+ y - 1,
1253
+ width + 2,
1254
+ height + 2
1255
+ );
1256
+ releaseOffscreen(offscreen);
1257
+ ctx.restore();
1258
+ return;
1259
+ }
1260
+ }
1178
1261
  ctx.save();
1179
1262
  if (opacity < 1) {
1180
1263
  ctx.globalAlpha *= opacity;
@@ -1344,6 +1427,193 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
1344
1427
  }
1345
1428
  ctx.restore();
1346
1429
  }
1430
+ function extractScale(transform) {
1431
+ const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
1432
+ if (!scaleMatch) return null;
1433
+ const [fullMatch, name, args] = scaleMatch;
1434
+ const values = args.split(",").map((s) => s.trim());
1435
+ const sx = name === "scaleY" ? 1 : parseFloat(values[0]);
1436
+ const sy = name === "scaleX" ? 1 : parseFloat(values[name === "scale" ? 1 : 0] ?? String(sx));
1437
+ const remaining = transform.replace(fullMatch, "").trim();
1438
+ return { sx, sy, remaining };
1439
+ }
1440
+ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, overrideTransform) {
1441
+ const x = parentX + node.x + offsetX;
1442
+ const y = parentY + node.y + offsetY;
1443
+ const { width, height, style } = node;
1444
+ if (style.display === "none") return;
1445
+ const opacity = style.opacity ?? 1;
1446
+ if (opacity <= 0) return;
1447
+ ctx.save();
1448
+ if (opacity < 1) {
1449
+ ctx.globalAlpha *= opacity;
1450
+ }
1451
+ if (style.filter) {
1452
+ ctx.filter = style.filter;
1453
+ }
1454
+ const transformToApply = overrideTransform !== void 0 ? overrideTransform : style.transform;
1455
+ if (transformToApply) {
1456
+ applyTransform(
1457
+ ctx,
1458
+ transformToApply,
1459
+ x,
1460
+ y,
1461
+ width,
1462
+ height,
1463
+ style.transformOrigin
1464
+ );
1465
+ }
1466
+ const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
1467
+ if (isClipped) {
1468
+ const borderRadius = getBorderRadiusFromStyle(style);
1469
+ applyClip(ctx, x, y, width, height, borderRadius);
1470
+ }
1471
+ if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
1472
+ drawRect(ctx, x, y, width, height, style);
1473
+ }
1474
+ if (style.backgroundImage) {
1475
+ const gradient = createGradientFromCSS(
1476
+ ctx,
1477
+ style.backgroundImage,
1478
+ x,
1479
+ y,
1480
+ width,
1481
+ height
1482
+ );
1483
+ if (gradient) {
1484
+ ctx.fillStyle = gradient;
1485
+ const borderRadius = getBorderRadiusFromStyle(style);
1486
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1487
+ ctx.beginPath();
1488
+ roundedRect(
1489
+ ctx,
1490
+ x,
1491
+ y,
1492
+ width,
1493
+ height,
1494
+ borderRadius.topLeft,
1495
+ borderRadius.topRight,
1496
+ borderRadius.bottomRight,
1497
+ borderRadius.bottomLeft
1498
+ );
1499
+ ctx.fill();
1500
+ } else {
1501
+ ctx.fillRect(x, y, width, height);
1502
+ }
1503
+ } else {
1504
+ const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
1505
+ if (urlMatch) {
1506
+ const borderRadius = getBorderRadiusFromStyle(style);
1507
+ const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
1508
+ if (hasRadius2) {
1509
+ applyClip(ctx, x, y, width, height, borderRadius);
1510
+ }
1511
+ const image = await loadImage3(urlMatch[1]);
1512
+ const bgSize = style.backgroundSize;
1513
+ if (bgSize === "cover") {
1514
+ const r = computeCover(
1515
+ image.width,
1516
+ image.height,
1517
+ x,
1518
+ y,
1519
+ width,
1520
+ height
1521
+ );
1522
+ ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1523
+ } else {
1524
+ let tileW, tileH;
1525
+ if (bgSize === "contain") {
1526
+ const r = computeContain(
1527
+ image.width,
1528
+ image.height,
1529
+ 0,
1530
+ 0,
1531
+ width,
1532
+ height
1533
+ );
1534
+ tileW = r.dw;
1535
+ tileH = r.dh;
1536
+ } else if (bgSize === "100% 100%") {
1537
+ tileW = width;
1538
+ tileH = height;
1539
+ } else {
1540
+ tileW = image.width;
1541
+ tileH = image.height;
1542
+ }
1543
+ for (let ty = y; ty < y + height; ty += tileH) {
1544
+ for (let tx = x; tx < x + width; tx += tileW) {
1545
+ ctx.drawImage(image, tx, ty, tileW, tileH);
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1552
+ if (debug) {
1553
+ ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
1554
+ ctx.lineWidth = 1;
1555
+ ctx.strokeRect(x, y, width, height);
1556
+ }
1557
+ if (node.textContent !== void 0 && node.textContent !== "") {
1558
+ const paddingTop = toNumber2(style.paddingTop);
1559
+ const paddingLeft = toNumber2(style.paddingLeft);
1560
+ const paddingRight = toNumber2(style.paddingRight);
1561
+ const borderTopW = toNumber2(style.borderTopWidth);
1562
+ const borderLeftW = toNumber2(style.borderLeftWidth);
1563
+ const borderRightW = toNumber2(style.borderRightWidth);
1564
+ const contentX = x + paddingLeft + borderLeftW;
1565
+ const contentY = y + paddingTop + borderTopW;
1566
+ const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
1567
+ const textLayout = layoutText(
1568
+ node.textContent,
1569
+ style,
1570
+ contentWidth,
1571
+ ctx,
1572
+ !!emojiStyle
1573
+ );
1574
+ await drawText(
1575
+ ctx,
1576
+ textLayout.segments,
1577
+ contentX,
1578
+ contentY,
1579
+ style.textShadow,
1580
+ emojiStyle
1581
+ );
1582
+ }
1583
+ if (node.type === "img" && node.props.src) {
1584
+ const paddingTop = toNumber2(style.paddingTop);
1585
+ const paddingLeft = toNumber2(style.paddingLeft);
1586
+ const paddingRight = toNumber2(style.paddingRight);
1587
+ const paddingBottom = toNumber2(style.paddingBottom);
1588
+ const imgX = x + paddingLeft;
1589
+ const imgY = y + paddingTop;
1590
+ const imgW = width - paddingLeft - paddingRight;
1591
+ const imgH = height - paddingTop - paddingBottom;
1592
+ if (!isClipped) {
1593
+ const borderRadius = getBorderRadiusFromStyle(style);
1594
+ if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
1595
+ applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
1596
+ }
1597
+ }
1598
+ await drawImage(
1599
+ ctx,
1600
+ node.props.src,
1601
+ imgX,
1602
+ imgY,
1603
+ imgW,
1604
+ imgH,
1605
+ style
1606
+ );
1607
+ }
1608
+ if (node.type === "svg") {
1609
+ drawSvgContainer(ctx, node, x, y, width, height);
1610
+ } else {
1611
+ for (const child of node.children) {
1612
+ await drawNodeInner(ctx, child, x, y, 0, 0, debug, emojiStyle, void 0);
1613
+ }
1614
+ }
1615
+ ctx.restore();
1616
+ }
1347
1617
  function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
1348
1618
  let ox = x + width / 2;
1349
1619
  let oy = y + height / 2;
@@ -1643,6 +1913,99 @@ function resolveStyle(rawStyle, parentStyle) {
1643
1913
  }
1644
1914
  return style;
1645
1915
  }
1916
+ var DIMENSION_PROPS = [
1917
+ "width",
1918
+ "height",
1919
+ "minWidth",
1920
+ "minHeight",
1921
+ "maxWidth",
1922
+ "maxHeight",
1923
+ "top",
1924
+ "right",
1925
+ "bottom",
1926
+ "left",
1927
+ "marginTop",
1928
+ "marginRight",
1929
+ "marginBottom",
1930
+ "marginLeft",
1931
+ "paddingTop",
1932
+ "paddingRight",
1933
+ "paddingBottom",
1934
+ "paddingLeft",
1935
+ "rowGap",
1936
+ "columnGap",
1937
+ "flexBasis"
1938
+ ];
1939
+ function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
1940
+ if (value.endsWith("%") || value === "auto") return value;
1941
+ if (value.endsWith("vmin")) {
1942
+ const n = parseFloat(value);
1943
+ return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
1944
+ }
1945
+ if (value.endsWith("vmax")) {
1946
+ const n = parseFloat(value);
1947
+ return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
1948
+ }
1949
+ if (value.endsWith("vw")) {
1950
+ const n = parseFloat(value);
1951
+ return isNaN(n) ? value : n / 100 * viewportWidth;
1952
+ }
1953
+ if (value.endsWith("vh")) {
1954
+ const n = parseFloat(value);
1955
+ return isNaN(n) ? value : n / 100 * viewportHeight;
1956
+ }
1957
+ if (value.endsWith("rem")) {
1958
+ const n = parseFloat(value);
1959
+ return isNaN(n) ? value : n * rootFontSize;
1960
+ }
1961
+ if (value.endsWith("em")) {
1962
+ const n = parseFloat(value);
1963
+ return isNaN(n) ? value : n * fontSize;
1964
+ }
1965
+ if (value.endsWith("px")) {
1966
+ const n = parseFloat(value);
1967
+ return isNaN(n) ? value : n;
1968
+ }
1969
+ if (value.endsWith("pt")) {
1970
+ const n = parseFloat(value);
1971
+ return isNaN(n) ? value : n * (96 / 72);
1972
+ }
1973
+ if (value.endsWith("pc")) {
1974
+ const n = parseFloat(value);
1975
+ return isNaN(n) ? value : n * 16;
1976
+ }
1977
+ if (value.endsWith("in")) {
1978
+ const n = parseFloat(value);
1979
+ return isNaN(n) ? value : n * 96;
1980
+ }
1981
+ if (value.endsWith("cm")) {
1982
+ const n = parseFloat(value);
1983
+ return isNaN(n) ? value : n * (96 / 2.54);
1984
+ }
1985
+ if (value.endsWith("mm")) {
1986
+ const n = parseFloat(value);
1987
+ return isNaN(n) ? value : n * (96 / 25.4);
1988
+ }
1989
+ return value;
1990
+ }
1991
+ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = DEFAULT_STYLE.fontSize) {
1992
+ const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
1993
+ for (const prop of DIMENSION_PROPS) {
1994
+ const value = style[prop];
1995
+ if (typeof value !== "string") continue;
1996
+ const resolved = resolveUnit(
1997
+ value,
1998
+ viewportWidth,
1999
+ viewportHeight,
2000
+ fontSize,
2001
+ rootFontSize
2002
+ );
2003
+ if (resolved !== value) {
2004
+ style[prop] = resolved;
2005
+ }
2006
+ }
2007
+ return style;
2008
+ }
1646
2009
 
1647
2010
  // src/jsx/yoga.ts
1648
2011
  import Yoga, {
@@ -1837,6 +2200,8 @@ function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEna
1837
2200
  element,
1838
2201
  DEFAULT_STYLE,
1839
2202
  rootYogaNode,
2203
+ containerWidth,
2204
+ containerHeight,
1840
2205
  ctx,
1841
2206
  emojiEnabled
1842
2207
  );
@@ -1847,7 +2212,7 @@ function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEna
1847
2212
  freeYogaNode(rootYogaNode);
1848
2213
  return layoutTree;
1849
2214
  }
1850
- function buildNode(element, parentStyle, yogaNode, ctx, emojiEnabled) {
2215
+ function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled) {
1851
2216
  if (element === null || element === void 0 || typeof element === "boolean") {
1852
2217
  return {
1853
2218
  type: "empty",
@@ -1880,7 +2245,17 @@ function buildNode(element, parentStyle, yogaNode, ctx, emojiEnabled) {
1880
2245
  continue;
1881
2246
  const childYogaNode = createYogaNode();
1882
2247
  yogaNode.insertChild(childYogaNode, children2.length);
1883
- children2.push(buildNode(child, style2, childYogaNode, ctx, emojiEnabled));
2248
+ children2.push(
2249
+ buildNode(
2250
+ child,
2251
+ style2,
2252
+ childYogaNode,
2253
+ viewportWidth,
2254
+ viewportHeight,
2255
+ ctx,
2256
+ emojiEnabled
2257
+ )
2258
+ );
1884
2259
  }
1885
2260
  return {
1886
2261
  type: "div",
@@ -1896,12 +2271,21 @@ function buildNode(element, parentStyle, yogaNode, ctx, emojiEnabled) {
1896
2271
  const rendered = type(
1897
2272
  el.props ?? {}
1898
2273
  );
1899
- return buildNode(rendered, parentStyle, yogaNode, ctx, emojiEnabled);
2274
+ return buildNode(
2275
+ rendered,
2276
+ parentStyle,
2277
+ yogaNode,
2278
+ viewportWidth,
2279
+ viewportHeight,
2280
+ ctx,
2281
+ emojiEnabled
2282
+ );
1900
2283
  }
1901
2284
  const props = el.props ?? {};
1902
2285
  const rawStyle = props.style ?? {};
1903
2286
  const expanded = expandStyle(rawStyle);
1904
2287
  const style = resolveStyle(expanded, parentStyle);
2288
+ resolveUnits(style, viewportWidth, viewportHeight);
1905
2289
  const tagName = String(type);
1906
2290
  if (tagName === "svg") {
1907
2291
  if (props.width != null && style.width === void 0)
@@ -1976,7 +2360,17 @@ function buildNode(element, parentStyle, yogaNode, ctx, emojiEnabled) {
1976
2360
  continue;
1977
2361
  const childYogaNode = createYogaNode();
1978
2362
  yogaNode.insertChild(childYogaNode, children.length);
1979
- children.push(buildNode(child, style, childYogaNode, ctx, emojiEnabled));
2363
+ children.push(
2364
+ buildNode(
2365
+ child,
2366
+ style,
2367
+ childYogaNode,
2368
+ viewportWidth,
2369
+ viewportHeight,
2370
+ ctx,
2371
+ emojiEnabled
2372
+ )
2373
+ );
1980
2374
  }
1981
2375
  }
1982
2376
  return {
@@ -2047,7 +2441,7 @@ async function renderReactElement(ctx, element, options) {
2047
2441
  }
2048
2442
 
2049
2443
  // src/index.ts
2050
- function createCanvas2(width, height) {
2444
+ function createCanvas3(width, height) {
2051
2445
  const canvas = _createCanvas(width, height);
2052
2446
  const origEncode = canvas.encode.bind(canvas);
2053
2447
  canvas.encode = (async (...args) => Buffer.from(await origEncode(...args)));
@@ -2058,7 +2452,7 @@ export {
2058
2452
  GlobalFonts2 as GlobalFonts,
2059
2453
  Image,
2060
2454
  LottieAnimation,
2061
- createCanvas2 as createCanvas,
2455
+ createCanvas3 as createCanvas,
2062
2456
  loadImage4 as loadImage,
2063
2457
  loadLottie,
2064
2458
  registerFont,