@effing/canvas 0.18.5 → 0.19.0
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 +726 -97
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -74,8 +74,28 @@ function getScratchCtx() {
|
|
|
74
74
|
}
|
|
75
75
|
return scratchCtx;
|
|
76
76
|
}
|
|
77
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
78
|
+
"serif",
|
|
79
|
+
"sans-serif",
|
|
80
|
+
"monospace",
|
|
81
|
+
"cursive",
|
|
82
|
+
"fantasy",
|
|
83
|
+
"system-ui",
|
|
84
|
+
"ui-serif",
|
|
85
|
+
"ui-sans-serif",
|
|
86
|
+
"ui-monospace",
|
|
87
|
+
"ui-rounded",
|
|
88
|
+
"math",
|
|
89
|
+
"emoji",
|
|
90
|
+
"fangsong"
|
|
91
|
+
]);
|
|
92
|
+
function quoteFontFamily(family) {
|
|
93
|
+
if (!family || GENERIC_FAMILIES.has(family)) return family;
|
|
94
|
+
return `"${family}"`;
|
|
95
|
+
}
|
|
77
96
|
function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
|
|
78
|
-
|
|
97
|
+
const quoted = fontFamily.split(",").map((f) => quoteFontFamily(f.trim())).join(", ");
|
|
98
|
+
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoted}`;
|
|
79
99
|
}
|
|
80
100
|
function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx) {
|
|
81
101
|
const c = ctx ?? getScratchCtx();
|
|
@@ -90,6 +110,54 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
|
|
|
90
110
|
height: ascent + descent
|
|
91
111
|
};
|
|
92
112
|
}
|
|
113
|
+
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx) {
|
|
114
|
+
const c = ctx ?? getScratchCtx();
|
|
115
|
+
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
116
|
+
const refMetrics = c.measureText("M");
|
|
117
|
+
const fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
118
|
+
const fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
119
|
+
const parts = edge.trim().split(/\s+/);
|
|
120
|
+
const overEdge = parts[0] ?? "text";
|
|
121
|
+
const underEdge = parts.length > 1 ? parts[1] : overEdge;
|
|
122
|
+
let targetAscent;
|
|
123
|
+
switch (overEdge) {
|
|
124
|
+
case "cap": {
|
|
125
|
+
const capMetrics = c.measureText("H");
|
|
126
|
+
targetAscent = capMetrics.actualBoundingBoxAscent ?? fontSize * 0.7;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "ex": {
|
|
130
|
+
const exMetrics = c.measureText("x");
|
|
131
|
+
targetAscent = exMetrics.actualBoundingBoxAscent ?? fontSize * 0.5;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "ideographic":
|
|
135
|
+
case "ideographic-ink":
|
|
136
|
+
case "text":
|
|
137
|
+
default:
|
|
138
|
+
targetAscent = fontAscent;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
let targetDescent;
|
|
142
|
+
switch (underEdge) {
|
|
143
|
+
case "alphabetic":
|
|
144
|
+
targetDescent = 0;
|
|
145
|
+
break;
|
|
146
|
+
case "ideographic":
|
|
147
|
+
case "ideographic-ink":
|
|
148
|
+
case "text":
|
|
149
|
+
default:
|
|
150
|
+
targetDescent = fontDescent;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
const halfLeading = (lineHeight - (fontAscent + fontDescent)) / 2;
|
|
154
|
+
const overTrim = halfLeading + (fontAscent - targetAscent);
|
|
155
|
+
const underTrim = halfLeading + (fontDescent - targetDescent);
|
|
156
|
+
return {
|
|
157
|
+
overTrim: Math.max(0, overTrim),
|
|
158
|
+
underTrim: Math.max(0, underTrim)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
93
161
|
function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx, letterSpacing = 0) {
|
|
94
162
|
const base = measureText(
|
|
95
163
|
word,
|
|
@@ -275,6 +343,28 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
275
343
|
totalHeight += lineHeightPx;
|
|
276
344
|
maxLineWidth = Math.max(maxLineWidth, lineWidth);
|
|
277
345
|
}
|
|
346
|
+
const textBoxTrim = style.textBoxTrim;
|
|
347
|
+
if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
|
|
348
|
+
const textBoxEdge = style.textBoxEdge ?? "text";
|
|
349
|
+
const trimMetrics = measureTrimMetrics(
|
|
350
|
+
fontSize,
|
|
351
|
+
fontFamily,
|
|
352
|
+
fontWeight,
|
|
353
|
+
fontStyle,
|
|
354
|
+
lineHeightPx,
|
|
355
|
+
textBoxEdge,
|
|
356
|
+
ctx
|
|
357
|
+
);
|
|
358
|
+
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
359
|
+
for (const seg of segments) {
|
|
360
|
+
seg.y -= trimMetrics.overTrim;
|
|
361
|
+
}
|
|
362
|
+
totalHeight -= trimMetrics.overTrim;
|
|
363
|
+
}
|
|
364
|
+
if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
|
|
365
|
+
totalHeight -= trimMetrics.underTrim;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
278
368
|
return {
|
|
279
369
|
segments,
|
|
280
370
|
width: maxLineWidth,
|
|
@@ -678,10 +768,7 @@ function drawRect(ctx, x, y, width, height, style) {
|
|
|
678
768
|
drawBorders(ctx, x, y, width, height, style, borderRadius);
|
|
679
769
|
}
|
|
680
770
|
function resolveRadius(v, width, height) {
|
|
681
|
-
if (typeof v === "string"
|
|
682
|
-
const pct = parseFloat(v) / 100;
|
|
683
|
-
return pct * Math.min(width, height);
|
|
684
|
-
}
|
|
771
|
+
if (typeof v === "string") return parseCSSLength(v, Math.min(width, height));
|
|
685
772
|
return toNumber(v);
|
|
686
773
|
}
|
|
687
774
|
function getBorderRadius(style, width, height) {
|
|
@@ -793,12 +880,6 @@ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
|
|
|
793
880
|
ctx.fill();
|
|
794
881
|
ctx.restore();
|
|
795
882
|
}
|
|
796
|
-
function toNumber(v) {
|
|
797
|
-
if (typeof v === "number") return v;
|
|
798
|
-
if (v === void 0 || v === null) return 0;
|
|
799
|
-
const n = parseFloat(String(v));
|
|
800
|
-
return isNaN(n) ? 0 : n;
|
|
801
|
-
}
|
|
802
883
|
|
|
803
884
|
// src/jsx/draw/svg.ts
|
|
804
885
|
import { Path2D } from "@napi-rs/canvas";
|
|
@@ -811,6 +892,349 @@ function mergeStyleIntoProps(props) {
|
|
|
811
892
|
if (!style) return props;
|
|
812
893
|
return { ...props, ...style };
|
|
813
894
|
}
|
|
895
|
+
function collectDefs(children) {
|
|
896
|
+
const clips = /* @__PURE__ */ new Map();
|
|
897
|
+
const gradients = /* @__PURE__ */ new Map();
|
|
898
|
+
for (const child of children) {
|
|
899
|
+
if (child.type !== "defs") continue;
|
|
900
|
+
for (const def of normalizeChildren(child)) {
|
|
901
|
+
const id = def.props.id;
|
|
902
|
+
if (!id) continue;
|
|
903
|
+
if (def.type === "clipPath") clips.set(id, normalizeChildren(def));
|
|
904
|
+
else if (def.type === "radialGradient" || def.type === "linearGradient")
|
|
905
|
+
gradients.set(id, def);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return { clips, gradients };
|
|
909
|
+
}
|
|
910
|
+
function parseUrlRef(value) {
|
|
911
|
+
if (typeof value !== "string") return void 0;
|
|
912
|
+
const m = value.match(/^url\(#(.+)\)$/);
|
|
913
|
+
return m?.[1];
|
|
914
|
+
}
|
|
915
|
+
function normalizeChildren(node) {
|
|
916
|
+
const raw = node.children ?? node.props.children;
|
|
917
|
+
if (raw == null) return [];
|
|
918
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
919
|
+
}
|
|
920
|
+
function parseFrac(value, fallback) {
|
|
921
|
+
if (value == null) return fallback;
|
|
922
|
+
const s = String(value);
|
|
923
|
+
if (s.endsWith("%")) return parseFloat(s) / 100;
|
|
924
|
+
return Number(s);
|
|
925
|
+
}
|
|
926
|
+
function applyOpacity(color, opacity) {
|
|
927
|
+
if (opacity >= 1) return color;
|
|
928
|
+
const m = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
929
|
+
if (m)
|
|
930
|
+
return `rgba(${parseInt(m[1], 16)}, ${parseInt(m[2], 16)}, ${parseInt(m[3], 16)}, ${opacity})`;
|
|
931
|
+
return color;
|
|
932
|
+
}
|
|
933
|
+
function addGradientStops(gradient, def) {
|
|
934
|
+
for (const stop of normalizeChildren(def)) {
|
|
935
|
+
if (stop.type !== "stop") continue;
|
|
936
|
+
const sp = stop.props;
|
|
937
|
+
const offsetRaw = sp.offset ?? 0;
|
|
938
|
+
const offset = typeof offsetRaw === "string" && offsetRaw.endsWith("%") ? parseFloat(offsetRaw) / 100 : Number(offsetRaw);
|
|
939
|
+
const stopColor = sp.stopColor ?? sp["stop-color"] ?? "black";
|
|
940
|
+
const stopOpacity = Number(sp.stopOpacity ?? sp["stop-opacity"] ?? 1);
|
|
941
|
+
gradient.addColorStop(offset, applyOpacity(stopColor, stopOpacity));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function fillWithSvgGradient(ctx, def, bbox, path, fillRule) {
|
|
945
|
+
const props = def.props;
|
|
946
|
+
if (def.type === "linearGradient") {
|
|
947
|
+
const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
|
|
948
|
+
const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
|
|
949
|
+
const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
|
|
950
|
+
const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
|
|
951
|
+
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
952
|
+
addGradientStops(gradient, def);
|
|
953
|
+
ctx.fillStyle = gradient;
|
|
954
|
+
ctx.fill(path, fillRule);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
if (def.type === "radialGradient") {
|
|
958
|
+
const cxF = parseFrac(props.cx, 0.5);
|
|
959
|
+
const cyF = parseFrac(props.cy, 0.5);
|
|
960
|
+
const rF = parseFrac(props.r, 0.5);
|
|
961
|
+
const fxF = parseFrac(props.fx, cxF);
|
|
962
|
+
const fyF = parseFrac(props.fy, cyF);
|
|
963
|
+
ctx.save();
|
|
964
|
+
ctx.translate(bbox.x, bbox.y);
|
|
965
|
+
ctx.scale(bbox.width, bbox.height);
|
|
966
|
+
const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
|
|
967
|
+
addGradientStops(gradient, def);
|
|
968
|
+
ctx.fillStyle = gradient;
|
|
969
|
+
const unitPath = new Path2D();
|
|
970
|
+
const invTransform = {
|
|
971
|
+
a: 1 / bbox.width,
|
|
972
|
+
b: 0,
|
|
973
|
+
c: 0,
|
|
974
|
+
d: 1 / bbox.height,
|
|
975
|
+
e: -bbox.x / bbox.width,
|
|
976
|
+
f: -bbox.y / bbox.height
|
|
977
|
+
};
|
|
978
|
+
unitPath.addPath(path, invTransform);
|
|
979
|
+
ctx.fill(unitPath, fillRule);
|
|
980
|
+
ctx.restore();
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
function strokeWithSvgGradient(ctx, def, bbox, path) {
|
|
986
|
+
const props = def.props;
|
|
987
|
+
if (def.type === "linearGradient") {
|
|
988
|
+
const x1 = bbox.x + parseFrac(props.x1, 0) * bbox.width;
|
|
989
|
+
const y1 = bbox.y + parseFrac(props.y1, 0) * bbox.height;
|
|
990
|
+
const x2 = bbox.x + parseFrac(props.x2, 1) * bbox.width;
|
|
991
|
+
const y2 = bbox.y + parseFrac(props.y2, 0) * bbox.height;
|
|
992
|
+
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
993
|
+
addGradientStops(gradient, def);
|
|
994
|
+
ctx.strokeStyle = gradient;
|
|
995
|
+
ctx.stroke(path);
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
if (def.type === "radialGradient") {
|
|
999
|
+
const cxAbs = bbox.x + parseFrac(props.cx, 0.5) * bbox.width;
|
|
1000
|
+
const cyAbs = bbox.y + parseFrac(props.cy, 0.5) * bbox.height;
|
|
1001
|
+
const rAbs = parseFrac(props.r, 0.5) * Math.sqrt(bbox.width * bbox.height);
|
|
1002
|
+
const fxAbs = bbox.x + parseFrac(props.fx, 0.5) * bbox.width;
|
|
1003
|
+
const fyAbs = bbox.y + parseFrac(props.fy, 0.5) * bbox.height;
|
|
1004
|
+
const gradient = ctx.createRadialGradient(
|
|
1005
|
+
fxAbs,
|
|
1006
|
+
fyAbs,
|
|
1007
|
+
0,
|
|
1008
|
+
cxAbs,
|
|
1009
|
+
cyAbs,
|
|
1010
|
+
rAbs
|
|
1011
|
+
);
|
|
1012
|
+
addGradientStops(gradient, def);
|
|
1013
|
+
ctx.strokeStyle = gradient;
|
|
1014
|
+
ctx.stroke(path);
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
function pathBBox(d) {
|
|
1020
|
+
let minX = Infinity;
|
|
1021
|
+
let minY = Infinity;
|
|
1022
|
+
let maxX = -Infinity;
|
|
1023
|
+
let maxY = -Infinity;
|
|
1024
|
+
let cx = 0;
|
|
1025
|
+
let cy = 0;
|
|
1026
|
+
const update = (x, y) => {
|
|
1027
|
+
if (x < minX) minX = x;
|
|
1028
|
+
if (x > maxX) maxX = x;
|
|
1029
|
+
if (y < minY) minY = y;
|
|
1030
|
+
if (y > maxY) maxY = y;
|
|
1031
|
+
};
|
|
1032
|
+
const tokens = d.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g);
|
|
1033
|
+
if (!tokens) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1034
|
+
let cmd = "";
|
|
1035
|
+
let i = 0;
|
|
1036
|
+
const num = () => Number(tokens[i++]);
|
|
1037
|
+
while (i < tokens.length) {
|
|
1038
|
+
const t = tokens[i];
|
|
1039
|
+
if (/[a-zA-Z]/.test(t)) {
|
|
1040
|
+
cmd = t;
|
|
1041
|
+
i++;
|
|
1042
|
+
}
|
|
1043
|
+
switch (cmd) {
|
|
1044
|
+
case "M":
|
|
1045
|
+
cx = num();
|
|
1046
|
+
cy = num();
|
|
1047
|
+
update(cx, cy);
|
|
1048
|
+
cmd = "L";
|
|
1049
|
+
break;
|
|
1050
|
+
case "m":
|
|
1051
|
+
cx += num();
|
|
1052
|
+
cy += num();
|
|
1053
|
+
update(cx, cy);
|
|
1054
|
+
cmd = "l";
|
|
1055
|
+
break;
|
|
1056
|
+
case "L":
|
|
1057
|
+
cx = num();
|
|
1058
|
+
cy = num();
|
|
1059
|
+
update(cx, cy);
|
|
1060
|
+
break;
|
|
1061
|
+
case "l":
|
|
1062
|
+
cx += num();
|
|
1063
|
+
cy += num();
|
|
1064
|
+
update(cx, cy);
|
|
1065
|
+
break;
|
|
1066
|
+
case "H":
|
|
1067
|
+
cx = num();
|
|
1068
|
+
update(cx, cy);
|
|
1069
|
+
break;
|
|
1070
|
+
case "h":
|
|
1071
|
+
cx += num();
|
|
1072
|
+
update(cx, cy);
|
|
1073
|
+
break;
|
|
1074
|
+
case "V":
|
|
1075
|
+
cy = num();
|
|
1076
|
+
update(cx, cy);
|
|
1077
|
+
break;
|
|
1078
|
+
case "v":
|
|
1079
|
+
cy += num();
|
|
1080
|
+
update(cx, cy);
|
|
1081
|
+
break;
|
|
1082
|
+
case "C": {
|
|
1083
|
+
for (let j = 0; j < 3; j++) {
|
|
1084
|
+
const px = num();
|
|
1085
|
+
const py = num();
|
|
1086
|
+
update(px, py);
|
|
1087
|
+
}
|
|
1088
|
+
cx = Number(tokens[i - 2]);
|
|
1089
|
+
cy = Number(tokens[i - 1]);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case "c": {
|
|
1093
|
+
for (let j = 0; j < 3; j++) {
|
|
1094
|
+
const dx = num();
|
|
1095
|
+
const dy = num();
|
|
1096
|
+
update(cx + dx, cy + dy);
|
|
1097
|
+
if (j === 2) {
|
|
1098
|
+
cx += dx;
|
|
1099
|
+
cy += dy;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
case "Q": {
|
|
1105
|
+
for (let j = 0; j < 2; j++) {
|
|
1106
|
+
const px = num();
|
|
1107
|
+
const py = num();
|
|
1108
|
+
update(px, py);
|
|
1109
|
+
}
|
|
1110
|
+
cx = Number(tokens[i - 2]);
|
|
1111
|
+
cy = Number(tokens[i - 1]);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
case "q": {
|
|
1115
|
+
for (let j = 0; j < 2; j++) {
|
|
1116
|
+
const dx = num();
|
|
1117
|
+
const dy = num();
|
|
1118
|
+
update(cx + dx, cy + dy);
|
|
1119
|
+
if (j === 1) {
|
|
1120
|
+
cx += dx;
|
|
1121
|
+
cy += dy;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case "A": {
|
|
1127
|
+
num();
|
|
1128
|
+
num();
|
|
1129
|
+
num();
|
|
1130
|
+
num();
|
|
1131
|
+
num();
|
|
1132
|
+
cx = num();
|
|
1133
|
+
cy = num();
|
|
1134
|
+
update(cx, cy);
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
case "a": {
|
|
1138
|
+
num();
|
|
1139
|
+
num();
|
|
1140
|
+
num();
|
|
1141
|
+
num();
|
|
1142
|
+
num();
|
|
1143
|
+
cx += num();
|
|
1144
|
+
cy += num();
|
|
1145
|
+
update(cx, cy);
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
case "Z":
|
|
1149
|
+
case "z":
|
|
1150
|
+
break;
|
|
1151
|
+
default:
|
|
1152
|
+
i++;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (!isFinite(minX)) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1157
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1158
|
+
}
|
|
1159
|
+
function pointsBBox(points) {
|
|
1160
|
+
let minX = Infinity;
|
|
1161
|
+
let minY = Infinity;
|
|
1162
|
+
let maxX = -Infinity;
|
|
1163
|
+
let maxY = -Infinity;
|
|
1164
|
+
for (const [x, y] of points) {
|
|
1165
|
+
if (x < minX) minX = x;
|
|
1166
|
+
if (x > maxX) maxX = x;
|
|
1167
|
+
if (y < minY) minY = y;
|
|
1168
|
+
if (y > maxY) maxY = y;
|
|
1169
|
+
}
|
|
1170
|
+
if (!isFinite(minX)) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1171
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1172
|
+
}
|
|
1173
|
+
function buildPath(child) {
|
|
1174
|
+
const props = mergeStyleIntoProps(child.props);
|
|
1175
|
+
switch (child.type) {
|
|
1176
|
+
case "path": {
|
|
1177
|
+
const d = props.d;
|
|
1178
|
+
if (!d) return void 0;
|
|
1179
|
+
return new Path2D(d);
|
|
1180
|
+
}
|
|
1181
|
+
case "circle": {
|
|
1182
|
+
const cx = Number(props.cx ?? 0);
|
|
1183
|
+
const cy = Number(props.cy ?? 0);
|
|
1184
|
+
const r = Number(props.r ?? 0);
|
|
1185
|
+
if (r <= 0) return void 0;
|
|
1186
|
+
const p = new Path2D();
|
|
1187
|
+
p.arc(cx, cy, r, 0, Math.PI * 2);
|
|
1188
|
+
return p;
|
|
1189
|
+
}
|
|
1190
|
+
case "rect": {
|
|
1191
|
+
const rx = Number(props.x ?? 0);
|
|
1192
|
+
const ry = Number(props.y ?? 0);
|
|
1193
|
+
const w = Number(props.width ?? 0);
|
|
1194
|
+
const h = Number(props.height ?? 0);
|
|
1195
|
+
if (w <= 0 || h <= 0) return void 0;
|
|
1196
|
+
const p = new Path2D();
|
|
1197
|
+
p.rect(rx, ry, w, h);
|
|
1198
|
+
return p;
|
|
1199
|
+
}
|
|
1200
|
+
case "ellipse": {
|
|
1201
|
+
const cx = Number(props.cx ?? 0);
|
|
1202
|
+
const cy = Number(props.cy ?? 0);
|
|
1203
|
+
const rx = Number(props.rx ?? 0);
|
|
1204
|
+
const ry = Number(props.ry ?? 0);
|
|
1205
|
+
if (rx <= 0 || ry <= 0) return void 0;
|
|
1206
|
+
const p = new Path2D();
|
|
1207
|
+
p.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
1208
|
+
return p;
|
|
1209
|
+
}
|
|
1210
|
+
case "polygon": {
|
|
1211
|
+
const points = parsePoints(props.points);
|
|
1212
|
+
if (points.length < 2) return void 0;
|
|
1213
|
+
const p = new Path2D();
|
|
1214
|
+
p.moveTo(points[0][0], points[0][1]);
|
|
1215
|
+
for (let i = 1; i < points.length; i++) {
|
|
1216
|
+
p.lineTo(points[i][0], points[i][1]);
|
|
1217
|
+
}
|
|
1218
|
+
p.closePath();
|
|
1219
|
+
return p;
|
|
1220
|
+
}
|
|
1221
|
+
default:
|
|
1222
|
+
return void 0;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function buildClipPath(shapes) {
|
|
1226
|
+
let combined;
|
|
1227
|
+
for (const shape of shapes) {
|
|
1228
|
+
const p = buildPath(shape);
|
|
1229
|
+
if (!p) continue;
|
|
1230
|
+
if (!combined) {
|
|
1231
|
+
combined = p;
|
|
1232
|
+
} else {
|
|
1233
|
+
combined.addPath(p);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return combined;
|
|
1237
|
+
}
|
|
814
1238
|
function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
815
1239
|
ctx.save();
|
|
816
1240
|
ctx.translate(x, y);
|
|
@@ -831,60 +1255,75 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
|
831
1255
|
const children = node.props.children;
|
|
832
1256
|
if (children != null) {
|
|
833
1257
|
const childArray = Array.isArray(children) ? children : [children];
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1258
|
+
const svgChildren = childArray.filter(
|
|
1259
|
+
(c) => c != null && typeof c === "object"
|
|
1260
|
+
);
|
|
1261
|
+
const defs = collectDefs(svgChildren);
|
|
1262
|
+
for (const child of svgChildren) {
|
|
1263
|
+
drawSvgChild(ctx, child, inheritedFill, color, defs);
|
|
838
1264
|
}
|
|
839
1265
|
}
|
|
840
1266
|
ctx.restore();
|
|
841
1267
|
}
|
|
842
|
-
|
|
1268
|
+
var EMPTY_DEFS = {
|
|
1269
|
+
clips: /* @__PURE__ */ new Map(),
|
|
1270
|
+
gradients: /* @__PURE__ */ new Map()
|
|
1271
|
+
};
|
|
1272
|
+
function drawSvgChild(ctx, child, inheritedFill, color, defs = EMPTY_DEFS) {
|
|
843
1273
|
const { type } = child;
|
|
844
1274
|
const props = mergeStyleIntoProps(child.props);
|
|
1275
|
+
if (type === "defs" || type === "clipPath") return;
|
|
1276
|
+
const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
|
|
1277
|
+
const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
|
|
1278
|
+
const clipPath = clipShapes ? buildClipPath(clipShapes) : void 0;
|
|
1279
|
+
if (clipPath) ctx.save();
|
|
1280
|
+
if (clipPath) ctx.clip(clipPath);
|
|
845
1281
|
switch (type) {
|
|
846
1282
|
case "path":
|
|
847
|
-
drawPath(ctx, props, inheritedFill, color);
|
|
1283
|
+
drawPath(ctx, props, inheritedFill, color, defs);
|
|
848
1284
|
break;
|
|
849
1285
|
case "circle":
|
|
850
|
-
drawCircle(ctx, props, inheritedFill, color);
|
|
1286
|
+
drawCircle(ctx, props, inheritedFill, color, defs);
|
|
851
1287
|
break;
|
|
852
1288
|
case "rect":
|
|
853
|
-
drawSvgRect(ctx, props, inheritedFill, color);
|
|
1289
|
+
drawSvgRect(ctx, props, inheritedFill, color, defs);
|
|
854
1290
|
break;
|
|
855
1291
|
case "line":
|
|
856
|
-
drawLine(ctx, props, color);
|
|
1292
|
+
drawLine(ctx, props, color, defs);
|
|
857
1293
|
break;
|
|
858
1294
|
case "ellipse":
|
|
859
|
-
drawEllipse(ctx, props, inheritedFill, color);
|
|
1295
|
+
drawEllipse(ctx, props, inheritedFill, color, defs);
|
|
860
1296
|
break;
|
|
861
1297
|
case "polygon":
|
|
862
|
-
drawPolygon(ctx, props, inheritedFill, color);
|
|
1298
|
+
drawPolygon(ctx, props, inheritedFill, color, defs);
|
|
863
1299
|
break;
|
|
864
1300
|
case "polyline":
|
|
865
|
-
drawPolyline(ctx, props, inheritedFill, color);
|
|
1301
|
+
drawPolyline(ctx, props, inheritedFill, color, defs);
|
|
866
1302
|
break;
|
|
867
1303
|
case "g":
|
|
868
|
-
drawGroup(ctx, child, inheritedFill, color);
|
|
1304
|
+
drawGroup(ctx, child, inheritedFill, color, defs);
|
|
869
1305
|
break;
|
|
870
1306
|
}
|
|
1307
|
+
if (clipPath) ctx.restore();
|
|
871
1308
|
}
|
|
872
|
-
function drawPath(ctx, props, inheritedFill, color) {
|
|
1309
|
+
function drawPath(ctx, props, inheritedFill, color, defs) {
|
|
873
1310
|
const d = props.d;
|
|
874
1311
|
if (!d) return;
|
|
875
1312
|
const path = new Path2D(d);
|
|
876
|
-
|
|
1313
|
+
const bbox = pathBBox(d);
|
|
1314
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
877
1315
|
}
|
|
878
|
-
function drawCircle(ctx, props, inheritedFill, color) {
|
|
1316
|
+
function drawCircle(ctx, props, inheritedFill, color, defs) {
|
|
879
1317
|
const cx = Number(props.cx ?? 0);
|
|
880
1318
|
const cy = Number(props.cy ?? 0);
|
|
881
1319
|
const r = Number(props.r ?? 0);
|
|
882
1320
|
if (r <= 0) return;
|
|
883
1321
|
const path = new Path2D();
|
|
884
1322
|
path.arc(cx, cy, r, 0, Math.PI * 2);
|
|
885
|
-
|
|
1323
|
+
const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
|
|
1324
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
886
1325
|
}
|
|
887
|
-
function drawSvgRect(ctx, props, inheritedFill, color) {
|
|
1326
|
+
function drawSvgRect(ctx, props, inheritedFill, color, defs) {
|
|
888
1327
|
const rx = Number(props.x ?? 0);
|
|
889
1328
|
const ry = Number(props.y ?? 0);
|
|
890
1329
|
const w = Number(props.width ?? 0);
|
|
@@ -892,9 +1331,10 @@ function drawSvgRect(ctx, props, inheritedFill, color) {
|
|
|
892
1331
|
if (w <= 0 || h <= 0) return;
|
|
893
1332
|
const path = new Path2D();
|
|
894
1333
|
path.rect(rx, ry, w, h);
|
|
895
|
-
|
|
1334
|
+
const bbox = { x: rx, y: ry, width: w, height: h };
|
|
1335
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
896
1336
|
}
|
|
897
|
-
function drawLine(ctx, props, color) {
|
|
1337
|
+
function drawLine(ctx, props, color, defs) {
|
|
898
1338
|
const x1 = Number(props.x1 ?? 0);
|
|
899
1339
|
const y1 = Number(props.y1 ?? 0);
|
|
900
1340
|
const x2 = Number(props.x2 ?? 0);
|
|
@@ -902,9 +1342,15 @@ function drawLine(ctx, props, color) {
|
|
|
902
1342
|
const path = new Path2D();
|
|
903
1343
|
path.moveTo(x1, y1);
|
|
904
1344
|
path.lineTo(x2, y2);
|
|
905
|
-
|
|
1345
|
+
const bbox = {
|
|
1346
|
+
x: Math.min(x1, x2),
|
|
1347
|
+
y: Math.min(y1, y2),
|
|
1348
|
+
width: Math.abs(x2 - x1),
|
|
1349
|
+
height: Math.abs(y2 - y1)
|
|
1350
|
+
};
|
|
1351
|
+
applyStroke(ctx, props, path, color, defs, bbox);
|
|
906
1352
|
}
|
|
907
|
-
function drawEllipse(ctx, props, inheritedFill, color) {
|
|
1353
|
+
function drawEllipse(ctx, props, inheritedFill, color, defs) {
|
|
908
1354
|
const cx = Number(props.cx ?? 0);
|
|
909
1355
|
const cy = Number(props.cy ?? 0);
|
|
910
1356
|
const rx = Number(props.rx ?? 0);
|
|
@@ -912,9 +1358,15 @@ function drawEllipse(ctx, props, inheritedFill, color) {
|
|
|
912
1358
|
if (rx <= 0 || ry <= 0) return;
|
|
913
1359
|
const path = new Path2D();
|
|
914
1360
|
path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
915
|
-
|
|
1361
|
+
const bbox = {
|
|
1362
|
+
x: cx - rx,
|
|
1363
|
+
y: cy - ry,
|
|
1364
|
+
width: 2 * rx,
|
|
1365
|
+
height: 2 * ry
|
|
1366
|
+
};
|
|
1367
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
916
1368
|
}
|
|
917
|
-
function drawPolygon(ctx, props, inheritedFill, color) {
|
|
1369
|
+
function drawPolygon(ctx, props, inheritedFill, color, defs) {
|
|
918
1370
|
const points = parsePoints(props.points);
|
|
919
1371
|
if (points.length < 2) return;
|
|
920
1372
|
const path = new Path2D();
|
|
@@ -923,9 +1375,10 @@ function drawPolygon(ctx, props, inheritedFill, color) {
|
|
|
923
1375
|
path.lineTo(points[i][0], points[i][1]);
|
|
924
1376
|
}
|
|
925
1377
|
path.closePath();
|
|
926
|
-
|
|
1378
|
+
const bbox = pointsBBox(points);
|
|
1379
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
927
1380
|
}
|
|
928
|
-
function drawPolyline(ctx, props, inheritedFill, color) {
|
|
1381
|
+
function drawPolyline(ctx, props, inheritedFill, color, defs) {
|
|
929
1382
|
const points = parsePoints(props.points);
|
|
930
1383
|
if (points.length < 2) return;
|
|
931
1384
|
const path = new Path2D();
|
|
@@ -933,19 +1386,88 @@ function drawPolyline(ctx, props, inheritedFill, color) {
|
|
|
933
1386
|
for (let i = 1; i < points.length; i++) {
|
|
934
1387
|
path.lineTo(points[i][0], points[i][1]);
|
|
935
1388
|
}
|
|
936
|
-
|
|
1389
|
+
const bbox = pointsBBox(points);
|
|
1390
|
+
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
1391
|
+
}
|
|
1392
|
+
function applySvgTransform(ctx, transform) {
|
|
1393
|
+
const re = /\b(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)/g;
|
|
1394
|
+
let match;
|
|
1395
|
+
while ((match = re.exec(transform)) !== null) {
|
|
1396
|
+
const fn = match[1];
|
|
1397
|
+
const args = match[2].split(/[\s,]+/).filter(Boolean).map(Number);
|
|
1398
|
+
switch (fn) {
|
|
1399
|
+
case "translate":
|
|
1400
|
+
ctx.translate(args[0] ?? 0, args[1] ?? 0);
|
|
1401
|
+
break;
|
|
1402
|
+
case "scale": {
|
|
1403
|
+
const sx = args[0] ?? 1;
|
|
1404
|
+
ctx.scale(sx, args[1] ?? sx);
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
case "rotate": {
|
|
1408
|
+
const angle = (args[0] ?? 0) * Math.PI / 180;
|
|
1409
|
+
if (args.length >= 3) {
|
|
1410
|
+
const cx = args[1];
|
|
1411
|
+
const cy = args[2];
|
|
1412
|
+
ctx.translate(cx, cy);
|
|
1413
|
+
ctx.rotate(angle);
|
|
1414
|
+
ctx.translate(-cx, -cy);
|
|
1415
|
+
} else {
|
|
1416
|
+
ctx.rotate(angle);
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
case "skewX":
|
|
1421
|
+
ctx.transform(
|
|
1422
|
+
1,
|
|
1423
|
+
0,
|
|
1424
|
+
Math.tan((args[0] ?? 0) * Math.PI / 180),
|
|
1425
|
+
1,
|
|
1426
|
+
0,
|
|
1427
|
+
0
|
|
1428
|
+
);
|
|
1429
|
+
break;
|
|
1430
|
+
case "skewY":
|
|
1431
|
+
ctx.transform(
|
|
1432
|
+
1,
|
|
1433
|
+
Math.tan((args[0] ?? 0) * Math.PI / 180),
|
|
1434
|
+
0,
|
|
1435
|
+
1,
|
|
1436
|
+
0,
|
|
1437
|
+
0
|
|
1438
|
+
);
|
|
1439
|
+
break;
|
|
1440
|
+
case "matrix":
|
|
1441
|
+
ctx.transform(
|
|
1442
|
+
args[0] ?? 1,
|
|
1443
|
+
args[1] ?? 0,
|
|
1444
|
+
args[2] ?? 0,
|
|
1445
|
+
args[3] ?? 1,
|
|
1446
|
+
args[4] ?? 0,
|
|
1447
|
+
args[5] ?? 0
|
|
1448
|
+
);
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
937
1452
|
}
|
|
938
|
-
function drawGroup(ctx, node, inheritedFill, color) {
|
|
939
|
-
const children = node
|
|
940
|
-
if (children
|
|
1453
|
+
function drawGroup(ctx, node, inheritedFill, color, defs = EMPTY_DEFS) {
|
|
1454
|
+
const children = normalizeChildren(node);
|
|
1455
|
+
if (children.length === 0) return;
|
|
941
1456
|
const merged = mergeStyleIntoProps(node.props);
|
|
942
1457
|
const groupFill = resolveCurrentColor(merged.fill, color) ?? inheritedFill;
|
|
943
|
-
const
|
|
944
|
-
|
|
1458
|
+
const transform = merged.transform;
|
|
1459
|
+
if (transform) {
|
|
1460
|
+
ctx.save();
|
|
1461
|
+
applySvgTransform(ctx, transform);
|
|
1462
|
+
}
|
|
1463
|
+
for (const child of children) {
|
|
945
1464
|
if (child != null && typeof child === "object") {
|
|
946
|
-
drawSvgChild(ctx, child, groupFill, color);
|
|
1465
|
+
drawSvgChild(ctx, child, groupFill, color, defs);
|
|
947
1466
|
}
|
|
948
1467
|
}
|
|
1468
|
+
if (transform) {
|
|
1469
|
+
ctx.restore();
|
|
1470
|
+
}
|
|
949
1471
|
}
|
|
950
1472
|
function parsePoints(value) {
|
|
951
1473
|
if (!value) return [];
|
|
@@ -956,26 +1478,36 @@ function parsePoints(value) {
|
|
|
956
1478
|
}
|
|
957
1479
|
return result;
|
|
958
1480
|
}
|
|
959
|
-
function applyFillAndStroke(ctx, props, path, inheritedFill, color) {
|
|
1481
|
+
function applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox) {
|
|
960
1482
|
const fill = resolveCurrentColor(props.fill, color) ?? inheritedFill;
|
|
961
1483
|
const fillRule = props.fillRule ?? props["fill-rule"];
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1484
|
+
const fillRef = parseUrlRef(fill);
|
|
1485
|
+
if (fillRef) {
|
|
1486
|
+
const gradientDef = defs.gradients.get(fillRef);
|
|
1487
|
+
if (gradientDef) {
|
|
1488
|
+
fillWithSvgGradient(ctx, gradientDef, bbox, path, fillRule ?? "nonzero");
|
|
1489
|
+
}
|
|
1490
|
+
} else if (fill !== "none") {
|
|
967
1491
|
ctx.fillStyle = fill;
|
|
968
1492
|
ctx.fill(path, fillRule ?? "nonzero");
|
|
969
1493
|
}
|
|
970
|
-
applyStroke(ctx, props, path, color);
|
|
1494
|
+
applyStroke(ctx, props, path, color, defs, bbox);
|
|
971
1495
|
}
|
|
972
|
-
function applyStroke(ctx, props, path, color) {
|
|
1496
|
+
function applyStroke(ctx, props, path, color, defs, bbox) {
|
|
973
1497
|
const stroke = resolveCurrentColor(props.stroke, color);
|
|
974
1498
|
if (!stroke || stroke === "none") return;
|
|
975
|
-
ctx.strokeStyle = stroke;
|
|
976
1499
|
ctx.lineWidth = Number(props.strokeWidth ?? props["stroke-width"] ?? 1);
|
|
977
1500
|
ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? "butt";
|
|
978
1501
|
ctx.lineJoin = props.strokeLinejoin ?? props["stroke-linejoin"] ?? "miter";
|
|
1502
|
+
const strokeRef = parseUrlRef(stroke);
|
|
1503
|
+
if (strokeRef) {
|
|
1504
|
+
const gradientDef = defs.gradients.get(strokeRef);
|
|
1505
|
+
if (gradientDef) {
|
|
1506
|
+
strokeWithSvgGradient(ctx, gradientDef, bbox, path);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
ctx.strokeStyle = stroke;
|
|
979
1511
|
ctx.stroke(path);
|
|
980
1512
|
}
|
|
981
1513
|
|
|
@@ -1419,12 +1951,12 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
|
1419
1951
|
ctx.strokeRect(x, y, width, height);
|
|
1420
1952
|
}
|
|
1421
1953
|
if (node.textContent !== void 0 && node.textContent !== "") {
|
|
1422
|
-
const paddingTop =
|
|
1423
|
-
const paddingLeft =
|
|
1424
|
-
const paddingRight =
|
|
1425
|
-
const borderTopW =
|
|
1426
|
-
const borderLeftW =
|
|
1427
|
-
const borderRightW =
|
|
1954
|
+
const paddingTop = toNumber(style.paddingTop);
|
|
1955
|
+
const paddingLeft = toNumber(style.paddingLeft);
|
|
1956
|
+
const paddingRight = toNumber(style.paddingRight);
|
|
1957
|
+
const borderTopW = toNumber(style.borderTopWidth);
|
|
1958
|
+
const borderLeftW = toNumber(style.borderLeftWidth);
|
|
1959
|
+
const borderRightW = toNumber(style.borderRightWidth);
|
|
1428
1960
|
const contentX = x + paddingLeft + borderLeftW;
|
|
1429
1961
|
const contentY = y + paddingTop + borderTopW;
|
|
1430
1962
|
const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
|
|
@@ -1445,10 +1977,10 @@ async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
|
1445
1977
|
);
|
|
1446
1978
|
}
|
|
1447
1979
|
if (node.type === "img" && node.props.src) {
|
|
1448
|
-
const paddingTop =
|
|
1449
|
-
const paddingLeft =
|
|
1450
|
-
const paddingRight =
|
|
1451
|
-
const paddingBottom =
|
|
1980
|
+
const paddingTop = toNumber(style.paddingTop);
|
|
1981
|
+
const paddingLeft = toNumber(style.paddingLeft);
|
|
1982
|
+
const paddingRight = toNumber(style.paddingRight);
|
|
1983
|
+
const paddingBottom = toNumber(style.paddingBottom);
|
|
1452
1984
|
const imgX = x + paddingLeft;
|
|
1453
1985
|
const imgY = y + paddingTop;
|
|
1454
1986
|
const imgW = width - paddingLeft - paddingRight;
|
|
@@ -1614,12 +2146,12 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
|
|
|
1614
2146
|
ctx.strokeRect(x, y, width, height);
|
|
1615
2147
|
}
|
|
1616
2148
|
if (node.textContent !== void 0 && node.textContent !== "") {
|
|
1617
|
-
const paddingTop =
|
|
1618
|
-
const paddingLeft =
|
|
1619
|
-
const paddingRight =
|
|
1620
|
-
const borderTopW =
|
|
1621
|
-
const borderLeftW =
|
|
1622
|
-
const borderRightW =
|
|
2149
|
+
const paddingTop = toNumber(style.paddingTop);
|
|
2150
|
+
const paddingLeft = toNumber(style.paddingLeft);
|
|
2151
|
+
const paddingRight = toNumber(style.paddingRight);
|
|
2152
|
+
const borderTopW = toNumber(style.borderTopWidth);
|
|
2153
|
+
const borderLeftW = toNumber(style.borderLeftWidth);
|
|
2154
|
+
const borderRightW = toNumber(style.borderRightWidth);
|
|
1623
2155
|
const contentX = x + paddingLeft + borderLeftW;
|
|
1624
2156
|
const contentY = y + paddingTop + borderTopW;
|
|
1625
2157
|
const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
|
|
@@ -1640,10 +2172,10 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
|
|
|
1640
2172
|
);
|
|
1641
2173
|
}
|
|
1642
2174
|
if (node.type === "img" && node.props.src) {
|
|
1643
|
-
const paddingTop =
|
|
1644
|
-
const paddingLeft =
|
|
1645
|
-
const paddingRight =
|
|
1646
|
-
const paddingBottom =
|
|
2175
|
+
const paddingTop = toNumber(style.paddingTop);
|
|
2176
|
+
const paddingLeft = toNumber(style.paddingLeft);
|
|
2177
|
+
const paddingRight = toNumber(style.paddingRight);
|
|
2178
|
+
const paddingBottom = toNumber(style.paddingBottom);
|
|
1647
2179
|
const imgX = x + paddingLeft;
|
|
1648
2180
|
const imgY = y + paddingTop;
|
|
1649
2181
|
const imgW = width - paddingLeft - paddingRight;
|
|
@@ -1674,6 +2206,10 @@ async function drawNodeInner(ctx, node, parentX, parentY, offsetX, offsetY, debu
|
|
|
1674
2206
|
}
|
|
1675
2207
|
ctx.restore();
|
|
1676
2208
|
}
|
|
2209
|
+
function parseCSSLength(value, referenceSize) {
|
|
2210
|
+
if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
|
|
2211
|
+
return parseFloat(value);
|
|
2212
|
+
}
|
|
1677
2213
|
function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
|
|
1678
2214
|
let ox = x + width / 2;
|
|
1679
2215
|
let oy = y + height / 2;
|
|
@@ -1690,8 +2226,11 @@ function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
|
|
|
1690
2226
|
case "translate":
|
|
1691
2227
|
case "translateX":
|
|
1692
2228
|
case "translateY": {
|
|
1693
|
-
const tx = name === "translateY" ? 0 :
|
|
1694
|
-
const ty = name === "translateX" ? 0 :
|
|
2229
|
+
const tx = name === "translateY" ? 0 : parseCSSLength(values[0], width);
|
|
2230
|
+
const ty = name === "translateX" ? 0 : parseCSSLength(
|
|
2231
|
+
values[name === "translate" ? 1 : 0] ?? "0",
|
|
2232
|
+
height
|
|
2233
|
+
);
|
|
1695
2234
|
ctx.translate(tx, ty);
|
|
1696
2235
|
break;
|
|
1697
2236
|
}
|
|
@@ -1727,8 +2266,7 @@ function resolveOrigin(value, base, size) {
|
|
|
1727
2266
|
if (value === "left" || value === "top") return base;
|
|
1728
2267
|
if (value === "right" || value === "bottom") return base + size;
|
|
1729
2268
|
if (value === "center") return base + size / 2;
|
|
1730
|
-
|
|
1731
|
-
return base + parseFloat(value);
|
|
2269
|
+
return base + parseCSSLength(value, size);
|
|
1732
2270
|
}
|
|
1733
2271
|
function parseAngle(value) {
|
|
1734
2272
|
if (value.endsWith("deg")) return parseFloat(value) * Math.PI / 180;
|
|
@@ -1736,7 +2274,7 @@ function parseAngle(value) {
|
|
|
1736
2274
|
if (value.endsWith("turn")) return parseFloat(value) * 2 * Math.PI;
|
|
1737
2275
|
return parseFloat(value);
|
|
1738
2276
|
}
|
|
1739
|
-
function
|
|
2277
|
+
function toNumber(v) {
|
|
1740
2278
|
if (typeof v === "number") return v;
|
|
1741
2279
|
if (v === void 0 || v === null) return 0;
|
|
1742
2280
|
const n = parseFloat(String(v));
|
|
@@ -1791,7 +2329,7 @@ function parseValue(v) {
|
|
|
1791
2329
|
if (s !== "" && !isNaN(n)) return n;
|
|
1792
2330
|
return s;
|
|
1793
2331
|
}
|
|
1794
|
-
function expandStyle(raw) {
|
|
2332
|
+
function expandStyle(raw, fontFamilies) {
|
|
1795
2333
|
const style = { ...raw };
|
|
1796
2334
|
if (style.margin !== void 0) {
|
|
1797
2335
|
const sides = parseSides(String(style.margin));
|
|
@@ -1862,6 +2400,20 @@ function expandStyle(raw) {
|
|
|
1862
2400
|
}
|
|
1863
2401
|
delete style.border;
|
|
1864
2402
|
}
|
|
2403
|
+
for (const side of SIDES) {
|
|
2404
|
+
const key = `border${side}`;
|
|
2405
|
+
if (style[key] !== void 0) {
|
|
2406
|
+
const parts = String(style[key]).split(/\s+/);
|
|
2407
|
+
const width = parseValue(parts[0]);
|
|
2408
|
+
const borderStyle = parts[1] ?? "solid";
|
|
2409
|
+
const color = parts[2] ?? "black";
|
|
2410
|
+
if (style[`${key}Width`] === void 0) style[`${key}Width`] = width;
|
|
2411
|
+
if (style[`${key}Style`] === void 0)
|
|
2412
|
+
style[`${key}Style`] = borderStyle;
|
|
2413
|
+
if (style[`${key}Color`] === void 0) style[`${key}Color`] = color;
|
|
2414
|
+
delete style[key];
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
1865
2417
|
if (style.flex !== void 0) {
|
|
1866
2418
|
const val = String(style.flex);
|
|
1867
2419
|
const parts = val.split(/\s+/);
|
|
@@ -1903,13 +2455,34 @@ function expandStyle(raw) {
|
|
|
1903
2455
|
if (style.overflowX === void 0) style.overflowX = style.overflow;
|
|
1904
2456
|
if (style.overflowY === void 0) style.overflowY = style.overflow;
|
|
1905
2457
|
}
|
|
2458
|
+
if (style.textBox !== void 0) {
|
|
2459
|
+
const val = String(style.textBox);
|
|
2460
|
+
if (val === "normal" || val === "none") {
|
|
2461
|
+
style.textBoxTrim ??= "none";
|
|
2462
|
+
} else {
|
|
2463
|
+
const parts = val.split(/\s+/);
|
|
2464
|
+
style.textBoxTrim ??= parts[0];
|
|
2465
|
+
if (parts.length > 1) {
|
|
2466
|
+
style.textBoxEdge ??= parts.slice(1).join(" ");
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
delete style.textBox;
|
|
2470
|
+
}
|
|
1906
2471
|
if (typeof style.fontFamily === "string") {
|
|
1907
|
-
|
|
2472
|
+
const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
2473
|
+
if (fontFamilies) {
|
|
2474
|
+
const present = new Set(families);
|
|
2475
|
+
for (const name of fontFamilies) {
|
|
2476
|
+
if (!present.has(name)) families.push(name);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
style.fontFamily = families.join(", ");
|
|
1908
2480
|
}
|
|
1909
2481
|
return style;
|
|
1910
2482
|
}
|
|
1911
2483
|
|
|
1912
2484
|
// src/jsx/style/compute.ts
|
|
2485
|
+
var ROOT_FONT_SIZE = 16;
|
|
1913
2486
|
var DEFAULT_STYLE = {
|
|
1914
2487
|
display: "flex",
|
|
1915
2488
|
flexDirection: "row",
|
|
@@ -1919,7 +2492,7 @@ var DEFAULT_STYLE = {
|
|
|
1919
2492
|
alignItems: "stretch",
|
|
1920
2493
|
justifyContent: "flex-start",
|
|
1921
2494
|
position: "relative",
|
|
1922
|
-
fontSize:
|
|
2495
|
+
fontSize: ROOT_FONT_SIZE,
|
|
1923
2496
|
fontWeight: 400,
|
|
1924
2497
|
fontStyle: "normal",
|
|
1925
2498
|
color: "black",
|
|
@@ -1944,7 +2517,9 @@ var INHERITABLE_PROPS = [
|
|
|
1944
2517
|
"letterSpacing",
|
|
1945
2518
|
"whiteSpace",
|
|
1946
2519
|
"wordBreak",
|
|
1947
|
-
"textOverflow"
|
|
2520
|
+
"textOverflow",
|
|
2521
|
+
"textBoxTrim",
|
|
2522
|
+
"textBoxEdge"
|
|
1948
2523
|
];
|
|
1949
2524
|
function resolveStyle(rawStyle, parentStyle) {
|
|
1950
2525
|
const style = { ...rawStyle };
|
|
@@ -1954,8 +2529,15 @@ function resolveStyle(rawStyle, parentStyle) {
|
|
|
1954
2529
|
}
|
|
1955
2530
|
}
|
|
1956
2531
|
if (typeof style.fontSize === "string") {
|
|
1957
|
-
const
|
|
1958
|
-
|
|
2532
|
+
const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
2533
|
+
const resolved = resolveUnit(
|
|
2534
|
+
style.fontSize,
|
|
2535
|
+
0,
|
|
2536
|
+
0,
|
|
2537
|
+
parentFontSize,
|
|
2538
|
+
ROOT_FONT_SIZE
|
|
2539
|
+
);
|
|
2540
|
+
style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
|
|
1959
2541
|
}
|
|
1960
2542
|
if (typeof style.lineHeight === "string") {
|
|
1961
2543
|
const str = style.lineHeight;
|
|
@@ -1969,9 +2551,16 @@ function resolveStyle(rawStyle, parentStyle) {
|
|
|
1969
2551
|
}
|
|
1970
2552
|
}
|
|
1971
2553
|
if (typeof style.letterSpacing === "string") {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
style.letterSpacing
|
|
2554
|
+
const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
2555
|
+
const resolved = resolveUnit(
|
|
2556
|
+
style.letterSpacing,
|
|
2557
|
+
0,
|
|
2558
|
+
0,
|
|
2559
|
+
currentFontSize,
|
|
2560
|
+
ROOT_FONT_SIZE
|
|
2561
|
+
);
|
|
2562
|
+
if (typeof resolved === "number") {
|
|
2563
|
+
style.letterSpacing = resolved;
|
|
1975
2564
|
}
|
|
1976
2565
|
}
|
|
1977
2566
|
return style;
|
|
@@ -2059,7 +2648,7 @@ function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSiz
|
|
|
2059
2648
|
}
|
|
2060
2649
|
return value;
|
|
2061
2650
|
}
|
|
2062
|
-
function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize =
|
|
2651
|
+
function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
|
|
2063
2652
|
const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
|
|
2064
2653
|
for (const prop of DIMENSION_PROPS) {
|
|
2065
2654
|
const value = style[prop];
|
|
@@ -2075,8 +2664,41 @@ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = DEFAU
|
|
|
2075
2664
|
style[prop] = resolved;
|
|
2076
2665
|
}
|
|
2077
2666
|
}
|
|
2667
|
+
if (style.transform) {
|
|
2668
|
+
style.transform = resolveTransformUnits(
|
|
2669
|
+
style.transform,
|
|
2670
|
+
viewportWidth,
|
|
2671
|
+
viewportHeight,
|
|
2672
|
+
fontSize,
|
|
2673
|
+
rootFontSize
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
if (style.transformOrigin) {
|
|
2677
|
+
style.transformOrigin = resolveTransformUnits(
|
|
2678
|
+
style.transformOrigin,
|
|
2679
|
+
viewportWidth,
|
|
2680
|
+
viewportHeight,
|
|
2681
|
+
fontSize,
|
|
2682
|
+
rootFontSize
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2078
2685
|
return style;
|
|
2079
2686
|
}
|
|
2687
|
+
function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
|
|
2688
|
+
return transform.replace(
|
|
2689
|
+
/(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
|
|
2690
|
+
(match) => {
|
|
2691
|
+
const resolved = resolveUnit(
|
|
2692
|
+
match,
|
|
2693
|
+
viewportWidth,
|
|
2694
|
+
viewportHeight,
|
|
2695
|
+
fontSize,
|
|
2696
|
+
rootFontSize
|
|
2697
|
+
);
|
|
2698
|
+
return typeof resolved === "number" ? String(resolved) : match;
|
|
2699
|
+
}
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2080
2702
|
|
|
2081
2703
|
// src/jsx/yoga.ts
|
|
2082
2704
|
import Yoga, {
|
|
@@ -2265,16 +2887,18 @@ function applyEdgeValue(node, setter, edge, value) {
|
|
|
2265
2887
|
}
|
|
2266
2888
|
|
|
2267
2889
|
// src/jsx/layout.ts
|
|
2268
|
-
async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled) {
|
|
2890
|
+
async function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled, fontFamilies) {
|
|
2269
2891
|
const rootYogaNode = createYogaNode();
|
|
2892
|
+
const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
|
|
2270
2893
|
const rootNode = await buildNode(
|
|
2271
2894
|
element,
|
|
2272
|
-
|
|
2895
|
+
rootStyle,
|
|
2273
2896
|
rootYogaNode,
|
|
2274
2897
|
containerWidth,
|
|
2275
2898
|
containerHeight,
|
|
2276
2899
|
ctx,
|
|
2277
|
-
emojiEnabled
|
|
2900
|
+
emojiEnabled,
|
|
2901
|
+
fontFamilies
|
|
2278
2902
|
);
|
|
2279
2903
|
rootYogaNode.setWidth(containerWidth);
|
|
2280
2904
|
rootYogaNode.setHeight(containerHeight);
|
|
@@ -2283,7 +2907,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
2283
2907
|
freeYogaNode(rootYogaNode);
|
|
2284
2908
|
return layoutTree;
|
|
2285
2909
|
}
|
|
2286
|
-
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled) {
|
|
2910
|
+
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, ctx, emojiEnabled, fontFamilies) {
|
|
2287
2911
|
if (element === null || element === void 0 || typeof element === "boolean") {
|
|
2288
2912
|
return {
|
|
2289
2913
|
type: "empty",
|
|
@@ -2324,7 +2948,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
2324
2948
|
viewportWidth,
|
|
2325
2949
|
viewportHeight,
|
|
2326
2950
|
ctx,
|
|
2327
|
-
emojiEnabled
|
|
2951
|
+
emojiEnabled,
|
|
2952
|
+
fontFamilies
|
|
2328
2953
|
)
|
|
2329
2954
|
);
|
|
2330
2955
|
}
|
|
@@ -2349,12 +2974,13 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
2349
2974
|
viewportWidth,
|
|
2350
2975
|
viewportHeight,
|
|
2351
2976
|
ctx,
|
|
2352
|
-
emojiEnabled
|
|
2977
|
+
emojiEnabled,
|
|
2978
|
+
fontFamilies
|
|
2353
2979
|
);
|
|
2354
2980
|
}
|
|
2355
2981
|
const props = el.props ?? {};
|
|
2356
2982
|
const rawStyle = props.style ?? {};
|
|
2357
|
-
const expanded = expandStyle(rawStyle);
|
|
2983
|
+
const expanded = expandStyle(rawStyle, fontFamilies);
|
|
2358
2984
|
const style = resolveStyle(expanded, parentStyle);
|
|
2359
2985
|
resolveUnits(style, viewportWidth, viewportHeight);
|
|
2360
2986
|
const tagName = String(type);
|
|
@@ -2472,7 +3098,8 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
2472
3098
|
viewportWidth,
|
|
2473
3099
|
viewportHeight,
|
|
2474
3100
|
ctx,
|
|
2475
|
-
emojiEnabled
|
|
3101
|
+
emojiEnabled,
|
|
3102
|
+
fontFamilies
|
|
2476
3103
|
)
|
|
2477
3104
|
);
|
|
2478
3105
|
}
|
|
@@ -2540,12 +3167,14 @@ async function renderReactElement(ctx, element, options) {
|
|
|
2540
3167
|
const width = ctx.canvas.width;
|
|
2541
3168
|
const height = ctx.canvas.height;
|
|
2542
3169
|
const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
|
|
3170
|
+
const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
|
|
2543
3171
|
const layoutTree = await buildLayoutTree(
|
|
2544
3172
|
element,
|
|
2545
3173
|
width,
|
|
2546
3174
|
height,
|
|
2547
3175
|
ctx,
|
|
2548
|
-
!!emojiStyle
|
|
3176
|
+
!!emojiStyle,
|
|
3177
|
+
fontFamilies
|
|
2549
3178
|
);
|
|
2550
3179
|
await drawNode(ctx, layoutTree, 0, 0, options.debug ?? false, emojiStyle);
|
|
2551
3180
|
}
|