@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 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
- ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
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" && v.endsWith("%")) {
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
- for (const child of childArray) {
835
- if (child != null && typeof child === "object") {
836
- drawSvgChild(ctx, child, inheritedFill, color);
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
- function drawSvgChild(ctx, child, inheritedFill, color) {
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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
- applyStroke(ctx, props, path, color);
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
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.children ?? node.props.children;
940
- if (children == null) return;
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 childArray = Array.isArray(children) ? children : [children];
944
- for (const child of childArray) {
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 clipRule = props.clipRule ?? props["clip-rule"];
963
- if (clipRule) {
964
- ctx.clip(path, clipRule);
965
- }
966
- if (fill !== "none") {
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 = toNumber2(style.paddingTop);
1423
- const paddingLeft = toNumber2(style.paddingLeft);
1424
- const paddingRight = toNumber2(style.paddingRight);
1425
- const borderTopW = toNumber2(style.borderTopWidth);
1426
- const borderLeftW = toNumber2(style.borderLeftWidth);
1427
- const borderRightW = toNumber2(style.borderRightWidth);
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 = toNumber2(style.paddingTop);
1449
- const paddingLeft = toNumber2(style.paddingLeft);
1450
- const paddingRight = toNumber2(style.paddingRight);
1451
- const paddingBottom = toNumber2(style.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 = toNumber2(style.paddingTop);
1618
- const paddingLeft = toNumber2(style.paddingLeft);
1619
- const paddingRight = toNumber2(style.paddingRight);
1620
- const borderTopW = toNumber2(style.borderTopWidth);
1621
- const borderLeftW = toNumber2(style.borderLeftWidth);
1622
- const borderRightW = toNumber2(style.borderRightWidth);
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 = toNumber2(style.paddingTop);
1644
- const paddingLeft = toNumber2(style.paddingLeft);
1645
- const paddingRight = toNumber2(style.paddingRight);
1646
- const paddingBottom = toNumber2(style.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 : parseFloat(values[0]);
1694
- const ty = name === "translateX" ? 0 : parseFloat(values[name === "translate" ? 1 : 0] ?? "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
- if (value.endsWith("%")) return base + parseFloat(value) / 100 * size;
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 toNumber2(v) {
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
- style.fontFamily = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean).join(", ");
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: 16,
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 parsed = parseFloat(style.fontSize);
1958
- style.fontSize = isNaN(parsed) ? parentStyle.fontSize : parsed;
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 parsed = parseFloat(style.letterSpacing);
1973
- if (!isNaN(parsed)) {
1974
- style.letterSpacing = parsed;
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 = DEFAULT_STYLE.fontSize) {
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
- DEFAULT_STYLE,
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
  }