@effing/canvas 0.23.0 → 0.23.2
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/README.md +5 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.js +507 -444
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createCanvas as _createCanvas } from "@napi-rs/canvas";
|
|
|
3
3
|
import {
|
|
4
4
|
Canvas,
|
|
5
5
|
GlobalFonts as GlobalFonts2,
|
|
6
|
-
loadImage as
|
|
6
|
+
loadImage as loadImage3,
|
|
7
7
|
Image
|
|
8
8
|
} from "@napi-rs/canvas";
|
|
9
9
|
|
|
@@ -20,8 +20,17 @@ function renderLottieFrame(ctx, animation, frame) {
|
|
|
20
20
|
animation.render(ctx);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// src/jsx/
|
|
24
|
-
import { loadImage
|
|
23
|
+
// src/jsx/image-cache.ts
|
|
24
|
+
import { loadImage } from "@napi-rs/canvas";
|
|
25
|
+
function cachedLoadImage(cache, src) {
|
|
26
|
+
if (Buffer.isBuffer(src)) return loadImage(src);
|
|
27
|
+
let entry = cache.get(src);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
entry = loadImage(src);
|
|
30
|
+
cache.set(src, entry);
|
|
31
|
+
}
|
|
32
|
+
return entry;
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
// src/jsx/style/compute.ts
|
|
27
36
|
var ROOT_FONT_SIZE = 16;
|
|
@@ -627,6 +636,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
627
636
|
const segments = [];
|
|
628
637
|
let totalHeight = 0;
|
|
629
638
|
let maxLineWidth = 0;
|
|
639
|
+
const isAutoLineHeight = style.lineHeight === void 0 || style.lineHeight === "normal";
|
|
640
|
+
let lastMetrics;
|
|
630
641
|
for (let i = 0; i < lines.length; i++) {
|
|
631
642
|
const line = lines[i];
|
|
632
643
|
const lineWidth = measure(line);
|
|
@@ -644,14 +655,16 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
644
655
|
fontStyle,
|
|
645
656
|
ctx
|
|
646
657
|
);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
658
|
+
lastMetrics = metrics;
|
|
659
|
+
const contentHeight = metrics.ascent + metrics.descent;
|
|
660
|
+
let effectiveAscent = metrics.ascent;
|
|
661
|
+
let effectiveDescent = metrics.descent;
|
|
662
|
+
if (isAutoLineHeight && contentHeight > lineHeightPx) {
|
|
663
|
+
const scale = lineHeightPx / contentHeight;
|
|
664
|
+
effectiveAscent = metrics.ascent * scale;
|
|
665
|
+
effectiveDescent = metrics.descent * scale;
|
|
666
|
+
}
|
|
667
|
+
const baselineY = totalHeight + (lineHeightPx + effectiveAscent - effectiveDescent) / 2;
|
|
655
668
|
segments.push({
|
|
656
669
|
text: line,
|
|
657
670
|
x,
|
|
@@ -673,6 +686,14 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
673
686
|
totalHeight += lineHeightPx;
|
|
674
687
|
maxLineWidth = Math.max(maxLineWidth, lineWidth);
|
|
675
688
|
}
|
|
689
|
+
if (isAutoLineHeight && lastMetrics && segments.length > 0) {
|
|
690
|
+
const lastSeg = segments[segments.length - 1];
|
|
691
|
+
const descentOverflow = lastSeg.y + lastMetrics.descent - totalHeight;
|
|
692
|
+
if (descentOverflow > 0) {
|
|
693
|
+
totalHeight += descentOverflow;
|
|
694
|
+
}
|
|
695
|
+
totalHeight = Math.ceil(totalHeight);
|
|
696
|
+
}
|
|
676
697
|
const textBoxTrim = style.textBoxTrim;
|
|
677
698
|
if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
|
|
678
699
|
const textBoxEdge = style.textBoxEdge ?? "text";
|
|
@@ -1004,9 +1025,6 @@ function splitGradientArgs(content) {
|
|
|
1004
1025
|
return parts;
|
|
1005
1026
|
}
|
|
1006
1027
|
|
|
1007
|
-
// src/jsx/draw/image.ts
|
|
1008
|
-
import { loadImage } from "@napi-rs/canvas";
|
|
1009
|
-
|
|
1010
1028
|
// src/jsx/draw/object-fit.ts
|
|
1011
1029
|
function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
1012
1030
|
const imgRatio = imgW / imgH;
|
|
@@ -1042,8 +1060,8 @@ function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
|
1042
1060
|
}
|
|
1043
1061
|
|
|
1044
1062
|
// src/jsx/draw/image.ts
|
|
1045
|
-
async function drawImage(ctx, src, x, y, width, height,
|
|
1046
|
-
const image =
|
|
1063
|
+
async function drawImage(ctx, src, x, y, width, height, imageCache, style) {
|
|
1064
|
+
const image = await cachedLoadImage(imageCache, src);
|
|
1047
1065
|
const objectFit = style?.objectFit ?? "fill";
|
|
1048
1066
|
if (objectFit === "fill") {
|
|
1049
1067
|
ctx.drawImage(image, x, y, width, height);
|
|
@@ -1359,9 +1377,7 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
|
|
|
1359
1377
|
ctx.restore();
|
|
1360
1378
|
}
|
|
1361
1379
|
|
|
1362
|
-
// src/jsx/draw/svg.ts
|
|
1363
|
-
import { Path2D } from "@napi-rs/canvas";
|
|
1364
|
-
import parseCssColor from "parse-css-color";
|
|
1380
|
+
// src/jsx/draw/svg/style.ts
|
|
1365
1381
|
function resolveCurrentColor(value, color) {
|
|
1366
1382
|
if (value?.toLowerCase() === "currentcolor") return color;
|
|
1367
1383
|
return value;
|
|
@@ -1371,6 +1387,37 @@ function mergeStyleIntoProps(props) {
|
|
|
1371
1387
|
if (!style) return props;
|
|
1372
1388
|
return { ...props, ...style };
|
|
1373
1389
|
}
|
|
1390
|
+
function svgLength(value, reference, fallback = 0) {
|
|
1391
|
+
if (value == null) return fallback;
|
|
1392
|
+
const s = String(value);
|
|
1393
|
+
if (s.endsWith("%")) return parseFloat(s) / 100 * reference;
|
|
1394
|
+
const n = Number(s);
|
|
1395
|
+
return isNaN(n) ? fallback : n;
|
|
1396
|
+
}
|
|
1397
|
+
function parseUrlRef(value) {
|
|
1398
|
+
if (typeof value !== "string") return void 0;
|
|
1399
|
+
const m = value.match(/^url\(#(.+)\)$/);
|
|
1400
|
+
return m?.[1];
|
|
1401
|
+
}
|
|
1402
|
+
function resolveInheritedStyle(merged, color, parent) {
|
|
1403
|
+
return {
|
|
1404
|
+
fill: resolveCurrentColor(merged.fill, color) ?? parent?.fill ?? "black",
|
|
1405
|
+
stroke: resolveCurrentColor(merged.stroke, color) ?? parent?.stroke,
|
|
1406
|
+
strokeWidth: merged.strokeWidth ?? merged["stroke-width"] ?? parent?.strokeWidth,
|
|
1407
|
+
strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"] ?? parent?.strokeLinecap,
|
|
1408
|
+
strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? parent?.strokeLinejoin,
|
|
1409
|
+
strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? parent?.strokeOpacity
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/jsx/draw/svg/tree.ts
|
|
1414
|
+
function normalizeChildren(node) {
|
|
1415
|
+
const raw = node.children ?? node.props.children;
|
|
1416
|
+
if (raw == null) return [];
|
|
1417
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/jsx/draw/svg/defs.ts
|
|
1374
1421
|
function collectDefs(children) {
|
|
1375
1422
|
const clips = /* @__PURE__ */ new Map();
|
|
1376
1423
|
const gradients = /* @__PURE__ */ new Map();
|
|
@@ -1394,122 +1441,85 @@ function collectDefs(children) {
|
|
|
1394
1441
|
}
|
|
1395
1442
|
return { clips, gradients, masks, filters };
|
|
1396
1443
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const n = Number(s);
|
|
1412
|
-
return isNaN(n) ? fallback : n;
|
|
1413
|
-
}
|
|
1414
|
-
function parseFrac(value, fallback) {
|
|
1415
|
-
if (value == null) return fallback;
|
|
1416
|
-
const s = String(value);
|
|
1417
|
-
if (s.endsWith("%")) return parseFloat(s) / 100;
|
|
1418
|
-
return Number(s);
|
|
1419
|
-
}
|
|
1420
|
-
function applyOpacity(color, opacity) {
|
|
1421
|
-
if (opacity >= 1) return color;
|
|
1422
|
-
const parsed = parseCssColor(color);
|
|
1423
|
-
if (!parsed) return color;
|
|
1424
|
-
const [r, g, b] = parsed.values;
|
|
1425
|
-
return `rgba(${r}, ${g}, ${b}, ${parsed.alpha * opacity})`;
|
|
1426
|
-
}
|
|
1427
|
-
function addGradientStops(gradient, def) {
|
|
1428
|
-
for (const stop of normalizeChildren(def)) {
|
|
1429
|
-
if (stop.type !== "stop") continue;
|
|
1430
|
-
const sp = stop.props;
|
|
1431
|
-
const offsetRaw = sp.offset ?? 0;
|
|
1432
|
-
const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
|
|
1433
|
-
const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
|
|
1434
|
-
const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
|
|
1435
|
-
gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
|
|
1439
|
-
const props = def.props;
|
|
1440
|
-
if (def.type === "linearGradient") {
|
|
1441
|
-
const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
|
|
1442
|
-
const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
|
|
1443
|
-
const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
|
|
1444
|
-
const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
|
|
1445
|
-
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
1446
|
-
addGradientStops(gradient, def);
|
|
1447
|
-
ctx.fillStyle = gradient;
|
|
1448
|
-
ctx.fill(path, fillRule);
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
if (def.type === "radialGradient") {
|
|
1452
|
-
const cxF = parseFrac(props.cx, 0.5);
|
|
1453
|
-
const cyF = parseFrac(props.cy, 0.5);
|
|
1454
|
-
const rF = parseFrac(props.r, 0.5);
|
|
1455
|
-
const fxF = parseFrac(props.fx, cxF);
|
|
1456
|
-
const fyF = parseFrac(props.fy, cyF);
|
|
1457
|
-
ctx.save();
|
|
1458
|
-
ctx.translate(bbox.x, bbox.y);
|
|
1459
|
-
ctx.scale(bbox.width, bbox.height);
|
|
1460
|
-
const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
|
|
1461
|
-
addGradientStops(gradient, def);
|
|
1462
|
-
ctx.fillStyle = gradient;
|
|
1463
|
-
const unitPath = new Path2D();
|
|
1464
|
-
const invTransform = {
|
|
1465
|
-
a: 1 / bbox.width,
|
|
1466
|
-
b: 0,
|
|
1467
|
-
c: 0,
|
|
1468
|
-
d: 1 / bbox.height,
|
|
1469
|
-
e: -bbox.x / bbox.width,
|
|
1470
|
-
f: -bbox.y / bbox.height
|
|
1471
|
-
};
|
|
1472
|
-
unitPath.addPath(path, invTransform);
|
|
1473
|
-
ctx.fill(unitPath, fillRule);
|
|
1474
|
-
ctx.restore();
|
|
1475
|
-
return true;
|
|
1476
|
-
}
|
|
1477
|
-
return false;
|
|
1444
|
+
var EMPTY_DEFS = {
|
|
1445
|
+
clips: /* @__PURE__ */ new Map(),
|
|
1446
|
+
gradients: /* @__PURE__ */ new Map(),
|
|
1447
|
+
masks: /* @__PURE__ */ new Map(),
|
|
1448
|
+
filters: /* @__PURE__ */ new Map()
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// src/jsx/draw/svg/transform.ts
|
|
1452
|
+
function applyTransformAndOpacity(ctx, transform, opacity) {
|
|
1453
|
+
if (!transform && opacity >= 1) return false;
|
|
1454
|
+
ctx.save();
|
|
1455
|
+
if (transform) applySvgTransform(ctx, transform);
|
|
1456
|
+
if (opacity < 1) ctx.globalAlpha *= opacity;
|
|
1457
|
+
return true;
|
|
1478
1458
|
}
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1459
|
+
function applySvgTransform(ctx, transform) {
|
|
1460
|
+
const re = /\b(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)/g;
|
|
1461
|
+
let match;
|
|
1462
|
+
while ((match = re.exec(transform)) !== null) {
|
|
1463
|
+
const fn = match[1];
|
|
1464
|
+
const args = match[2].split(/[\s,]+/).filter(Boolean).map(Number);
|
|
1465
|
+
switch (fn) {
|
|
1466
|
+
case "translate":
|
|
1467
|
+
ctx.translate(args[0] ?? 0, args[1] ?? 0);
|
|
1468
|
+
break;
|
|
1469
|
+
case "scale": {
|
|
1470
|
+
const sx = args[0] ?? 1;
|
|
1471
|
+
ctx.scale(sx, args[1] ?? sx);
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
case "rotate": {
|
|
1475
|
+
const angle = (args[0] ?? 0) * Math.PI / 180;
|
|
1476
|
+
if (args.length >= 3) {
|
|
1477
|
+
const cx = args[1];
|
|
1478
|
+
const cy = args[2];
|
|
1479
|
+
ctx.translate(cx, cy);
|
|
1480
|
+
ctx.rotate(angle);
|
|
1481
|
+
ctx.translate(-cx, -cy);
|
|
1482
|
+
} else {
|
|
1483
|
+
ctx.rotate(angle);
|
|
1484
|
+
}
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
case "skewX":
|
|
1488
|
+
ctx.transform(
|
|
1489
|
+
1,
|
|
1490
|
+
0,
|
|
1491
|
+
Math.tan((args[0] ?? 0) * Math.PI / 180),
|
|
1492
|
+
1,
|
|
1493
|
+
0,
|
|
1494
|
+
0
|
|
1495
|
+
);
|
|
1496
|
+
break;
|
|
1497
|
+
case "skewY":
|
|
1498
|
+
ctx.transform(
|
|
1499
|
+
1,
|
|
1500
|
+
Math.tan((args[0] ?? 0) * Math.PI / 180),
|
|
1501
|
+
0,
|
|
1502
|
+
1,
|
|
1503
|
+
0,
|
|
1504
|
+
0
|
|
1505
|
+
);
|
|
1506
|
+
break;
|
|
1507
|
+
case "matrix":
|
|
1508
|
+
ctx.transform(
|
|
1509
|
+
args[0] ?? 1,
|
|
1510
|
+
args[1] ?? 0,
|
|
1511
|
+
args[2] ?? 0,
|
|
1512
|
+
args[3] ?? 1,
|
|
1513
|
+
args[4] ?? 0,
|
|
1514
|
+
args[5] ?? 0
|
|
1515
|
+
);
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1510
1518
|
}
|
|
1511
|
-
return false;
|
|
1512
1519
|
}
|
|
1520
|
+
|
|
1521
|
+
// src/jsx/draw/svg/path.ts
|
|
1522
|
+
import { Path2D } from "@napi-rs/canvas";
|
|
1513
1523
|
function pathBBox(d) {
|
|
1514
1524
|
let minX = Infinity;
|
|
1515
1525
|
let minY = Infinity;
|
|
@@ -1739,53 +1749,17 @@ function buildClipPath(shapes, vbW, vbH) {
|
|
|
1739
1749
|
}
|
|
1740
1750
|
return combined;
|
|
1741
1751
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const
|
|
1746
|
-
let
|
|
1747
|
-
|
|
1748
|
-
if (viewBox) {
|
|
1749
|
-
const parts = viewBox.split(/[\s,]+/).map(Number);
|
|
1750
|
-
if (parts.length === 4) {
|
|
1751
|
-
const [vbX, vbY, parsedW, parsedH] = parts;
|
|
1752
|
-
vbW = parsedW;
|
|
1753
|
-
vbH = parsedH;
|
|
1754
|
-
const scaleX = width / vbW;
|
|
1755
|
-
const scaleY = height / vbH;
|
|
1756
|
-
ctx.scale(scaleX, scaleY);
|
|
1757
|
-
ctx.translate(-vbX, -vbY);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
const color = node.style.color ?? "black";
|
|
1761
|
-
const merged = mergeStyleIntoProps(node.props);
|
|
1762
|
-
const inherited = {
|
|
1763
|
-
fill: resolveCurrentColor(merged.fill, color) ?? "black",
|
|
1764
|
-
stroke: resolveCurrentColor(merged.stroke, color),
|
|
1765
|
-
strokeWidth: merged.strokeWidth ?? merged["stroke-width"],
|
|
1766
|
-
strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"],
|
|
1767
|
-
strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"],
|
|
1768
|
-
strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"]
|
|
1769
|
-
};
|
|
1770
|
-
const children = node.props.children;
|
|
1771
|
-
if (children != null) {
|
|
1772
|
-
const childArray = Array.isArray(children) ? children : [children];
|
|
1773
|
-
const svgChildren = childArray.filter(
|
|
1774
|
-
(c) => c != null && typeof c === "object"
|
|
1775
|
-
);
|
|
1776
|
-
const defs = collectDefs(svgChildren);
|
|
1777
|
-
for (const child of svgChildren) {
|
|
1778
|
-
drawSvgChild(ctx, child, inherited, color, defs, vbW, vbH);
|
|
1779
|
-
}
|
|
1752
|
+
function parsePoints(value) {
|
|
1753
|
+
if (!value) return [];
|
|
1754
|
+
const nums = value.trim().split(/[\s,]+/).map(Number);
|
|
1755
|
+
const result = [];
|
|
1756
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
1757
|
+
result.push([nums[i], nums[i + 1]]);
|
|
1780
1758
|
}
|
|
1781
|
-
|
|
1759
|
+
return result;
|
|
1782
1760
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
gradients: /* @__PURE__ */ new Map(),
|
|
1786
|
-
masks: /* @__PURE__ */ new Map(),
|
|
1787
|
-
filters: /* @__PURE__ */ new Map()
|
|
1788
|
-
};
|
|
1761
|
+
|
|
1762
|
+
// src/jsx/draw/svg/filters.ts
|
|
1789
1763
|
function applyFeOffset(input, dx, dy, w, h) {
|
|
1790
1764
|
const [canvas, ctx] = acquireOffscreen(w, h);
|
|
1791
1765
|
ctx.drawImage(input, dx, dy);
|
|
@@ -1845,18 +1819,18 @@ function applyFeColorMatrix(input, type, values, w, h) {
|
|
|
1845
1819
|
}
|
|
1846
1820
|
return canvas;
|
|
1847
1821
|
}
|
|
1822
|
+
var BLEND_MODE_MAP = {
|
|
1823
|
+
normal: "source-over",
|
|
1824
|
+
multiply: "multiply",
|
|
1825
|
+
screen: "screen",
|
|
1826
|
+
darken: "darken",
|
|
1827
|
+
lighten: "lighten",
|
|
1828
|
+
overlay: "overlay"
|
|
1829
|
+
};
|
|
1848
1830
|
function applyFeBlend(in1, in2, mode, w, h) {
|
|
1849
|
-
const modeMap = {
|
|
1850
|
-
normal: "source-over",
|
|
1851
|
-
multiply: "multiply",
|
|
1852
|
-
screen: "screen",
|
|
1853
|
-
darken: "darken",
|
|
1854
|
-
lighten: "lighten",
|
|
1855
|
-
overlay: "overlay"
|
|
1856
|
-
};
|
|
1857
1831
|
const [canvas, ctx] = acquireOffscreen(w, h);
|
|
1858
1832
|
ctx.drawImage(in2, 0, 0);
|
|
1859
|
-
ctx.globalCompositeOperation =
|
|
1833
|
+
ctx.globalCompositeOperation = BLEND_MODE_MAP[mode] ?? "source-over";
|
|
1860
1834
|
ctx.drawImage(in1, 0, 0);
|
|
1861
1835
|
ctx.globalCompositeOperation = "source-over";
|
|
1862
1836
|
return canvas;
|
|
@@ -1932,103 +1906,181 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
|
|
|
1932
1906
|
}
|
|
1933
1907
|
return lastResult;
|
|
1934
1908
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1909
|
+
|
|
1910
|
+
// src/jsx/draw/svg/shapes.ts
|
|
1911
|
+
import { Path2D as Path2D3 } from "@napi-rs/canvas";
|
|
1912
|
+
|
|
1913
|
+
// src/jsx/draw/svg/gradients.ts
|
|
1914
|
+
import { Path2D as Path2D2 } from "@napi-rs/canvas";
|
|
1915
|
+
import parseCssColor from "parse-css-color";
|
|
1916
|
+
function parseFrac(value, fallback) {
|
|
1917
|
+
if (value == null) return fallback;
|
|
1918
|
+
const s = String(value);
|
|
1919
|
+
if (s.endsWith("%")) return parseFloat(s) / 100;
|
|
1920
|
+
return Number(s);
|
|
1921
|
+
}
|
|
1922
|
+
function applyOpacity(color, opacity) {
|
|
1923
|
+
if (opacity >= 1) return color;
|
|
1924
|
+
const parsed = parseCssColor(color);
|
|
1925
|
+
if (!parsed) return color;
|
|
1926
|
+
const [r, g, b] = parsed.values;
|
|
1927
|
+
return `rgba(${r}, ${g}, ${b}, ${parsed.alpha * opacity})`;
|
|
1928
|
+
}
|
|
1929
|
+
function addGradientStops(gradient, def) {
|
|
1930
|
+
for (const stop of normalizeChildren(def)) {
|
|
1931
|
+
if (stop.type !== "stop") continue;
|
|
1932
|
+
const sp = stop.props;
|
|
1933
|
+
const offsetRaw = sp.offset ?? 0;
|
|
1934
|
+
const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
|
|
1935
|
+
const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
|
|
1936
|
+
const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
|
|
1937
|
+
gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function createLinearGradientFromBBox(ctx, props, bbox, def) {
|
|
1941
|
+
const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
|
|
1942
|
+
const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
|
|
1943
|
+
const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
|
|
1944
|
+
const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
|
|
1945
|
+
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
1946
|
+
addGradientStops(gradient, def);
|
|
1947
|
+
return gradient;
|
|
1948
|
+
}
|
|
1949
|
+
function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
|
|
1950
|
+
const props = def.props;
|
|
1951
|
+
if (def.type === "linearGradient") {
|
|
1952
|
+
ctx.fillStyle = createLinearGradientFromBBox(ctx, props, bbox, def);
|
|
1953
|
+
ctx.fill(path, fillRule);
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
if (def.type === "radialGradient") {
|
|
1957
|
+
const cxF = parseFrac(props.cx, 0.5);
|
|
1958
|
+
const cyF = parseFrac(props.cy, 0.5);
|
|
1959
|
+
const rF = parseFrac(props.r, 0.5);
|
|
1960
|
+
const fxF = parseFrac(props.fx, cxF);
|
|
1961
|
+
const fyF = parseFrac(props.fy, cyF);
|
|
1943
1962
|
ctx.save();
|
|
1944
|
-
|
|
1945
|
-
|
|
1963
|
+
ctx.translate(bbox.x, bbox.y);
|
|
1964
|
+
ctx.scale(bbox.width, bbox.height);
|
|
1965
|
+
const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
|
|
1966
|
+
addGradientStops(gradient, def);
|
|
1967
|
+
ctx.fillStyle = gradient;
|
|
1968
|
+
const unitPath = new Path2D2();
|
|
1969
|
+
const invTransform = {
|
|
1970
|
+
a: 1 / bbox.width,
|
|
1971
|
+
b: 0,
|
|
1972
|
+
c: 0,
|
|
1973
|
+
d: 1 / bbox.height,
|
|
1974
|
+
e: -bbox.x / bbox.width,
|
|
1975
|
+
f: -bbox.y / bbox.height
|
|
1976
|
+
};
|
|
1977
|
+
unitPath.addPath(path, invTransform);
|
|
1978
|
+
ctx.fill(unitPath, fillRule);
|
|
1979
|
+
ctx.restore();
|
|
1980
|
+
return true;
|
|
1946
1981
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
if (clipPath) ctx.clip(clipPath);
|
|
1956
|
-
const needsOffscreen = maskShapes || filterDef;
|
|
1957
|
-
let elemCanvas;
|
|
1958
|
-
const targetCtx = needsOffscreen ? (() => {
|
|
1959
|
-
const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
|
|
1960
|
-
elemCanvas = c;
|
|
1961
|
-
return cCtx;
|
|
1962
|
-
})() : ctx;
|
|
1963
|
-
switch (type) {
|
|
1964
|
-
case "path":
|
|
1965
|
-
drawPath(targetCtx, props, inherited, color, defs);
|
|
1966
|
-
break;
|
|
1967
|
-
case "circle":
|
|
1968
|
-
drawCircle(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
1969
|
-
break;
|
|
1970
|
-
case "rect":
|
|
1971
|
-
drawSvgRect(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
1972
|
-
break;
|
|
1973
|
-
case "line":
|
|
1974
|
-
drawLine(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
1975
|
-
break;
|
|
1976
|
-
case "ellipse":
|
|
1977
|
-
drawEllipse(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
1978
|
-
break;
|
|
1979
|
-
case "polygon":
|
|
1980
|
-
drawPolygon(targetCtx, props, inherited, color, defs);
|
|
1981
|
-
break;
|
|
1982
|
-
case "polyline":
|
|
1983
|
-
drawPolyline(targetCtx, props, inherited, color, defs);
|
|
1984
|
-
break;
|
|
1985
|
-
case "g":
|
|
1986
|
-
drawGroup(targetCtx, child, inherited, color, defs, vbW, vbH);
|
|
1987
|
-
break;
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
function strokeWithSvgGradient(ctx, def, bbox, path) {
|
|
1985
|
+
const props = def.props;
|
|
1986
|
+
if (def.type === "linearGradient") {
|
|
1987
|
+
ctx.strokeStyle = createLinearGradientFromBBox(ctx, props, bbox, def);
|
|
1988
|
+
ctx.stroke(path);
|
|
1989
|
+
return true;
|
|
1988
1990
|
}
|
|
1989
|
-
if (
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1991
|
+
if (def.type === "radialGradient") {
|
|
1992
|
+
const cxAbs = bbox.x + parseFrac(props.cx, 0.5) * bbox.width;
|
|
1993
|
+
const cyAbs = bbox.y + parseFrac(props.cy, 0.5) * bbox.height;
|
|
1994
|
+
const rAbs = parseFrac(props.r, 0.5) * Math.sqrt(bbox.width * bbox.height);
|
|
1995
|
+
const fxAbs = bbox.x + parseFrac(props.fx, 0.5) * bbox.width;
|
|
1996
|
+
const fyAbs = bbox.y + parseFrac(props.fy, 0.5) * bbox.height;
|
|
1997
|
+
const gradient = ctx.createRadialGradient(
|
|
1998
|
+
fxAbs,
|
|
1999
|
+
fyAbs,
|
|
2000
|
+
0,
|
|
2001
|
+
cxAbs,
|
|
2002
|
+
cyAbs,
|
|
2003
|
+
rAbs
|
|
2004
|
+
);
|
|
2005
|
+
addGradientStops(gradient, def);
|
|
2006
|
+
ctx.strokeStyle = gradient;
|
|
2007
|
+
ctx.stroke(path);
|
|
2008
|
+
return true;
|
|
2009
|
+
}
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// src/jsx/draw/svg/paint.ts
|
|
2014
|
+
function applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox) {
|
|
2015
|
+
const fill = resolveCurrentColor(props.fill, color) ?? inherited.fill;
|
|
2016
|
+
const fillRule = props.fillRule ?? props["fill-rule"];
|
|
2017
|
+
const fillOpacity = Number(props.fillOpacity ?? props["fill-opacity"] ?? 1);
|
|
2018
|
+
const fillRef = parseUrlRef(fill);
|
|
2019
|
+
if (fillRef) {
|
|
2020
|
+
const gradientDef = defs.gradients.get(fillRef);
|
|
2021
|
+
if (gradientDef) {
|
|
2022
|
+
if (fillOpacity < 1) {
|
|
2023
|
+
ctx.save();
|
|
2024
|
+
ctx.globalAlpha *= fillOpacity;
|
|
2025
|
+
fillWithSvgGradient(
|
|
2026
|
+
ctx,
|
|
2027
|
+
gradientDef,
|
|
2028
|
+
bbox,
|
|
2029
|
+
path,
|
|
2030
|
+
fillRule ?? "nonzero"
|
|
2031
|
+
);
|
|
2032
|
+
ctx.restore();
|
|
2033
|
+
} else {
|
|
2034
|
+
fillWithSvgGradient(
|
|
2035
|
+
ctx,
|
|
2036
|
+
gradientDef,
|
|
2037
|
+
bbox,
|
|
2038
|
+
path,
|
|
2039
|
+
fillRule ?? "nonzero"
|
|
2004
2040
|
);
|
|
2005
2041
|
}
|
|
2006
|
-
targetCtx.globalCompositeOperation = "destination-in";
|
|
2007
|
-
targetCtx.drawImage(maskCanvas, 0, 0);
|
|
2008
|
-
targetCtx.globalCompositeOperation = "source-over";
|
|
2009
|
-
releaseOffscreen(maskCanvas);
|
|
2010
|
-
}
|
|
2011
|
-
if (filterDef) {
|
|
2012
|
-
const filteredCanvas = applyFilter(
|
|
2013
|
-
filterDef,
|
|
2014
|
-
elemCanvas,
|
|
2015
|
-
Math.ceil(vbW),
|
|
2016
|
-
Math.ceil(vbH)
|
|
2017
|
-
);
|
|
2018
|
-
ctx.drawImage(filteredCanvas, 0, 0);
|
|
2019
|
-
if (filteredCanvas !== elemCanvas) releaseOffscreen(filteredCanvas);
|
|
2020
|
-
} else {
|
|
2021
|
-
ctx.drawImage(elemCanvas, 0, 0);
|
|
2022
2042
|
}
|
|
2023
|
-
|
|
2043
|
+
} else if (fill !== "none") {
|
|
2044
|
+
ctx.fillStyle = applyOpacity(fill, fillOpacity);
|
|
2045
|
+
ctx.fill(path, fillRule ?? "nonzero");
|
|
2024
2046
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2047
|
+
applyStroke(ctx, props, path, inherited, color, defs, bbox);
|
|
2048
|
+
}
|
|
2049
|
+
function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
|
|
2050
|
+
const stroke = resolveCurrentColor(props.stroke, color) ?? inherited.stroke;
|
|
2051
|
+
if (!stroke || stroke === "none") return;
|
|
2052
|
+
const strokeOpacity = Number(
|
|
2053
|
+
props.strokeOpacity ?? props["stroke-opacity"] ?? inherited.strokeOpacity ?? 1
|
|
2054
|
+
);
|
|
2055
|
+
ctx.lineWidth = Number(
|
|
2056
|
+
props.strokeWidth ?? props["stroke-width"] ?? inherited.strokeWidth ?? 1
|
|
2057
|
+
);
|
|
2058
|
+
ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? inherited.strokeLinecap ?? "butt";
|
|
2059
|
+
ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? inherited.strokeLinejoin ?? "miter";
|
|
2060
|
+
const strokeRef = parseUrlRef(stroke);
|
|
2061
|
+
if (strokeRef) {
|
|
2062
|
+
const gradientDef = defs.gradients.get(strokeRef);
|
|
2063
|
+
if (gradientDef) {
|
|
2064
|
+
if (strokeOpacity < 1) {
|
|
2065
|
+
ctx.save();
|
|
2066
|
+
ctx.globalAlpha *= strokeOpacity;
|
|
2067
|
+
strokeWithSvgGradient(ctx, gradientDef, bbox, path);
|
|
2068
|
+
ctx.restore();
|
|
2069
|
+
} else {
|
|
2070
|
+
strokeWithSvgGradient(ctx, gradientDef, bbox, path);
|
|
2071
|
+
}
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
ctx.strokeStyle = applyOpacity(stroke, strokeOpacity);
|
|
2076
|
+
ctx.stroke(path);
|
|
2027
2077
|
}
|
|
2078
|
+
|
|
2079
|
+
// src/jsx/draw/svg/shapes.ts
|
|
2028
2080
|
function drawPath(ctx, props, inherited, color, defs) {
|
|
2029
2081
|
const d = props.d;
|
|
2030
2082
|
if (!d) return;
|
|
2031
|
-
const path = new
|
|
2083
|
+
const path = new Path2D3(d);
|
|
2032
2084
|
const bbox = pathBBox(d);
|
|
2033
2085
|
applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
|
|
2034
2086
|
}
|
|
@@ -2037,7 +2089,7 @@ function drawCircle(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
|
|
|
2037
2089
|
const cy = svgLength(props.cy, vbH);
|
|
2038
2090
|
const r = svgLength(props.r, Math.min(vbW, vbH));
|
|
2039
2091
|
if (r <= 0) return;
|
|
2040
|
-
const path = new
|
|
2092
|
+
const path = new Path2D3();
|
|
2041
2093
|
path.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2042
2094
|
const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
|
|
2043
2095
|
applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
|
|
@@ -2054,7 +2106,7 @@ function drawSvgRect(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
|
|
|
2054
2106
|
let ry = ryRaw >= 0 ? ryRaw : rx;
|
|
2055
2107
|
rx = Math.min(rx, w / 2);
|
|
2056
2108
|
ry = Math.min(ry, h / 2);
|
|
2057
|
-
const path = new
|
|
2109
|
+
const path = new Path2D3();
|
|
2058
2110
|
if (rx > 0 || ry > 0) {
|
|
2059
2111
|
path.roundRect(x, y, w, h, [rx]);
|
|
2060
2112
|
} else {
|
|
@@ -2068,7 +2120,7 @@ function drawLine(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
|
|
|
2068
2120
|
const y1 = svgLength(props.y1, vbH);
|
|
2069
2121
|
const x2 = svgLength(props.x2, vbW);
|
|
2070
2122
|
const y2 = svgLength(props.y2, vbH);
|
|
2071
|
-
const path = new
|
|
2123
|
+
const path = new Path2D3();
|
|
2072
2124
|
path.moveTo(x1, y1);
|
|
2073
2125
|
path.lineTo(x2, y2);
|
|
2074
2126
|
const bbox = {
|
|
@@ -2085,7 +2137,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
|
|
|
2085
2137
|
const rx = svgLength(props.rx, vbW);
|
|
2086
2138
|
const ry = svgLength(props.ry, vbH);
|
|
2087
2139
|
if (rx <= 0 || ry <= 0) return;
|
|
2088
|
-
const path = new
|
|
2140
|
+
const path = new Path2D3();
|
|
2089
2141
|
path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
2090
2142
|
const bbox = {
|
|
2091
2143
|
x: cx - rx,
|
|
@@ -2098,7 +2150,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
|
|
|
2098
2150
|
function drawPolygon(ctx, props, inherited, color, defs) {
|
|
2099
2151
|
const points = parsePoints(props.points);
|
|
2100
2152
|
if (points.length < 2) return;
|
|
2101
|
-
const path = new
|
|
2153
|
+
const path = new Path2D3();
|
|
2102
2154
|
path.moveTo(points[0][0], points[0][1]);
|
|
2103
2155
|
for (let i = 1; i < points.length; i++) {
|
|
2104
2156
|
path.lineTo(points[i][0], points[i][1]);
|
|
@@ -2110,7 +2162,7 @@ function drawPolygon(ctx, props, inherited, color, defs) {
|
|
|
2110
2162
|
function drawPolyline(ctx, props, inherited, color, defs) {
|
|
2111
2163
|
const points = parsePoints(props.points);
|
|
2112
2164
|
if (points.length < 2) return;
|
|
2113
|
-
const path = new
|
|
2165
|
+
const path = new Path2D3();
|
|
2114
2166
|
path.moveTo(points[0][0], points[0][1]);
|
|
2115
2167
|
for (let i = 1; i < points.length; i++) {
|
|
2116
2168
|
path.lineTo(points[i][0], points[i][1]);
|
|
@@ -2118,167 +2170,156 @@ function drawPolyline(ctx, props, inherited, color, defs) {
|
|
|
2118
2170
|
const bbox = pointsBBox(points);
|
|
2119
2171
|
applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
|
|
2120
2172
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2173
|
+
|
|
2174
|
+
// src/jsx/draw/svg/traverse.ts
|
|
2175
|
+
function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
|
|
2176
|
+
const { type } = child;
|
|
2177
|
+
const props = mergeStyleIntoProps(child.props);
|
|
2178
|
+
if (type === "defs" || type === "clipPath" || type === "mask" || type === "filter")
|
|
2179
|
+
return;
|
|
2180
|
+
const transform = type !== "g" ? props.transform : void 0;
|
|
2181
|
+
const opacity = type !== "g" ? Number(props.opacity ?? 1) : 1;
|
|
2182
|
+
const savedTransform = applyTransformAndOpacity(ctx, transform, opacity);
|
|
2183
|
+
const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
|
|
2184
|
+
const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
|
|
2185
|
+
const clipPath = clipShapes ? buildClipPath(clipShapes, vbW, vbH) : void 0;
|
|
2186
|
+
const maskRef = parseUrlRef(props.mask);
|
|
2187
|
+
const maskShapes = maskRef ? defs.masks.get(maskRef) : void 0;
|
|
2188
|
+
const filterRef = parseUrlRef(props.filter);
|
|
2189
|
+
const filterDef = filterRef ? defs.filters.get(filterRef) : void 0;
|
|
2190
|
+
if (clipPath) {
|
|
2191
|
+
ctx.save();
|
|
2192
|
+
ctx.clip(clipPath);
|
|
2193
|
+
}
|
|
2194
|
+
const needsOffscreen = maskShapes || filterDef;
|
|
2195
|
+
let elemCanvas;
|
|
2196
|
+
let targetCtx;
|
|
2197
|
+
if (needsOffscreen) {
|
|
2198
|
+
const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
|
|
2199
|
+
elemCanvas = c;
|
|
2200
|
+
targetCtx = cCtx;
|
|
2201
|
+
} else {
|
|
2202
|
+
targetCtx = ctx;
|
|
2203
|
+
}
|
|
2204
|
+
switch (type) {
|
|
2205
|
+
case "path":
|
|
2206
|
+
drawPath(targetCtx, props, inherited, color, defs);
|
|
2207
|
+
break;
|
|
2208
|
+
case "circle":
|
|
2209
|
+
drawCircle(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
2210
|
+
break;
|
|
2211
|
+
case "rect":
|
|
2212
|
+
drawSvgRect(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
2213
|
+
break;
|
|
2214
|
+
case "line":
|
|
2215
|
+
drawLine(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
2216
|
+
break;
|
|
2217
|
+
case "ellipse":
|
|
2218
|
+
drawEllipse(targetCtx, props, inherited, color, defs, vbW, vbH);
|
|
2219
|
+
break;
|
|
2220
|
+
case "polygon":
|
|
2221
|
+
drawPolygon(targetCtx, props, inherited, color, defs);
|
|
2222
|
+
break;
|
|
2223
|
+
case "polyline":
|
|
2224
|
+
drawPolyline(targetCtx, props, inherited, color, defs);
|
|
2225
|
+
break;
|
|
2226
|
+
case "g":
|
|
2227
|
+
drawGroup(targetCtx, child, props, inherited, color, defs, vbW, vbH);
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
if (needsOffscreen && elemCanvas) {
|
|
2231
|
+
applyOffscreenEffects(
|
|
2232
|
+
ctx,
|
|
2233
|
+
elemCanvas,
|
|
2234
|
+
targetCtx,
|
|
2235
|
+
maskShapes,
|
|
2236
|
+
filterDef,
|
|
2237
|
+
color,
|
|
2238
|
+
defs,
|
|
2239
|
+
vbW,
|
|
2240
|
+
vbH
|
|
2241
|
+
);
|
|
2180
2242
|
}
|
|
2243
|
+
if (clipPath) ctx.restore();
|
|
2244
|
+
if (savedTransform) ctx.restore();
|
|
2181
2245
|
}
|
|
2182
|
-
function drawGroup(ctx, node, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
|
|
2246
|
+
function drawGroup(ctx, node, mergedProps, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
|
|
2183
2247
|
const children = normalizeChildren(node);
|
|
2184
2248
|
if (children.length === 0) return;
|
|
2185
|
-
const
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
strokeWidth: merged.strokeWidth ?? merged["stroke-width"] ?? inherited.strokeWidth,
|
|
2190
|
-
strokeLinecap: merged.strokeLinecap ?? merged["stroke-linecap"] ?? inherited.strokeLinecap,
|
|
2191
|
-
strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? inherited.strokeLinejoin,
|
|
2192
|
-
strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? inherited.strokeOpacity
|
|
2193
|
-
};
|
|
2194
|
-
const opacity = Number(merged.opacity ?? 1);
|
|
2195
|
-
const transform = merged.transform;
|
|
2196
|
-
if (transform || opacity < 1) {
|
|
2197
|
-
ctx.save();
|
|
2198
|
-
if (transform) applySvgTransform(ctx, transform);
|
|
2199
|
-
if (opacity < 1) ctx.globalAlpha *= opacity;
|
|
2200
|
-
}
|
|
2249
|
+
const groupInherited = resolveInheritedStyle(mergedProps, color, inherited);
|
|
2250
|
+
const opacity = Number(mergedProps.opacity ?? 1);
|
|
2251
|
+
const transform = mergedProps.transform;
|
|
2252
|
+
const saved = applyTransformAndOpacity(ctx, transform, opacity);
|
|
2201
2253
|
for (const child of children) {
|
|
2202
2254
|
if (child != null && typeof child === "object") {
|
|
2203
2255
|
drawSvgChild(ctx, child, groupInherited, color, defs, vbW, vbH);
|
|
2204
2256
|
}
|
|
2205
2257
|
}
|
|
2206
|
-
if (
|
|
2207
|
-
ctx.restore();
|
|
2208
|
-
}
|
|
2258
|
+
if (saved) ctx.restore();
|
|
2209
2259
|
}
|
|
2210
|
-
function
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2260
|
+
function applyOffscreenEffects(ctx, elemCanvas, targetCtx, maskShapes, filterDef, color, defs, vbW, vbH) {
|
|
2261
|
+
if (maskShapes) {
|
|
2262
|
+
const [maskCanvas, maskCtx] = acquireOffscreen(
|
|
2263
|
+
Math.ceil(vbW),
|
|
2264
|
+
Math.ceil(vbH)
|
|
2265
|
+
);
|
|
2266
|
+
for (const shape of maskShapes) {
|
|
2267
|
+
drawSvgChild(maskCtx, shape, { fill: "white" }, "white", defs, vbW, vbH);
|
|
2268
|
+
}
|
|
2269
|
+
targetCtx.globalCompositeOperation = "destination-in";
|
|
2270
|
+
targetCtx.drawImage(maskCanvas, 0, 0);
|
|
2271
|
+
targetCtx.globalCompositeOperation = "source-over";
|
|
2272
|
+
releaseOffscreen(maskCanvas);
|
|
2273
|
+
}
|
|
2274
|
+
if (filterDef) {
|
|
2275
|
+
const filteredCanvas = applyFilter(
|
|
2276
|
+
filterDef,
|
|
2277
|
+
elemCanvas,
|
|
2278
|
+
Math.ceil(vbW),
|
|
2279
|
+
Math.ceil(vbH)
|
|
2280
|
+
);
|
|
2281
|
+
ctx.drawImage(filteredCanvas, 0, 0);
|
|
2282
|
+
if (filteredCanvas !== elemCanvas) releaseOffscreen(filteredCanvas);
|
|
2283
|
+
} else {
|
|
2284
|
+
ctx.drawImage(elemCanvas, 0, 0);
|
|
2216
2285
|
}
|
|
2217
|
-
|
|
2286
|
+
releaseOffscreen(elemCanvas);
|
|
2218
2287
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
ctx.restore();
|
|
2238
|
-
} else {
|
|
2239
|
-
fillWithSvgGradient(
|
|
2240
|
-
ctx,
|
|
2241
|
-
gradientDef,
|
|
2242
|
-
bbox,
|
|
2243
|
-
path,
|
|
2244
|
-
fillRule ?? "nonzero"
|
|
2245
|
-
);
|
|
2246
|
-
}
|
|
2288
|
+
|
|
2289
|
+
// src/jsx/draw/svg/index.ts
|
|
2290
|
+
function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
2291
|
+
ctx.save();
|
|
2292
|
+
ctx.translate(x, y);
|
|
2293
|
+
const viewBox = node.props.viewBox;
|
|
2294
|
+
let vbW = width;
|
|
2295
|
+
let vbH = height;
|
|
2296
|
+
if (viewBox) {
|
|
2297
|
+
const parts = viewBox.split(/[\s,]+/).map(Number);
|
|
2298
|
+
if (parts.length === 4) {
|
|
2299
|
+
const [vbX, vbY, parsedW, parsedH] = parts;
|
|
2300
|
+
vbW = parsedW;
|
|
2301
|
+
vbH = parsedH;
|
|
2302
|
+
const scaleX = width / vbW;
|
|
2303
|
+
const scaleY = height / vbH;
|
|
2304
|
+
ctx.scale(scaleX, scaleY);
|
|
2305
|
+
ctx.translate(-vbX, -vbY);
|
|
2247
2306
|
}
|
|
2248
|
-
} else if (fill !== "none") {
|
|
2249
|
-
ctx.fillStyle = applyOpacity(fill, fillOpacity);
|
|
2250
|
-
ctx.fill(path, fillRule ?? "nonzero");
|
|
2251
2307
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const
|
|
2256
|
-
if (
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? inherited.strokeLinejoin ?? "miter";
|
|
2265
|
-
const strokeRef = parseUrlRef(stroke);
|
|
2266
|
-
if (strokeRef) {
|
|
2267
|
-
const gradientDef = defs.gradients.get(strokeRef);
|
|
2268
|
-
if (gradientDef) {
|
|
2269
|
-
if (strokeOpacity < 1) {
|
|
2270
|
-
ctx.save();
|
|
2271
|
-
ctx.globalAlpha *= strokeOpacity;
|
|
2272
|
-
strokeWithSvgGradient(ctx, gradientDef, bbox, path);
|
|
2273
|
-
ctx.restore();
|
|
2274
|
-
} else {
|
|
2275
|
-
strokeWithSvgGradient(ctx, gradientDef, bbox, path);
|
|
2276
|
-
}
|
|
2277
|
-
return;
|
|
2308
|
+
const color = node.style.color ?? "black";
|
|
2309
|
+
const merged = mergeStyleIntoProps(node.props);
|
|
2310
|
+
const inherited = resolveInheritedStyle(merged, color);
|
|
2311
|
+
const children = node.props.children;
|
|
2312
|
+
if (children != null) {
|
|
2313
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
2314
|
+
const svgChildren = childArray.filter(
|
|
2315
|
+
(c) => c != null && typeof c === "object"
|
|
2316
|
+
);
|
|
2317
|
+
const defs = collectDefs(svgChildren);
|
|
2318
|
+
for (const child of svgChildren) {
|
|
2319
|
+
drawSvgChild(ctx, child, inherited, color, defs, vbW, vbH);
|
|
2278
2320
|
}
|
|
2279
2321
|
}
|
|
2280
|
-
ctx.
|
|
2281
|
-
ctx.stroke(path);
|
|
2322
|
+
ctx.restore();
|
|
2282
2323
|
}
|
|
2283
2324
|
|
|
2284
2325
|
// src/jsx/draw/text.ts
|
|
@@ -2565,7 +2606,7 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|
|
|
2565
2606
|
}
|
|
2566
2607
|
|
|
2567
2608
|
// src/jsx/draw/index.ts
|
|
2568
|
-
async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
2609
|
+
async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle, imageCache) {
|
|
2569
2610
|
const x = parentX + node.x;
|
|
2570
2611
|
const y = parentY + node.y;
|
|
2571
2612
|
const { width, height, style } = node;
|
|
@@ -2585,6 +2626,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
|
2585
2626
|
const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
|
|
2586
2627
|
offCtx.save();
|
|
2587
2628
|
offCtx.scale(qx, qy);
|
|
2629
|
+
const cache2 = imageCache ?? /* @__PURE__ */ new Map();
|
|
2588
2630
|
await drawNodeCore(
|
|
2589
2631
|
offCtx,
|
|
2590
2632
|
node,
|
|
@@ -2594,6 +2636,7 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
|
2594
2636
|
1 - y,
|
|
2595
2637
|
debug,
|
|
2596
2638
|
emojiStyle,
|
|
2639
|
+
cache2,
|
|
2597
2640
|
transformWithoutScale
|
|
2598
2641
|
);
|
|
2599
2642
|
offCtx.restore();
|
|
@@ -2627,7 +2670,18 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
|
2627
2670
|
return;
|
|
2628
2671
|
}
|
|
2629
2672
|
}
|
|
2630
|
-
|
|
2673
|
+
const cache = imageCache ?? /* @__PURE__ */ new Map();
|
|
2674
|
+
await drawNodeCore(
|
|
2675
|
+
ctx,
|
|
2676
|
+
node,
|
|
2677
|
+
parentX,
|
|
2678
|
+
parentY,
|
|
2679
|
+
0,
|
|
2680
|
+
0,
|
|
2681
|
+
debug,
|
|
2682
|
+
emojiStyle,
|
|
2683
|
+
cache
|
|
2684
|
+
);
|
|
2631
2685
|
}
|
|
2632
2686
|
function extractScale(transform) {
|
|
2633
2687
|
const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
|
|
@@ -2639,7 +2693,7 @@ function extractScale(transform) {
|
|
|
2639
2693
|
const remaining = transform.replace(fullMatch, "").trim();
|
|
2640
2694
|
return { sx, sy, remaining };
|
|
2641
2695
|
}
|
|
2642
|
-
async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, overrideTransform) {
|
|
2696
|
+
async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug, emojiStyle, imageCache, overrideTransform) {
|
|
2643
2697
|
const x = parentX + node.x + offsetX;
|
|
2644
2698
|
const y = parentY + node.y + offsetY;
|
|
2645
2699
|
const { width, height, style } = node;
|
|
@@ -2705,7 +2759,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
|
|
|
2705
2759
|
if (hasRadius(borderRadius)) {
|
|
2706
2760
|
applyClip(ctx, x, y, width, height, borderRadius);
|
|
2707
2761
|
}
|
|
2708
|
-
const image = await
|
|
2762
|
+
const image = await cachedLoadImage(imageCache, urlMatch[1]);
|
|
2709
2763
|
const bgSize = style.backgroundSize;
|
|
2710
2764
|
if (bgSize === "cover") {
|
|
2711
2765
|
const r = computeCover(
|
|
@@ -2814,8 +2868,8 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
|
|
|
2814
2868
|
imgY,
|
|
2815
2869
|
imgW,
|
|
2816
2870
|
imgH,
|
|
2817
|
-
|
|
2818
|
-
|
|
2871
|
+
imageCache,
|
|
2872
|
+
style
|
|
2819
2873
|
);
|
|
2820
2874
|
}
|
|
2821
2875
|
if (node.type === "svg") {
|
|
@@ -2823,7 +2877,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
|
|
|
2823
2877
|
} else {
|
|
2824
2878
|
for (const child of node.children) {
|
|
2825
2879
|
if (offsetX === 0 && offsetY === 0) {
|
|
2826
|
-
await drawNode(ctx, child, x, y, debug, emojiStyle);
|
|
2880
|
+
await drawNode(ctx, child, x, y, debug, emojiStyle, imageCache);
|
|
2827
2881
|
} else {
|
|
2828
2882
|
await drawNodeCore(
|
|
2829
2883
|
ctx,
|
|
@@ -2834,7 +2888,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, debug
|
|
|
2834
2888
|
0,
|
|
2835
2889
|
debug,
|
|
2836
2890
|
emojiStyle,
|
|
2837
|
-
|
|
2891
|
+
imageCache
|
|
2838
2892
|
);
|
|
2839
2893
|
}
|
|
2840
2894
|
}
|
|
@@ -2906,9 +2960,6 @@ function parseAngle(value) {
|
|
|
2906
2960
|
return parseFloat(value);
|
|
2907
2961
|
}
|
|
2908
2962
|
|
|
2909
|
-
// src/jsx/layout.ts
|
|
2910
|
-
import { loadImage as loadImage4 } from "@napi-rs/canvas";
|
|
2911
|
-
|
|
2912
2963
|
// src/jsx/style/expand.ts
|
|
2913
2964
|
var SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
2914
2965
|
function parseSides(value) {
|
|
@@ -3295,6 +3346,7 @@ function applyEdgeValue(node, setter, edge, value) {
|
|
|
3295
3346
|
|
|
3296
3347
|
// src/jsx/layout.ts
|
|
3297
3348
|
async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
|
|
3349
|
+
const imageCache = /* @__PURE__ */ new Map();
|
|
3298
3350
|
const elementYogaNode = createYogaNode();
|
|
3299
3351
|
const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
|
|
3300
3352
|
const elementNode = await buildNode(
|
|
@@ -3305,7 +3357,8 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
3305
3357
|
containerHeight,
|
|
3306
3358
|
ctx,
|
|
3307
3359
|
emojiEnabled,
|
|
3308
|
-
fontFamilies
|
|
3360
|
+
fontFamilies,
|
|
3361
|
+
imageCache
|
|
3309
3362
|
);
|
|
3310
3363
|
const rootYogaNode = createYogaNode();
|
|
3311
3364
|
rootYogaNode.setWidth(containerWidth);
|
|
@@ -3315,7 +3368,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
3315
3368
|
rootYogaNode.calculateLayout(containerWidth, containerHeight);
|
|
3316
3369
|
const elementLayout = extractLayout(elementNode, elementYogaNode);
|
|
3317
3370
|
freeYogaNode(rootYogaNode);
|
|
3318
|
-
|
|
3371
|
+
const tree = {
|
|
3319
3372
|
type: "div",
|
|
3320
3373
|
style: rootStyle,
|
|
3321
3374
|
children: [elementLayout],
|
|
@@ -3325,8 +3378,9 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
3325
3378
|
width: containerWidth,
|
|
3326
3379
|
height: containerHeight
|
|
3327
3380
|
};
|
|
3381
|
+
return { tree, imageCache };
|
|
3328
3382
|
}
|
|
3329
|
-
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
|
|
3383
|
+
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies, imageCache) {
|
|
3330
3384
|
if (element === null || element === void 0 || typeof element === "boolean") {
|
|
3331
3385
|
return {
|
|
3332
3386
|
type: "empty",
|
|
@@ -3364,7 +3418,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
3364
3418
|
viewportHeight,
|
|
3365
3419
|
ctx,
|
|
3366
3420
|
emojiEnabled,
|
|
3367
|
-
fontFamilies
|
|
3421
|
+
fontFamilies,
|
|
3422
|
+
imageCache
|
|
3368
3423
|
);
|
|
3369
3424
|
}
|
|
3370
3425
|
const props = el.props ?? {};
|
|
@@ -3424,7 +3479,7 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
3424
3479
|
const src = props.src;
|
|
3425
3480
|
if (src) {
|
|
3426
3481
|
try {
|
|
3427
|
-
const image = await
|
|
3482
|
+
const image = await cachedLoadImage(imageCache ?? /* @__PURE__ */ new Map(), src);
|
|
3428
3483
|
const naturalW = image.width;
|
|
3429
3484
|
const naturalH = image.height;
|
|
3430
3485
|
const wSet = style.width !== void 0;
|
|
@@ -3434,7 +3489,6 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
3434
3489
|
} else if (hSet && !wSet && typeof style.height === "number" && naturalH > 0) {
|
|
3435
3490
|
style.width = style.height * (naturalW / naturalH);
|
|
3436
3491
|
}
|
|
3437
|
-
props.__loadedImage = image;
|
|
3438
3492
|
} catch {
|
|
3439
3493
|
}
|
|
3440
3494
|
}
|
|
@@ -3512,7 +3566,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
3512
3566
|
viewportHeight,
|
|
3513
3567
|
ctx,
|
|
3514
3568
|
emojiEnabled,
|
|
3515
|
-
fontFamilies
|
|
3569
|
+
fontFamilies,
|
|
3570
|
+
imageCache
|
|
3516
3571
|
)
|
|
3517
3572
|
);
|
|
3518
3573
|
}
|
|
@@ -3586,7 +3641,7 @@ async function renderReactElement(ctx, element, options) {
|
|
|
3586
3641
|
const height = options.height ?? ctx.canvas.height;
|
|
3587
3642
|
const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
|
|
3588
3643
|
const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
|
|
3589
|
-
const layoutTree = await buildLayoutTree(
|
|
3644
|
+
const { tree: layoutTree, imageCache } = await buildLayoutTree(
|
|
3590
3645
|
element,
|
|
3591
3646
|
width,
|
|
3592
3647
|
height,
|
|
@@ -3594,7 +3649,15 @@ async function renderReactElement(ctx, element, options) {
|
|
|
3594
3649
|
!!emojiStyle,
|
|
3595
3650
|
fontFamilies
|
|
3596
3651
|
);
|
|
3597
|
-
await drawNode(
|
|
3652
|
+
await drawNode(
|
|
3653
|
+
ctx,
|
|
3654
|
+
layoutTree,
|
|
3655
|
+
0,
|
|
3656
|
+
0,
|
|
3657
|
+
options.debug ?? false,
|
|
3658
|
+
emojiStyle,
|
|
3659
|
+
imageCache
|
|
3660
|
+
);
|
|
3598
3661
|
}
|
|
3599
3662
|
|
|
3600
3663
|
// src/index.ts
|
|
@@ -3610,7 +3673,7 @@ export {
|
|
|
3610
3673
|
Image,
|
|
3611
3674
|
LottieAnimation,
|
|
3612
3675
|
createCanvas3 as createCanvas,
|
|
3613
|
-
|
|
3676
|
+
loadImage3 as loadImage,
|
|
3614
3677
|
loadLottie,
|
|
3615
3678
|
registerFont,
|
|
3616
3679
|
registerFontFromPath,
|