@effing/canvas 0.18.6 → 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
@@ -110,6 +110,54 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
110
110
  height: ascent + descent
111
111
  };
112
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
+ }
113
161
  function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx, letterSpacing = 0) {
114
162
  const base = measureText(
115
163
  word,
@@ -295,6 +343,28 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
295
343
  totalHeight += lineHeightPx;
296
344
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
297
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
+ }
298
368
  return {
299
369
  segments,
300
370
  width: maxLineWidth,
@@ -823,20 +893,19 @@ function mergeStyleIntoProps(props) {
823
893
  return { ...props, ...style };
824
894
  }
825
895
  function collectDefs(children) {
826
- const defs = /* @__PURE__ */ new Map();
896
+ const clips = /* @__PURE__ */ new Map();
897
+ const gradients = /* @__PURE__ */ new Map();
827
898
  for (const child of children) {
828
899
  if (child.type !== "defs") continue;
829
- const defsChildren = normalizeChildren(child);
830
- for (const def of defsChildren) {
831
- if (def.type === "clipPath") {
832
- const id = def.props.id;
833
- if (id) {
834
- defs.set(id, normalizeChildren(def));
835
- }
836
- }
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);
837
906
  }
838
907
  }
839
- return defs;
908
+ return { clips, gradients };
840
909
  }
841
910
  function parseUrlRef(value) {
842
911
  if (typeof value !== "string") return void 0;
@@ -848,6 +917,259 @@ function normalizeChildren(node) {
848
917
  if (raw == null) return [];
849
918
  return Array.isArray(raw) ? raw : [raw];
850
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
+ }
851
1173
  function buildPath(child) {
852
1174
  const props = mergeStyleIntoProps(child.props);
853
1175
  switch (child.type) {
@@ -943,36 +1265,40 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
943
1265
  }
944
1266
  ctx.restore();
945
1267
  }
946
- function drawSvgChild(ctx, child, inheritedFill, color, defs = /* @__PURE__ */ new Map()) {
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) {
947
1273
  const { type } = child;
948
1274
  const props = mergeStyleIntoProps(child.props);
949
1275
  if (type === "defs" || type === "clipPath") return;
950
1276
  const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
951
- const clipShapes = clipRef ? defs.get(clipRef) : void 0;
1277
+ const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
952
1278
  const clipPath = clipShapes ? buildClipPath(clipShapes) : void 0;
953
1279
  if (clipPath) ctx.save();
954
1280
  if (clipPath) ctx.clip(clipPath);
955
1281
  switch (type) {
956
1282
  case "path":
957
- drawPath(ctx, props, inheritedFill, color);
1283
+ drawPath(ctx, props, inheritedFill, color, defs);
958
1284
  break;
959
1285
  case "circle":
960
- drawCircle(ctx, props, inheritedFill, color);
1286
+ drawCircle(ctx, props, inheritedFill, color, defs);
961
1287
  break;
962
1288
  case "rect":
963
- drawSvgRect(ctx, props, inheritedFill, color);
1289
+ drawSvgRect(ctx, props, inheritedFill, color, defs);
964
1290
  break;
965
1291
  case "line":
966
- drawLine(ctx, props, color);
1292
+ drawLine(ctx, props, color, defs);
967
1293
  break;
968
1294
  case "ellipse":
969
- drawEllipse(ctx, props, inheritedFill, color);
1295
+ drawEllipse(ctx, props, inheritedFill, color, defs);
970
1296
  break;
971
1297
  case "polygon":
972
- drawPolygon(ctx, props, inheritedFill, color);
1298
+ drawPolygon(ctx, props, inheritedFill, color, defs);
973
1299
  break;
974
1300
  case "polyline":
975
- drawPolyline(ctx, props, inheritedFill, color);
1301
+ drawPolyline(ctx, props, inheritedFill, color, defs);
976
1302
  break;
977
1303
  case "g":
978
1304
  drawGroup(ctx, child, inheritedFill, color, defs);
@@ -980,22 +1306,24 @@ function drawSvgChild(ctx, child, inheritedFill, color, defs = /* @__PURE__ */ n
980
1306
  }
981
1307
  if (clipPath) ctx.restore();
982
1308
  }
983
- function drawPath(ctx, props, inheritedFill, color) {
1309
+ function drawPath(ctx, props, inheritedFill, color, defs) {
984
1310
  const d = props.d;
985
1311
  if (!d) return;
986
1312
  const path = new Path2D(d);
987
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
1313
+ const bbox = pathBBox(d);
1314
+ applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
988
1315
  }
989
- function drawCircle(ctx, props, inheritedFill, color) {
1316
+ function drawCircle(ctx, props, inheritedFill, color, defs) {
990
1317
  const cx = Number(props.cx ?? 0);
991
1318
  const cy = Number(props.cy ?? 0);
992
1319
  const r = Number(props.r ?? 0);
993
1320
  if (r <= 0) return;
994
1321
  const path = new Path2D();
995
1322
  path.arc(cx, cy, r, 0, Math.PI * 2);
996
- 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);
997
1325
  }
998
- function drawSvgRect(ctx, props, inheritedFill, color) {
1326
+ function drawSvgRect(ctx, props, inheritedFill, color, defs) {
999
1327
  const rx = Number(props.x ?? 0);
1000
1328
  const ry = Number(props.y ?? 0);
1001
1329
  const w = Number(props.width ?? 0);
@@ -1003,9 +1331,10 @@ function drawSvgRect(ctx, props, inheritedFill, color) {
1003
1331
  if (w <= 0 || h <= 0) return;
1004
1332
  const path = new Path2D();
1005
1333
  path.rect(rx, ry, w, h);
1006
- 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);
1007
1336
  }
1008
- function drawLine(ctx, props, color) {
1337
+ function drawLine(ctx, props, color, defs) {
1009
1338
  const x1 = Number(props.x1 ?? 0);
1010
1339
  const y1 = Number(props.y1 ?? 0);
1011
1340
  const x2 = Number(props.x2 ?? 0);
@@ -1013,9 +1342,15 @@ function drawLine(ctx, props, color) {
1013
1342
  const path = new Path2D();
1014
1343
  path.moveTo(x1, y1);
1015
1344
  path.lineTo(x2, y2);
1016
- 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);
1017
1352
  }
1018
- function drawEllipse(ctx, props, inheritedFill, color) {
1353
+ function drawEllipse(ctx, props, inheritedFill, color, defs) {
1019
1354
  const cx = Number(props.cx ?? 0);
1020
1355
  const cy = Number(props.cy ?? 0);
1021
1356
  const rx = Number(props.rx ?? 0);
@@ -1023,9 +1358,15 @@ function drawEllipse(ctx, props, inheritedFill, color) {
1023
1358
  if (rx <= 0 || ry <= 0) return;
1024
1359
  const path = new Path2D();
1025
1360
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
1026
- 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);
1027
1368
  }
1028
- function drawPolygon(ctx, props, inheritedFill, color) {
1369
+ function drawPolygon(ctx, props, inheritedFill, color, defs) {
1029
1370
  const points = parsePoints(props.points);
1030
1371
  if (points.length < 2) return;
1031
1372
  const path = new Path2D();
@@ -1034,9 +1375,10 @@ function drawPolygon(ctx, props, inheritedFill, color) {
1034
1375
  path.lineTo(points[i][0], points[i][1]);
1035
1376
  }
1036
1377
  path.closePath();
1037
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
1378
+ const bbox = pointsBBox(points);
1379
+ applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
1038
1380
  }
1039
- function drawPolyline(ctx, props, inheritedFill, color) {
1381
+ function drawPolyline(ctx, props, inheritedFill, color, defs) {
1040
1382
  const points = parsePoints(props.points);
1041
1383
  if (points.length < 2) return;
1042
1384
  const path = new Path2D();
@@ -1044,18 +1386,88 @@ function drawPolyline(ctx, props, inheritedFill, color) {
1044
1386
  for (let i = 1; i < points.length; i++) {
1045
1387
  path.lineTo(points[i][0], points[i][1]);
1046
1388
  }
1047
- applyFillAndStroke(ctx, props, path, inheritedFill, color);
1389
+ const bbox = pointsBBox(points);
1390
+ applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
1048
1391
  }
1049
- function drawGroup(ctx, node, inheritedFill, color, defs = /* @__PURE__ */ new Map()) {
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
+ }
1452
+ }
1453
+ function drawGroup(ctx, node, inheritedFill, color, defs = EMPTY_DEFS) {
1050
1454
  const children = normalizeChildren(node);
1051
1455
  if (children.length === 0) return;
1052
1456
  const merged = mergeStyleIntoProps(node.props);
1053
1457
  const groupFill = resolveCurrentColor(merged.fill, color) ?? inheritedFill;
1458
+ const transform = merged.transform;
1459
+ if (transform) {
1460
+ ctx.save();
1461
+ applySvgTransform(ctx, transform);
1462
+ }
1054
1463
  for (const child of children) {
1055
1464
  if (child != null && typeof child === "object") {
1056
1465
  drawSvgChild(ctx, child, groupFill, color, defs);
1057
1466
  }
1058
1467
  }
1468
+ if (transform) {
1469
+ ctx.restore();
1470
+ }
1059
1471
  }
1060
1472
  function parsePoints(value) {
1061
1473
  if (!value) return [];
@@ -1066,26 +1478,36 @@ function parsePoints(value) {
1066
1478
  }
1067
1479
  return result;
1068
1480
  }
1069
- function applyFillAndStroke(ctx, props, path, inheritedFill, color) {
1481
+ function applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox) {
1070
1482
  const fill = resolveCurrentColor(props.fill, color) ?? inheritedFill;
1071
1483
  const fillRule = props.fillRule ?? props["fill-rule"];
1072
- const clipRule = props.clipRule ?? props["clip-rule"];
1073
- if (clipRule) {
1074
- ctx.clip(path, clipRule);
1075
- }
1076
- 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") {
1077
1491
  ctx.fillStyle = fill;
1078
1492
  ctx.fill(path, fillRule ?? "nonzero");
1079
1493
  }
1080
- applyStroke(ctx, props, path, color);
1494
+ applyStroke(ctx, props, path, color, defs, bbox);
1081
1495
  }
1082
- function applyStroke(ctx, props, path, color) {
1496
+ function applyStroke(ctx, props, path, color, defs, bbox) {
1083
1497
  const stroke = resolveCurrentColor(props.stroke, color);
1084
1498
  if (!stroke || stroke === "none") return;
1085
- ctx.strokeStyle = stroke;
1086
1499
  ctx.lineWidth = Number(props.strokeWidth ?? props["stroke-width"] ?? 1);
1087
1500
  ctx.lineCap = props.strokeLinecap ?? props["stroke-linecap"] ?? "butt";
1088
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;
1089
1511
  ctx.stroke(path);
1090
1512
  }
1091
1513
 
@@ -1978,6 +2400,20 @@ function expandStyle(raw, fontFamilies) {
1978
2400
  }
1979
2401
  delete style.border;
1980
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
+ }
1981
2417
  if (style.flex !== void 0) {
1982
2418
  const val = String(style.flex);
1983
2419
  const parts = val.split(/\s+/);
@@ -2019,6 +2455,19 @@ function expandStyle(raw, fontFamilies) {
2019
2455
  if (style.overflowX === void 0) style.overflowX = style.overflow;
2020
2456
  if (style.overflowY === void 0) style.overflowY = style.overflow;
2021
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
+ }
2022
2471
  if (typeof style.fontFamily === "string") {
2023
2472
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
2024
2473
  if (fontFamilies) {
@@ -2033,6 +2482,7 @@ function expandStyle(raw, fontFamilies) {
2033
2482
  }
2034
2483
 
2035
2484
  // src/jsx/style/compute.ts
2485
+ var ROOT_FONT_SIZE = 16;
2036
2486
  var DEFAULT_STYLE = {
2037
2487
  display: "flex",
2038
2488
  flexDirection: "row",
@@ -2042,7 +2492,7 @@ var DEFAULT_STYLE = {
2042
2492
  alignItems: "stretch",
2043
2493
  justifyContent: "flex-start",
2044
2494
  position: "relative",
2045
- fontSize: 16,
2495
+ fontSize: ROOT_FONT_SIZE,
2046
2496
  fontWeight: 400,
2047
2497
  fontStyle: "normal",
2048
2498
  color: "black",
@@ -2067,7 +2517,9 @@ var INHERITABLE_PROPS = [
2067
2517
  "letterSpacing",
2068
2518
  "whiteSpace",
2069
2519
  "wordBreak",
2070
- "textOverflow"
2520
+ "textOverflow",
2521
+ "textBoxTrim",
2522
+ "textBoxEdge"
2071
2523
  ];
2072
2524
  function resolveStyle(rawStyle, parentStyle) {
2073
2525
  const style = { ...rawStyle };
@@ -2077,8 +2529,15 @@ function resolveStyle(rawStyle, parentStyle) {
2077
2529
  }
2078
2530
  }
2079
2531
  if (typeof style.fontSize === "string") {
2080
- const parsed = parseFloat(style.fontSize);
2081
- 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;
2082
2541
  }
2083
2542
  if (typeof style.lineHeight === "string") {
2084
2543
  const str = style.lineHeight;
@@ -2092,9 +2551,16 @@ function resolveStyle(rawStyle, parentStyle) {
2092
2551
  }
2093
2552
  }
2094
2553
  if (typeof style.letterSpacing === "string") {
2095
- const parsed = parseFloat(style.letterSpacing);
2096
- if (!isNaN(parsed)) {
2097
- 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;
2098
2564
  }
2099
2565
  }
2100
2566
  return style;
@@ -2182,7 +2648,7 @@ function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSiz
2182
2648
  }
2183
2649
  return value;
2184
2650
  }
2185
- function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = DEFAULT_STYLE.fontSize) {
2651
+ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
2186
2652
  const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
2187
2653
  for (const prop of DIMENSION_PROPS) {
2188
2654
  const value = style[prop];