@effing/canvas 0.40.1 → 0.41.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
@@ -3,7 +3,16 @@ import { createCanvas as _createCanvas } from "@napi-rs/canvas";
3
3
  import {
4
4
  Canvas,
5
5
  GlobalFonts as GlobalFonts2,
6
- Image
6
+ Image,
7
+ ImageData,
8
+ Path2D as Path2D5,
9
+ DOMMatrix,
10
+ DOMPoint,
11
+ DOMRect,
12
+ PathOp,
13
+ FillType,
14
+ StrokeJoin,
15
+ StrokeCap
7
16
  } from "@napi-rs/canvas";
8
17
 
9
18
  // src/image.ts
@@ -59,6 +68,30 @@ function renderLottieFrame(ctx, animation, frame) {
59
68
  animation.render(ctx);
60
69
  }
61
70
 
71
+ // src/jsx/draw/utils.ts
72
+ function parseCSSLength(value, referenceSize) {
73
+ if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
74
+ return parseFloat(value);
75
+ }
76
+ function toNumber(v) {
77
+ if (typeof v === "number") return v;
78
+ if (v === void 0 || v === null) return 0;
79
+ const n = parseFloat(String(v));
80
+ return isNaN(n) ? 0 : n;
81
+ }
82
+ function resolveBoxValue(v, referenceWidth) {
83
+ if (typeof v === "number") return v;
84
+ if (v === void 0 || v === null) return 0;
85
+ const s = String(v);
86
+ if (s.endsWith("%")) return parseFloat(s) / 100 * referenceWidth;
87
+ const n = parseFloat(s);
88
+ return isNaN(n) ? 0 : n;
89
+ }
90
+ function formatCSSNumber(n) {
91
+ const fixed = n.toFixed(6).replace(/\.?0+$/, "");
92
+ return fixed === "-0" || fixed === "" ? "0" : fixed;
93
+ }
94
+
62
95
  // src/jsx/style/compute.ts
63
96
  var ROOT_FONT_SIZE = 16;
64
97
  var DEFAULT_FONT_FAMILY = "Helvetica, Arial, sans-serif";
@@ -249,29 +282,31 @@ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_
249
282
  style[prop] = resolved;
250
283
  }
251
284
  }
252
- if (style.transform) {
253
- style.transform = resolveTransformUnits(
254
- style.transform,
255
- viewportWidth,
256
- viewportHeight,
257
- fontSize,
258
- rootFontSize
259
- );
260
- }
261
- if (style.transformOrigin) {
262
- style.transformOrigin = resolveTransformUnits(
263
- style.transformOrigin,
285
+ for (const [prop, suffix] of LENGTH_STRING_PROPS) {
286
+ const value = style[prop];
287
+ if (typeof value !== "string") continue;
288
+ style[prop] = resolveLengthUnits(
289
+ value,
264
290
  viewportWidth,
265
291
  viewportHeight,
266
292
  fontSize,
267
- rootFontSize
293
+ rootFontSize,
294
+ suffix
268
295
  );
269
296
  }
270
297
  return style;
271
298
  }
272
- function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
299
+ var LENGTH_STRING_PROPS = [
300
+ ["transform", ""],
301
+ ["transformOrigin", ""],
302
+ ["clipPath", "px"],
303
+ ["filter", "px"],
304
+ ["backdropFilter", "px"]
305
+ ];
306
+ function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize, suffix = "") {
273
307
  return transform.replace(
274
- /(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
308
+ // The lookbehind keeps a number from being matched mid-token (`1e1em`).
309
+ /(?<![\w.])(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
275
310
  (match) => {
276
311
  const resolved = resolveUnit(
277
312
  match,
@@ -280,7 +315,7 @@ function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSiz
280
315
  fontSize,
281
316
  rootFontSize
282
317
  );
283
- return typeof resolved === "number" ? String(resolved) : match;
318
+ return typeof resolved === "number" ? `${formatCSSNumber(resolved)}${suffix}` : match;
284
319
  }
285
320
  );
286
321
  }
@@ -998,7 +1033,7 @@ function applyClip(ctx, x, y, width, height, borderRadius) {
998
1033
  ctx.clip();
999
1034
  }
1000
1035
  function roundedRect(ctx, x, y, w, h, tl, tr, br, bl) {
1001
- const maxR = Math.min(w, h) / 2;
1036
+ const maxR = maxCornerRadius(w, h);
1002
1037
  tl = Math.min(tl, maxR);
1003
1038
  tr = Math.min(tr, maxR);
1004
1039
  br = Math.min(br, maxR);
@@ -1014,188 +1049,13 @@ function roundedRect(ctx, x, y, w, h, tl, tr, br, bl) {
1014
1049
  if (tl > 0) ctx.arcTo(x, y, x + tl, y, tl);
1015
1050
  ctx.closePath();
1016
1051
  }
1052
+ function maxCornerRadius(w, h) {
1053
+ return Math.min(w, h) / 2;
1054
+ }
1017
1055
  function hasRadius(r) {
1018
1056
  return r.topLeft > 0 || r.topRight > 0 || r.bottomRight > 0 || r.bottomLeft > 0;
1019
1057
  }
1020
1058
 
1021
- // src/jsx/draw/gradient.ts
1022
- function createGradientFromCSS(ctx, cssGradient, x, y, width, height) {
1023
- const trimmed = cssGradient.trim();
1024
- if (trimmed.startsWith("linear-gradient")) {
1025
- return parseLinearGradient(ctx, trimmed, x, y, width, height);
1026
- }
1027
- if (trimmed.startsWith("radial-gradient")) {
1028
- return parseRadialGradient(ctx, trimmed, x, y, width, height);
1029
- }
1030
- return null;
1031
- }
1032
- function parseLinearGradient(ctx, css, x, y, width, height) {
1033
- const match = css.match(/linear-gradient\((.*)\)/s);
1034
- if (!match) return null;
1035
- const content = match[1].trim();
1036
- const parts = splitGradientArgs(content);
1037
- let angle = 180;
1038
- let colorStartIdx = 0;
1039
- const first = parts[0]?.trim();
1040
- if (first) {
1041
- if (first.startsWith("to ")) {
1042
- angle = directionToAngle(first);
1043
- colorStartIdx = 1;
1044
- } else if (first.endsWith("deg")) {
1045
- angle = parseFloat(first);
1046
- colorStartIdx = 1;
1047
- } else if (first.endsWith("turn")) {
1048
- angle = parseFloat(first) * 360;
1049
- colorStartIdx = 1;
1050
- }
1051
- }
1052
- const rad = (angle - 90) * Math.PI / 180;
1053
- const cx = x + width / 2;
1054
- const cy = y + height / 2;
1055
- const halfDiag = Math.abs(width * Math.cos(rad)) / 2 + Math.abs(height * Math.sin(rad)) / 2;
1056
- const x0 = cx - halfDiag * Math.cos(rad);
1057
- const y0 = cy - halfDiag * Math.sin(rad);
1058
- const x1 = cx + halfDiag * Math.cos(rad);
1059
- const y1 = cy + halfDiag * Math.sin(rad);
1060
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
1061
- const stops = parts.slice(colorStartIdx);
1062
- addColorStops(gradient, stops);
1063
- return gradient;
1064
- }
1065
- function parseRadialGradient(ctx, css, x, y, width, height) {
1066
- const match = css.match(/radial-gradient\((.*)\)/s);
1067
- if (!match) return null;
1068
- const content = match[1].trim();
1069
- const parts = splitGradientArgs(content);
1070
- const cx = x + width / 2;
1071
- const cy = y + height / 2;
1072
- const radius = Math.max(width, height) / 2;
1073
- const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
1074
- let colorStartIdx = 0;
1075
- const first = parts[0]?.trim() ?? "";
1076
- if (first.startsWith("circle") || first.startsWith("ellipse") || first.startsWith("closest") || first.startsWith("farthest")) {
1077
- colorStartIdx = 1;
1078
- }
1079
- addColorStops(gradient, parts.slice(colorStartIdx));
1080
- return gradient;
1081
- }
1082
- function addColorStops(gradient, stops) {
1083
- if (stops.length === 0) return;
1084
- for (let i = 0; i < stops.length; i++) {
1085
- const stop = stops[i].trim();
1086
- const percentMatch = stop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);
1087
- if (percentMatch) {
1088
- const color = percentMatch[1];
1089
- const pos = percentMatch[2];
1090
- const offset = pos.endsWith("%") ? parseFloat(pos) / 100 : parseFloat(pos);
1091
- gradient.addColorStop(Math.max(0, Math.min(1, offset)), color);
1092
- } else {
1093
- const offset = stops.length === 1 ? 0.5 : i / (stops.length - 1);
1094
- gradient.addColorStop(offset, stop);
1095
- }
1096
- }
1097
- }
1098
- function directionToAngle(dir) {
1099
- const map = {
1100
- "to top": 0,
1101
- "to right": 90,
1102
- "to bottom": 180,
1103
- "to left": 270,
1104
- "to top right": 45,
1105
- "to top left": 315,
1106
- "to bottom right": 135,
1107
- "to bottom left": 225
1108
- };
1109
- return map[dir] ?? 180;
1110
- }
1111
- function splitGradientArgs(content) {
1112
- const parts = [];
1113
- let current = "";
1114
- let parenDepth = 0;
1115
- for (const char of content) {
1116
- if (char === "(") parenDepth++;
1117
- if (char === ")") parenDepth--;
1118
- if (char === "," && parenDepth === 0) {
1119
- parts.push(current);
1120
- current = "";
1121
- } else {
1122
- current += char;
1123
- }
1124
- }
1125
- if (current.trim()) parts.push(current);
1126
- return parts;
1127
- }
1128
-
1129
- // src/jsx/draw/object-fit.ts
1130
- function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
1131
- const imgRatio = imgW / imgH;
1132
- const boxRatio = boxW / boxH;
1133
- let sx, sy, sw, sh;
1134
- if (imgRatio > boxRatio) {
1135
- sh = imgH;
1136
- sw = imgH * boxRatio;
1137
- sx = (imgW - sw) / 2;
1138
- sy = 0;
1139
- } else {
1140
- sw = imgW;
1141
- sh = imgW / boxRatio;
1142
- sx = 0;
1143
- sy = (imgH - sh) / 2;
1144
- }
1145
- return { sx, sy, sw, sh, dx: boxX, dy: boxY, dw: boxW, dh: boxH };
1146
- }
1147
- function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
1148
- const imgRatio = imgW / imgH;
1149
- const boxRatio = boxW / boxH;
1150
- let dw, dh;
1151
- if (imgRatio > boxRatio) {
1152
- dw = boxW;
1153
- dh = boxW / imgRatio;
1154
- } else {
1155
- dh = boxH;
1156
- dw = boxH * imgRatio;
1157
- }
1158
- const dx = boxX + (boxW - dw) / 2;
1159
- const dy = boxY + (boxH - dh) / 2;
1160
- return { sx: 0, sy: 0, sw: imgW, sh: imgH, dx, dy, dw, dh };
1161
- }
1162
-
1163
- // src/jsx/draw/image.ts
1164
- async function drawImage(ctx, src, x, y, width, height, context, style) {
1165
- const image = await cachedLoadImage(
1166
- context.imageCache,
1167
- src,
1168
- context.userAgent
1169
- );
1170
- const objectFit = style?.objectFit ?? "fill";
1171
- if (objectFit === "fill") {
1172
- ctx.drawImage(image, x, y, width, height);
1173
- return;
1174
- }
1175
- const imgW = image.width;
1176
- const imgH = image.height;
1177
- if (objectFit === "contain") {
1178
- const r = computeContain(imgW, imgH, x, y, width, height);
1179
- ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
1180
- } else if (objectFit === "cover") {
1181
- const r = computeCover(imgW, imgH, x, y, width, height);
1182
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1183
- } else if (objectFit === "none") {
1184
- const dx = x + (width - imgW) / 2;
1185
- const dy = y + (height - imgH) / 2;
1186
- ctx.drawImage(image, dx, dy);
1187
- } else if (objectFit === "scale-down") {
1188
- if (imgW <= width && imgH <= height) {
1189
- const dx = x + (width - imgW) / 2;
1190
- const dy = y + (height - imgH) / 2;
1191
- ctx.drawImage(image, dx, dy);
1192
- } else {
1193
- const r = computeContain(imgW, imgH, x, y, width, height);
1194
- ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
1195
- }
1196
- }
1197
- }
1198
-
1199
1059
  // src/jsx/draw/offscreen.ts
1200
1060
  import { createCanvas as createCanvas2 } from "@napi-rs/canvas";
1201
1061
  var canvasPool = /* @__PURE__ */ new Map();
@@ -1226,25 +1086,168 @@ function releaseOffscreen(canvas) {
1226
1086
  stack.push(new WeakRef(canvas));
1227
1087
  }
1228
1088
 
1229
- // src/jsx/draw/utils.ts
1230
- function parseCSSLength(value, referenceSize) {
1231
- if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
1232
- return parseFloat(value);
1089
+ // src/jsx/draw/backdrop-filter.ts
1090
+ function drawBackdropFilter(ctx, filter, x, y, width, height, borderRadius) {
1091
+ if (width <= 0 || height <= 0) return;
1092
+ const normalized = filter.trim();
1093
+ if (!normalized || normalized === "none") return;
1094
+ const m = ctx.getTransform();
1095
+ const det = m.a * m.d - m.b * m.c;
1096
+ if (det === 0) return;
1097
+ const pad = Math.ceil(filterBleed(normalized)) + 1;
1098
+ const bx = x - pad;
1099
+ const by = y - pad;
1100
+ const bw = width + 2 * pad;
1101
+ const bh = height + 2 * pad;
1102
+ const bounds = transformedBounds(m, bx, by, bw, bh);
1103
+ const rx = Math.floor(bounds.x0);
1104
+ const ry = Math.floor(bounds.y0);
1105
+ const rw = Math.ceil(bounds.x1) - rx;
1106
+ const rh = Math.ceil(bounds.y1) - ry;
1107
+ const canvasW = ctx.canvas.width;
1108
+ const canvasH = ctx.canvas.height;
1109
+ if (rx + rw <= 0 || ry + rh <= 0 || rx >= canvasW || ry >= canvasH) return;
1110
+ const sx = Math.hypot(m.a, m.b);
1111
+ const sy = Math.hypot(m.c, m.d);
1112
+ const eps = 1e-6 * Math.max(sx, sy);
1113
+ const similar = Math.abs(sx - sy) <= eps && Math.abs(m.a * m.c + m.b * m.d) <= eps * sx;
1114
+ let bufW;
1115
+ let bufH;
1116
+ let scale;
1117
+ if (similar) {
1118
+ bufW = rw;
1119
+ bufH = rh;
1120
+ scale = sx;
1121
+ } else {
1122
+ scale = Math.max(sx, sy);
1123
+ bufW = Math.ceil(bw * scale);
1124
+ bufH = Math.ceil(bh * scale);
1125
+ }
1126
+ if (bufW <= 0 || bufH <= 0) return;
1127
+ const [snapshot, snapCtx] = acquireOffscreen(bufW, bufH);
1128
+ if (similar) {
1129
+ snapCtx.translate(-rx, -ry);
1130
+ } else {
1131
+ const inv = invert(m, det);
1132
+ snapCtx.setTransform(
1133
+ scale * inv.a,
1134
+ scale * inv.b,
1135
+ scale * inv.c,
1136
+ scale * inv.d,
1137
+ scale * (inv.e - bx),
1138
+ scale * (inv.f - by)
1139
+ );
1140
+ }
1141
+ drawExtendedBackdrop(snapCtx, ctx.canvas, rx, ry, rw, rh);
1142
+ const filtered = filterSnapshot(snapshot, normalized, scale);
1143
+ releaseOffscreen(snapshot);
1144
+ ctx.save();
1145
+ applyClip(ctx, x, y, width, height, borderRadius);
1146
+ ctx.filter = "none";
1147
+ if (similar) {
1148
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1149
+ ctx.drawImage(filtered, 0, 0, bufW, bufH, rx, ry, bufW, bufH);
1150
+ } else {
1151
+ ctx.drawImage(filtered, 0, 0, bufW, bufH, bx, by, bw, bh);
1152
+ }
1153
+ ctx.restore();
1154
+ releaseOffscreen(filtered);
1155
+ }
1156
+ function drawExtendedBackdrop(target, canvas, rx, ry, rw, rh) {
1157
+ const cw = canvas.width;
1158
+ const ch = canvas.height;
1159
+ const ix0 = Math.max(rx, 0);
1160
+ const iy0 = Math.max(ry, 0);
1161
+ const ix1 = Math.min(rx + rw, cw);
1162
+ const iy1 = Math.min(ry + rh, ch);
1163
+ if (ix1 <= ix0 || iy1 <= iy0) return;
1164
+ const iw = ix1 - ix0;
1165
+ const ih = iy1 - iy0;
1166
+ target.drawImage(canvas, ix0, iy0, iw, ih, ix0, iy0, iw, ih);
1167
+ const left = ix0 - rx;
1168
+ const right = rx + rw - ix1;
1169
+ const top = iy0 - ry;
1170
+ const bottom = ry + rh - iy1;
1171
+ if (left > 0) target.drawImage(canvas, 0, iy0, 1, ih, rx, iy0, left, ih);
1172
+ if (right > 0) {
1173
+ target.drawImage(canvas, cw - 1, iy0, 1, ih, ix1, iy0, right, ih);
1174
+ }
1175
+ if (top > 0) target.drawImage(canvas, ix0, 0, iw, 1, ix0, ry, iw, top);
1176
+ if (bottom > 0) {
1177
+ target.drawImage(canvas, ix0, ch - 1, iw, 1, ix0, iy1, iw, bottom);
1178
+ }
1179
+ if (left > 0 && top > 0) {
1180
+ target.drawImage(canvas, 0, 0, 1, 1, rx, ry, left, top);
1181
+ }
1182
+ if (right > 0 && top > 0) {
1183
+ target.drawImage(canvas, cw - 1, 0, 1, 1, ix1, ry, right, top);
1184
+ }
1185
+ if (left > 0 && bottom > 0) {
1186
+ target.drawImage(canvas, 0, ch - 1, 1, 1, rx, iy1, left, bottom);
1187
+ }
1188
+ if (right > 0 && bottom > 0) {
1189
+ target.drawImage(canvas, cw - 1, ch - 1, 1, 1, ix1, iy1, right, bottom);
1190
+ }
1191
+ }
1192
+ function filterSnapshot(snapshot, filter, scale) {
1193
+ const [out, outCtx] = acquireOffscreen(snapshot.width, snapshot.height);
1194
+ outCtx.filter = scale === 1 ? filter : scaleFilterLengths(filter, scale);
1195
+ outCtx.drawImage(snapshot, 0, 0);
1196
+ outCtx.filter = "none";
1197
+ return out;
1233
1198
  }
1234
- function toNumber(v) {
1235
- if (typeof v === "number") return v;
1236
- if (v === void 0 || v === null) return 0;
1237
- const n = parseFloat(String(v));
1238
- return isNaN(n) ? 0 : n;
1199
+ function transformedBounds(m, x, y, w, h) {
1200
+ let x0 = Infinity;
1201
+ let y0 = Infinity;
1202
+ let x1 = -Infinity;
1203
+ let y1 = -Infinity;
1204
+ for (const [px, py] of [
1205
+ [x, y],
1206
+ [x + w, y],
1207
+ [x, y + h],
1208
+ [x + w, y + h]
1209
+ ]) {
1210
+ const dx = m.a * px + m.c * py + m.e;
1211
+ const dy = m.b * px + m.d * py + m.f;
1212
+ if (dx < x0) x0 = dx;
1213
+ if (dx > x1) x1 = dx;
1214
+ if (dy < y0) y0 = dy;
1215
+ if (dy > y1) y1 = dy;
1216
+ }
1217
+ return { x0, y0, x1, y1 };
1218
+ }
1219
+ function invert(m, det) {
1220
+ return {
1221
+ a: m.d / det,
1222
+ b: -m.b / det,
1223
+ c: -m.c / det,
1224
+ d: m.a / det,
1225
+ e: (m.c * m.f - m.d * m.e) / det,
1226
+ f: (m.b * m.e - m.a * m.f) / det
1227
+ };
1239
1228
  }
1240
- function resolveBoxValue(v, referenceWidth) {
1241
- if (typeof v === "number") return v;
1242
- if (v === void 0 || v === null) return 0;
1243
- const s = String(v);
1244
- if (s.endsWith("%")) return parseFloat(s) / 100 * referenceWidth;
1245
- const n = parseFloat(s);
1246
- return isNaN(n) ? 0 : n;
1229
+ function filterBleed(filter) {
1230
+ let bleed = 0;
1231
+ for (const m of filter.matchAll(/blur\(\s*(-?\d*\.?\d+)(?:px)?\s*\)/g)) {
1232
+ bleed += Math.abs(parseFloat(m[1])) * 3;
1233
+ }
1234
+ for (const m of filter.matchAll(/drop-shadow\(((?:[^()]|\([^()]*\))*)\)/g)) {
1235
+ const args = m[1].replace(/[\w-]+\([^()]*\)/g, " ");
1236
+ const nums = args.match(/-?\d*\.?\d+(?=px|\s|$)/g) ?? [];
1237
+ const [ox = "0", oy = "0", blur = "0"] = nums;
1238
+ bleed += Math.abs(parseFloat(ox)) + Math.abs(parseFloat(oy)) + Math.abs(parseFloat(blur)) * 3;
1239
+ }
1240
+ return bleed;
1247
1241
  }
1242
+ function scaleFilterLengths(filter, scale) {
1243
+ return filter.replace(
1244
+ /(?<![\w.])(-?\d*\.?\d+)px/g,
1245
+ (_, n) => `${formatCSSNumber(parseFloat(n) * scale)}px`
1246
+ );
1247
+ }
1248
+
1249
+ // src/jsx/draw/clip-path.ts
1250
+ import { Path2D } from "@napi-rs/canvas";
1248
1251
 
1249
1252
  // src/jsx/draw/rect.ts
1250
1253
  function drawRect(ctx, x, y, width, height, style) {
@@ -1316,174 +1319,1063 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
1316
1319
  );
1317
1320
  ctx.stroke();
1318
1321
  } else {
1319
- ctx.strokeRect(x + tw / 2, y + tw / 2, width - tw, height - tw);
1322
+ ctx.strokeRect(x + tw / 2, y + tw / 2, width - tw, height - tw);
1323
+ }
1324
+ return;
1325
+ }
1326
+ if (hasRoundedCorners) {
1327
+ const maxR = Math.min(width, height) / 2;
1328
+ const tl = Math.min(borderRadius.topLeft, maxR);
1329
+ const tr = Math.min(borderRadius.topRight, maxR);
1330
+ const br = Math.min(borderRadius.bottomRight, maxR);
1331
+ const bl = Math.min(borderRadius.bottomLeft, maxR);
1332
+ const top = y + tw / 2;
1333
+ const right = x + width - rw / 2;
1334
+ const bottom = y + height - bw / 2;
1335
+ const left = x + lw / 2;
1336
+ const eTL = Math.max(0, tl - Math.max(tw, lw) / 2);
1337
+ const eTR = Math.max(0, tr - Math.max(tw, rw) / 2);
1338
+ const eBR = Math.max(0, br - Math.max(bw, rw) / 2);
1339
+ const eBL = Math.max(0, bl - Math.max(bw, lw) / 2);
1340
+ if (tw > 0) {
1341
+ ctx.strokeStyle = tc;
1342
+ ctx.lineWidth = tw;
1343
+ ctx.beginPath();
1344
+ if (eTL > 0 && lw === 0) {
1345
+ ctx.moveTo(left, top + eTL);
1346
+ ctx.arcTo(left, top, left + eTL, top, eTL);
1347
+ } else if (eTL > 0) {
1348
+ ctx.moveTo(left + eTL, top);
1349
+ } else {
1350
+ ctx.moveTo(x, top);
1351
+ }
1352
+ if (eTR > 0) {
1353
+ ctx.lineTo(right - eTR, top);
1354
+ ctx.arcTo(right, top, right, top + eTR, eTR);
1355
+ } else {
1356
+ ctx.lineTo(x + width, top);
1357
+ }
1358
+ ctx.stroke();
1359
+ }
1360
+ if (rw > 0) {
1361
+ ctx.strokeStyle = rc;
1362
+ ctx.lineWidth = rw;
1363
+ ctx.beginPath();
1364
+ if (eTR > 0 && tw === 0) {
1365
+ ctx.moveTo(right - eTR, top);
1366
+ ctx.arcTo(right, top, right, top + eTR, eTR);
1367
+ } else if (eTR > 0) {
1368
+ ctx.moveTo(right, top + eTR);
1369
+ } else {
1370
+ ctx.moveTo(right, y);
1371
+ }
1372
+ if (eBR > 0) {
1373
+ ctx.lineTo(right, bottom - eBR);
1374
+ ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1375
+ } else {
1376
+ ctx.lineTo(right, y + height);
1377
+ }
1378
+ ctx.stroke();
1379
+ }
1380
+ if (bw > 0) {
1381
+ ctx.strokeStyle = bc;
1382
+ ctx.lineWidth = bw;
1383
+ ctx.beginPath();
1384
+ if (eBR > 0 && rw === 0) {
1385
+ ctx.moveTo(right, bottom - eBR);
1386
+ ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1387
+ } else if (eBR > 0) {
1388
+ ctx.moveTo(right - eBR, bottom);
1389
+ } else {
1390
+ ctx.moveTo(x + width, bottom);
1391
+ }
1392
+ if (eBL > 0) {
1393
+ ctx.lineTo(left + eBL, bottom);
1394
+ ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1395
+ } else {
1396
+ ctx.lineTo(x, bottom);
1397
+ }
1398
+ ctx.stroke();
1399
+ }
1400
+ if (lw > 0) {
1401
+ ctx.strokeStyle = lc;
1402
+ ctx.lineWidth = lw;
1403
+ ctx.beginPath();
1404
+ if (eBL > 0 && bw === 0) {
1405
+ ctx.moveTo(left + eBL, bottom);
1406
+ ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1407
+ } else if (eBL > 0) {
1408
+ ctx.moveTo(left, bottom - eBL);
1409
+ } else {
1410
+ ctx.moveTo(left, y + height);
1411
+ }
1412
+ if (eTL > 0) {
1413
+ ctx.lineTo(left, top + eTL);
1414
+ ctx.arcTo(left, top, left + eTL, top, eTL);
1415
+ } else {
1416
+ ctx.lineTo(left, y);
1417
+ }
1418
+ ctx.stroke();
1419
+ }
1420
+ } else {
1421
+ if (tw > 0) {
1422
+ ctx.strokeStyle = tc;
1423
+ ctx.lineWidth = tw;
1424
+ ctx.beginPath();
1425
+ ctx.moveTo(x, y + tw / 2);
1426
+ ctx.lineTo(x + width, y + tw / 2);
1427
+ ctx.stroke();
1428
+ }
1429
+ if (rw > 0) {
1430
+ ctx.strokeStyle = rc;
1431
+ ctx.lineWidth = rw;
1432
+ ctx.beginPath();
1433
+ ctx.moveTo(x + width - rw / 2, y);
1434
+ ctx.lineTo(x + width - rw / 2, y + height);
1435
+ ctx.stroke();
1436
+ }
1437
+ if (bw > 0) {
1438
+ ctx.strokeStyle = bc;
1439
+ ctx.lineWidth = bw;
1440
+ ctx.beginPath();
1441
+ ctx.moveTo(x, y + height - bw / 2);
1442
+ ctx.lineTo(x + width, y + height - bw / 2);
1443
+ ctx.stroke();
1444
+ }
1445
+ if (lw > 0) {
1446
+ ctx.strokeStyle = lc;
1447
+ ctx.lineWidth = lw;
1448
+ ctx.beginPath();
1449
+ ctx.moveTo(x + lw / 2, y);
1450
+ ctx.lineTo(x + lw / 2, y + height);
1451
+ ctx.stroke();
1452
+ }
1453
+ }
1454
+ }
1455
+ var BOX_SHADOW_RE = /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/;
1456
+ function boxShadowExtent(boxShadow) {
1457
+ const parts = boxShadow.match(BOX_SHADOW_RE);
1458
+ if (!parts) return 0;
1459
+ const offsetX = parseFloat(parts[1]);
1460
+ const offsetY = parseFloat(parts[2]);
1461
+ const blur = parseFloat(parts[3]);
1462
+ return blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
1463
+ }
1464
+ function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1465
+ const parts = boxShadow.match(BOX_SHADOW_RE);
1466
+ if (!parts) return;
1467
+ const offsetX = parseFloat(parts[1]);
1468
+ const offsetY = parseFloat(parts[2]);
1469
+ const blur = parseFloat(parts[3]);
1470
+ const color = parts[5].trim();
1471
+ const radii = [
1472
+ borderRadius.topLeft,
1473
+ borderRadius.topRight,
1474
+ borderRadius.bottomRight,
1475
+ borderRadius.bottomLeft
1476
+ ];
1477
+ const margin = blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
1478
+ ctx.save();
1479
+ ctx.beginPath();
1480
+ ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
1481
+ ctx.roundRect(x, y, width, height, radii);
1482
+ ctx.clip("evenodd");
1483
+ ctx.filter = `blur(${blur / 2}px)`;
1484
+ ctx.translate(offsetX, offsetY);
1485
+ ctx.fillStyle = color;
1486
+ ctx.beginPath();
1487
+ ctx.roundRect(x, y, width, height, radii);
1488
+ ctx.fill();
1489
+ ctx.restore();
1490
+ }
1491
+
1492
+ // src/jsx/draw/clip-path.ts
1493
+ var GEOMETRY_BOXES = /* @__PURE__ */ new Set([
1494
+ "border-box",
1495
+ "padding-box",
1496
+ "content-box",
1497
+ "margin-box",
1498
+ "fill-box",
1499
+ "stroke-box",
1500
+ "view-box"
1501
+ ]);
1502
+ var H_KEYWORDS = /* @__PURE__ */ new Map([
1503
+ ["left", 0],
1504
+ ["x-start", 0],
1505
+ ["center", 0.5],
1506
+ ["right", 1],
1507
+ ["x-end", 1]
1508
+ ]);
1509
+ var V_KEYWORDS = /* @__PURE__ */ new Map([
1510
+ ["top", 0],
1511
+ ["y-start", 0],
1512
+ ["center", 0.5],
1513
+ ["bottom", 1],
1514
+ ["y-end", 1]
1515
+ ]);
1516
+ var ARC_OPTIONS = /* @__PURE__ */ new Set(["of", "cw", "ccw", "large", "small", "rotate"]);
1517
+ function applyClipPath(ctx, clipPath, style, x, y, width, height) {
1518
+ const shape = parseClipPath(clipPath, style, { x, y, width, height });
1519
+ if (!shape || shape.kind === "none") return;
1520
+ const path = clipShapeToPath(shape);
1521
+ if (path) ctx.clip(path, shapeFillRule(shape));
1522
+ }
1523
+ function shapeFillRule(shape) {
1524
+ return shape.kind === "polygon" || shape.kind === "path" ? shape.fillRule : "nonzero";
1525
+ }
1526
+ function parseClipPath(value, style, box) {
1527
+ const trimmed = value.trim();
1528
+ if (trimmed === "none") return { kind: "none" };
1529
+ let fn = null;
1530
+ let geometryBox = "border-box";
1531
+ const fnMatch = trimmed.match(/^(.*?)([a-z]+)\((.*)\)(.*)$/s);
1532
+ let keywords;
1533
+ if (fnMatch) {
1534
+ fn = { name: fnMatch[2], args: fnMatch[3] };
1535
+ keywords = [fnMatch[1], fnMatch[4]].join(" ").split(/\s+/).filter(Boolean);
1536
+ } else {
1537
+ keywords = trimmed.split(/\s+/).filter(Boolean);
1538
+ }
1539
+ for (const kw of keywords) {
1540
+ if (!GEOMETRY_BOXES.has(kw)) return null;
1541
+ geometryBox = kw;
1542
+ }
1543
+ if (!fn && keywords.length === 0) return null;
1544
+ const ref = resolveReferenceBox(geometryBox, style, box);
1545
+ const { width: w, height: h } = ref;
1546
+ if (!fn) {
1547
+ return {
1548
+ kind: "rect",
1549
+ x: ref.x,
1550
+ y: ref.y,
1551
+ width: w,
1552
+ height: h,
1553
+ radii: referenceBoxRadii(geometryBox, style, box, ref)
1554
+ };
1555
+ }
1556
+ switch (fn.name) {
1557
+ case "inset":
1558
+ return parseInset(fn.args, ref);
1559
+ case "rect":
1560
+ return parseRect(fn.args, ref);
1561
+ case "xywh":
1562
+ return parseXywh(fn.args, ref);
1563
+ case "circle":
1564
+ return parseCircle(fn.args, ref);
1565
+ case "ellipse":
1566
+ return parseEllipse(fn.args, ref);
1567
+ case "polygon":
1568
+ return parsePolygon(fn.args, ref);
1569
+ case "path":
1570
+ return parsePath(fn.args, ref);
1571
+ case "shape":
1572
+ return parseShape(fn.args, ref);
1573
+ default:
1574
+ return null;
1575
+ }
1576
+ }
1577
+ function clipShapeToPath(shape) {
1578
+ const p = new Path2D();
1579
+ switch (shape.kind) {
1580
+ case "none":
1581
+ break;
1582
+ case "rect":
1583
+ if (shape.radii && shape.width > 0 && shape.height > 0) {
1584
+ roundedRectPath(
1585
+ p,
1586
+ shape.x,
1587
+ shape.y,
1588
+ shape.width,
1589
+ shape.height,
1590
+ shape.radii
1591
+ );
1592
+ } else {
1593
+ p.rect(
1594
+ shape.x,
1595
+ shape.y,
1596
+ Math.max(0, shape.width),
1597
+ Math.max(0, shape.height)
1598
+ );
1599
+ }
1600
+ break;
1601
+ case "circle":
1602
+ if (shape.r > 0) p.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
1603
+ break;
1604
+ case "ellipse":
1605
+ if (shape.rx > 0 && shape.ry > 0) {
1606
+ p.ellipse(shape.cx, shape.cy, shape.rx, shape.ry, 0, 0, Math.PI * 2);
1607
+ }
1608
+ break;
1609
+ case "polygon":
1610
+ if (shape.points.length >= 3) {
1611
+ const [first, ...rest] = shape.points;
1612
+ p.moveTo(first[0], first[1]);
1613
+ for (const [px, py] of rest) p.lineTo(px, py);
1614
+ p.closePath();
1615
+ }
1616
+ break;
1617
+ case "path": {
1618
+ let inner;
1619
+ try {
1620
+ inner = new Path2D(shape.d);
1621
+ } catch {
1622
+ return null;
1623
+ }
1624
+ if (shape.dx === 0 && shape.dy === 0) return inner;
1625
+ p.addPath(inner, { a: 1, b: 0, c: 0, d: 1, e: shape.dx, f: shape.dy });
1626
+ break;
1627
+ }
1628
+ }
1629
+ return p;
1630
+ }
1631
+ function resolveReferenceBox(geometryBox, style, box) {
1632
+ const { x, y, width, height } = box;
1633
+ switch (geometryBox) {
1634
+ case "padding-box": {
1635
+ const t = resolveBoxValue(style.borderTopWidth, width);
1636
+ const r = resolveBoxValue(style.borderRightWidth, width);
1637
+ const b = resolveBoxValue(style.borderBottomWidth, width);
1638
+ const l = resolveBoxValue(style.borderLeftWidth, width);
1639
+ return {
1640
+ x: x + l,
1641
+ y: y + t,
1642
+ width: width - l - r,
1643
+ height: height - t - b
1644
+ };
1645
+ }
1646
+ case "content-box": {
1647
+ const t = resolveBoxValue(style.borderTopWidth, width) + resolveBoxValue(style.paddingTop, width);
1648
+ const r = resolveBoxValue(style.borderRightWidth, width) + resolveBoxValue(style.paddingRight, width);
1649
+ const b = resolveBoxValue(style.borderBottomWidth, width) + resolveBoxValue(style.paddingBottom, width);
1650
+ const l = resolveBoxValue(style.borderLeftWidth, width) + resolveBoxValue(style.paddingLeft, width);
1651
+ return {
1652
+ x: x + l,
1653
+ y: y + t,
1654
+ width: width - l - r,
1655
+ height: height - t - b
1656
+ };
1657
+ }
1658
+ case "margin-box": {
1659
+ const t = marginValue(style.marginTop, width);
1660
+ const r = marginValue(style.marginRight, width);
1661
+ const b = marginValue(style.marginBottom, width);
1662
+ const l = marginValue(style.marginLeft, width);
1663
+ return {
1664
+ x: x - l,
1665
+ y: y - t,
1666
+ width: width + l + r,
1667
+ height: height + t + b
1668
+ };
1669
+ }
1670
+ default:
1671
+ return box;
1672
+ }
1673
+ }
1674
+ function marginValue(v, referenceWidth) {
1675
+ if (v === "auto") return 0;
1676
+ return resolveBoxValue(v, referenceWidth);
1677
+ }
1678
+ function referenceBoxRadii(geometryBox, style, box, ref) {
1679
+ const br = getBorderRadiusFromStyle(style, box.width, box.height);
1680
+ const maxR = maxCornerRadius(box.width, box.height);
1681
+ const base = [br.topLeft, br.topRight, br.bottomRight, br.bottomLeft].map(
1682
+ (r) => Math.min(Math.max(0, r), maxR)
1683
+ );
1684
+ if (base.every((r) => r <= 0)) return null;
1685
+ const left = ref.x - box.x;
1686
+ const top = ref.y - box.y;
1687
+ const right = box.x + box.width - (ref.x + ref.width);
1688
+ const bottom = box.y + box.height - (ref.y + ref.height);
1689
+ const insets = [
1690
+ [left, top],
1691
+ [right, top],
1692
+ [right, bottom],
1693
+ [left, bottom]
1694
+ ];
1695
+ const radii = base.map((r, i) => {
1696
+ const [ix, iy] = insets[i];
1697
+ return [adjustRadius(r, ix), adjustRadius(r, iy)];
1698
+ });
1699
+ if (radii.every(([rx, ry]) => rx <= 0 || ry <= 0)) return null;
1700
+ return normalizeRadii(radii, ref.width, ref.height);
1701
+ }
1702
+ function adjustRadius(r, inset) {
1703
+ if (inset >= 0) return Math.max(0, r - inset);
1704
+ if (r <= 0) return 0;
1705
+ const m = -inset;
1706
+ if (r >= m) return r + m;
1707
+ const ratio = r / m - 1;
1708
+ return r + m * (1 + ratio * ratio * ratio);
1709
+ }
1710
+ function parseInset(args, ref) {
1711
+ const [insetPart, roundPart] = splitRound(args);
1712
+ const values = tokenize(insetPart);
1713
+ if (values.length < 1 || values.length > 4) return null;
1714
+ const [t, r, b, l] = expandSides(values);
1715
+ const top = parseCSSLength(t, ref.height);
1716
+ const right = parseCSSLength(r, ref.width);
1717
+ const bottom = parseCSSLength(b, ref.height);
1718
+ const left = parseCSSLength(l, ref.width);
1719
+ if ([top, right, bottom, left].some(isNaN)) return null;
1720
+ return makeRect(
1721
+ ref.x + left,
1722
+ ref.y + top,
1723
+ ref.width - left - right,
1724
+ ref.height - top - bottom,
1725
+ roundPart,
1726
+ ref
1727
+ );
1728
+ }
1729
+ function parseRect(args, ref) {
1730
+ const [rectPart, roundPart] = splitRound(args);
1731
+ const values = tokenize(rectPart);
1732
+ if (values.length !== 4) return null;
1733
+ const [t, r, b, l] = values;
1734
+ const top = t === "auto" ? 0 : parseCSSLength(t, ref.height);
1735
+ const right = r === "auto" ? ref.width : parseCSSLength(r, ref.width);
1736
+ const bottom = b === "auto" ? ref.height : parseCSSLength(b, ref.height);
1737
+ const left = l === "auto" ? 0 : parseCSSLength(l, ref.width);
1738
+ if ([top, right, bottom, left].some(isNaN)) return null;
1739
+ const x2 = Math.max(left, right);
1740
+ const y2 = Math.max(top, bottom);
1741
+ return makeRect(
1742
+ ref.x + left,
1743
+ ref.y + top,
1744
+ x2 - left,
1745
+ y2 - top,
1746
+ roundPart,
1747
+ ref
1748
+ );
1749
+ }
1750
+ function parseXywh(args, ref) {
1751
+ const [xywhPart, roundPart] = splitRound(args);
1752
+ const values = tokenize(xywhPart);
1753
+ if (values.length !== 4) return null;
1754
+ const [xs, ys, ws, hs] = values;
1755
+ const x = parseCSSLength(xs, ref.width);
1756
+ const y = parseCSSLength(ys, ref.height);
1757
+ const w = parseCSSLength(ws, ref.width);
1758
+ const h = parseCSSLength(hs, ref.height);
1759
+ if ([x, y, w, h].some(isNaN)) return null;
1760
+ return makeRect(
1761
+ ref.x + x,
1762
+ ref.y + y,
1763
+ Math.max(0, w),
1764
+ Math.max(0, h),
1765
+ roundPart,
1766
+ ref
1767
+ );
1768
+ }
1769
+ function makeRect(x, y, width, height, roundPart, ref) {
1770
+ const w = Math.max(0, width);
1771
+ const h = Math.max(0, height);
1772
+ let radii = null;
1773
+ if (roundPart !== void 0) {
1774
+ const parsed = parseRadii(roundPart, ref.width, ref.height, w, h);
1775
+ if (parsed === void 0) return null;
1776
+ radii = parsed;
1777
+ }
1778
+ return { kind: "rect", x, y, width: w, height: h, radii };
1779
+ }
1780
+ function parseCircle(args, ref) {
1781
+ const [radiusPart, posPart] = splitAt(args);
1782
+ const radiusTokens = tokenize(radiusPart);
1783
+ if (radiusTokens.length > 1) return null;
1784
+ const pos = resolvePosition(tokenize(posPart ?? ""), ref.width, ref.height);
1785
+ if (!pos) return null;
1786
+ const [cx, cy] = pos;
1787
+ const closest = Math.min(cx, cy, ref.width - cx, ref.height - cy);
1788
+ const farthest = Math.max(cx, cy, ref.width - cx, ref.height - cy);
1789
+ const percentBase = Math.hypot(ref.width, ref.height) / Math.SQRT2;
1790
+ const r = resolveRadius2(radiusTokens[0], closest, farthest, percentBase);
1791
+ if (r === void 0) return null;
1792
+ return { kind: "circle", cx: ref.x + cx, cy: ref.y + cy, r: Math.max(0, r) };
1793
+ }
1794
+ function parseEllipse(args, ref) {
1795
+ const [radiusPart, posPart] = splitAt(args);
1796
+ const radiusTokens = tokenize(radiusPart);
1797
+ if (radiusTokens.length !== 0 && radiusTokens.length !== 2) return null;
1798
+ const pos = resolvePosition(tokenize(posPart ?? ""), ref.width, ref.height);
1799
+ if (!pos) return null;
1800
+ const [cx, cy] = pos;
1801
+ const rx = resolveRadius2(
1802
+ radiusTokens[0],
1803
+ Math.min(cx, ref.width - cx),
1804
+ Math.max(cx, ref.width - cx),
1805
+ ref.width
1806
+ );
1807
+ const ry = resolveRadius2(
1808
+ radiusTokens[1],
1809
+ Math.min(cy, ref.height - cy),
1810
+ Math.max(cy, ref.height - cy),
1811
+ ref.height
1812
+ );
1813
+ if (rx === void 0 || ry === void 0) return null;
1814
+ return {
1815
+ kind: "ellipse",
1816
+ cx: ref.x + cx,
1817
+ cy: ref.y + cy,
1818
+ rx: Math.max(0, rx),
1819
+ ry: Math.max(0, ry)
1820
+ };
1821
+ }
1822
+ function parsePolygon(args, ref) {
1823
+ const parts = splitArgs(args);
1824
+ let fillRule = "nonzero";
1825
+ const first = parts[0]?.trim();
1826
+ if (first === "nonzero" || first === "evenodd") {
1827
+ fillRule = first;
1828
+ parts.shift();
1829
+ }
1830
+ const points = [];
1831
+ for (const part of parts) {
1832
+ const pair = tokenize(part);
1833
+ if (pair.length !== 2) return null;
1834
+ const px = parseCSSLength(pair[0], ref.width);
1835
+ const py = parseCSSLength(pair[1], ref.height);
1836
+ if (isNaN(px) || isNaN(py)) return null;
1837
+ points.push([ref.x + px, ref.y + py]);
1838
+ }
1839
+ if (points.length === 0) return null;
1840
+ return { kind: "polygon", points, fillRule };
1841
+ }
1842
+ function parsePath(args, ref) {
1843
+ const parts = splitArgs(args);
1844
+ let fillRule = "nonzero";
1845
+ const first = parts[0]?.trim();
1846
+ if (first === "nonzero" || first === "evenodd") {
1847
+ fillRule = first;
1848
+ parts.shift();
1849
+ }
1850
+ if (parts.length !== 1) return null;
1851
+ const d = unquote(parts[0].trim());
1852
+ if (!d) return null;
1853
+ return { kind: "path", d, dx: ref.x, dy: ref.y, fillRule };
1854
+ }
1855
+ function parseShape(args, ref) {
1856
+ const { width: w, height: h } = ref;
1857
+ const parts = splitArgs(args);
1858
+ const first = parts.shift()?.trim();
1859
+ if (!first) return null;
1860
+ const startMatch = first.match(/^(?:(nonzero|evenodd)\s+)?from\s+(.+)$/s);
1861
+ if (!startMatch) return null;
1862
+ const fillRule = startMatch[1] ?? "nonzero";
1863
+ let current = resolvePoint(tokenize(startMatch[2]), w, h);
1864
+ if (!current) return null;
1865
+ let subpathStart = current;
1866
+ let d = `M${fmtPoint(current)}`;
1867
+ for (const part of parts) {
1868
+ const tokens = tokenize(part);
1869
+ const command = tokens.shift();
1870
+ switch (command) {
1871
+ case "close": {
1872
+ if (tokens.length) return null;
1873
+ d += " Z";
1874
+ current = subpathStart;
1875
+ break;
1876
+ }
1877
+ case "move":
1878
+ case "line": {
1879
+ const end = resolveEndpoint(tokens, current, w, h);
1880
+ if (!end) return null;
1881
+ d += ` ${command === "move" ? "M" : "L"}${fmtPoint(end)}`;
1882
+ current = end;
1883
+ if (command === "move") subpathStart = end;
1884
+ break;
1885
+ }
1886
+ case "hline":
1887
+ case "vline": {
1888
+ const [mode, token, ...rest] = tokens;
1889
+ if (mode !== "to" && mode !== "by" || !token || rest.length) {
1890
+ return null;
1891
+ }
1892
+ const axis = command === "hline" ? 0 : 1;
1893
+ const base = axis === 0 ? w : h;
1894
+ const keywords = axis === 0 ? H_KEYWORDS : V_KEYWORDS;
1895
+ const kw = keywords.get(token);
1896
+ const resolved = mode === "to" && kw !== void 0 ? kw * base : parseCSSLength(token, base);
1897
+ if (isNaN(resolved)) return null;
1898
+ const next = [current[0], current[1]];
1899
+ next[axis] = mode === "by" ? current[axis] + resolved : resolved;
1900
+ d += axis === 0 ? ` H${fmt(next[0])}` : ` V${fmt(next[1])}`;
1901
+ current = next;
1902
+ break;
1903
+ }
1904
+ case "curve":
1905
+ case "smooth": {
1906
+ const withIndex = tokens.indexOf("with");
1907
+ const endTokens = withIndex === -1 ? tokens : tokens.slice(0, withIndex);
1908
+ const controlTokens = withIndex === -1 ? [] : tokens.slice(withIndex + 1);
1909
+ const mode = endTokens[0];
1910
+ const end = resolveEndpoint(endTokens, current, w, h);
1911
+ if (!end) return null;
1912
+ if (command === "curve" && withIndex === -1) return null;
1913
+ const controls = [];
1914
+ for (const control of splitAtSlash(controlTokens)) {
1915
+ const point = resolveControlPoint(control, mode, current, end, w, h);
1916
+ if (!point) return null;
1917
+ controls.push(point);
1918
+ }
1919
+ if (command === "curve") {
1920
+ if (controls.length === 1) {
1921
+ d += ` Q${fmtPoint(controls[0])} ${fmtPoint(end)}`;
1922
+ } else if (controls.length === 2) {
1923
+ d += ` C${fmtPoint(controls[0])} ${fmtPoint(controls[1])} ${fmtPoint(end)}`;
1924
+ } else {
1925
+ return null;
1926
+ }
1927
+ } else if (controls.length === 0) {
1928
+ d += ` T${fmtPoint(end)}`;
1929
+ } else if (controls.length === 1) {
1930
+ d += ` S${fmtPoint(controls[0])} ${fmtPoint(end)}`;
1931
+ } else {
1932
+ return null;
1933
+ }
1934
+ current = end;
1935
+ break;
1936
+ }
1937
+ case "arc": {
1938
+ const optionIndex = tokens.findIndex((t) => ARC_OPTIONS.has(t));
1939
+ const endTokens = optionIndex === -1 ? tokens : tokens.slice(0, optionIndex);
1940
+ const options = optionIndex === -1 ? [] : tokens.slice(optionIndex);
1941
+ const end = resolveEndpoint(endTokens, current, w, h);
1942
+ if (!end) return null;
1943
+ let rx = 0;
1944
+ let ry = 0;
1945
+ let rotation = 0;
1946
+ let large = 0;
1947
+ let sweep = 0;
1948
+ for (let i = 0; i < options.length; i++) {
1949
+ const option = options[i];
1950
+ if (option === "of") {
1951
+ const r1 = options[++i];
1952
+ if (!r1) return null;
1953
+ const r2 = options[i + 1];
1954
+ const hasSecond = r2 !== void 0 && !ARC_OPTIONS.has(r2);
1955
+ if (hasSecond) {
1956
+ i++;
1957
+ rx = parseCSSLength(r1, w);
1958
+ ry = parseCSSLength(r2, h);
1959
+ } else {
1960
+ rx = ry = parseCSSLength(r1, Math.hypot(w, h) / Math.SQRT2);
1961
+ }
1962
+ if (isNaN(rx) || isNaN(ry)) return null;
1963
+ } else if (option === "cw" || option === "ccw") {
1964
+ sweep = option === "cw" ? 1 : 0;
1965
+ } else if (option === "large" || option === "small") {
1966
+ large = option === "large" ? 1 : 0;
1967
+ } else if (option === "rotate") {
1968
+ const angle = options[++i];
1969
+ if (!angle) return null;
1970
+ rotation = parseAngleDegrees(angle);
1971
+ if (isNaN(rotation)) return null;
1972
+ } else {
1973
+ return null;
1974
+ }
1975
+ }
1976
+ d += ` A${fmt(Math.abs(rx))} ${fmt(Math.abs(ry))} ${fmt(rotation)} ${large} ${sweep} ${fmtPoint(end)}`;
1977
+ current = end;
1978
+ break;
1979
+ }
1980
+ default:
1981
+ return null;
1982
+ }
1983
+ }
1984
+ return { kind: "path", d, dx: ref.x, dy: ref.y, fillRule };
1985
+ }
1986
+ function resolveEndpoint(tokens, current, w, h) {
1987
+ const mode = tokens[0];
1988
+ if (mode !== "to" && mode !== "by") return null;
1989
+ const point = resolvePoint(tokens.slice(1), w, h);
1990
+ if (!point) return null;
1991
+ return mode === "by" ? [current[0] + point[0], current[1] + point[1]] : point;
1992
+ }
1993
+ function resolveControlPoint(tokens, mode, start, end, w, h) {
1994
+ let origin = mode === "by" ? "start" : "origin";
1995
+ let coords = tokens;
1996
+ const fromIndex = tokens.indexOf("from");
1997
+ if (fromIndex !== -1) {
1998
+ const ref = tokens[fromIndex + 1];
1999
+ if (ref !== "start" && ref !== "end" && ref !== "origin" || tokens.length !== fromIndex + 2) {
2000
+ return null;
2001
+ }
2002
+ origin = ref;
2003
+ coords = tokens.slice(0, fromIndex);
2004
+ }
2005
+ const point = resolvePoint(coords, w, h);
2006
+ if (!point) return null;
2007
+ const base = origin === "start" ? start : origin === "end" ? end : [0, 0];
2008
+ return [base[0] + point[0], base[1] + point[1]];
2009
+ }
2010
+ function resolvePoint(tokens, w, h) {
2011
+ if (tokens.length !== 2) return null;
2012
+ const x = resolveAxis(tokens[0], w, H_KEYWORDS);
2013
+ const y = resolveAxis(tokens[1], h, V_KEYWORDS);
2014
+ if (isNaN(x) || isNaN(y)) return null;
2015
+ return [x, y];
2016
+ }
2017
+ function resolvePosition(tokens, w, h) {
2018
+ if (tokens.length === 0) return [w / 2, h / 2];
2019
+ if (tokens.length === 1) {
2020
+ const t = tokens[0];
2021
+ if (V_KEYWORDS.has(t) && !H_KEYWORDS.has(t)) {
2022
+ return [w / 2, V_KEYWORDS.get(t) * h];
2023
+ }
2024
+ const x2 = resolveAxis(t, w, H_KEYWORDS);
2025
+ return isNaN(x2) ? null : [x2, h / 2];
2026
+ }
2027
+ if (tokens.length === 2) {
2028
+ let [a, b] = tokens;
2029
+ const bothKeywords = (H_KEYWORDS.has(a) || V_KEYWORDS.has(a)) && (H_KEYWORDS.has(b) || V_KEYWORDS.has(b));
2030
+ if (bothKeywords && (V_KEYWORDS.has(a) && !H_KEYWORDS.has(a) || H_KEYWORDS.has(b) && !V_KEYWORDS.has(b))) {
2031
+ [a, b] = [b, a];
2032
+ }
2033
+ const x2 = resolveAxis(a, w, H_KEYWORDS);
2034
+ const y2 = resolveAxis(b, h, V_KEYWORDS);
2035
+ return isNaN(x2) || isNaN(y2) ? null : [x2, y2];
2036
+ }
2037
+ let x = w / 2;
2038
+ let y = h / 2;
2039
+ for (let i = 0; i < tokens.length; i++) {
2040
+ const t = tokens[i];
2041
+ const next = tokens[i + 1];
2042
+ const hasOffset = next !== void 0 && !H_KEYWORDS.has(next) && !V_KEYWORDS.has(next);
2043
+ if (t === "left" || t === "right") {
2044
+ const off = hasOffset ? parseCSSLength(next, w) : 0;
2045
+ if (isNaN(off)) return null;
2046
+ x = t === "left" ? off : w - off;
2047
+ } else if (t === "top" || t === "bottom") {
2048
+ const off = hasOffset ? parseCSSLength(next, h) : 0;
2049
+ if (isNaN(off)) return null;
2050
+ y = t === "top" ? off : h - off;
2051
+ } else if (t !== "center") {
2052
+ return null;
2053
+ }
2054
+ if (hasOffset) i++;
2055
+ }
2056
+ return [x, y];
2057
+ }
2058
+ function resolveAxis(token, size, keywords) {
2059
+ const kw = keywords.get(token);
2060
+ if (kw !== void 0) return kw * size;
2061
+ return parseCSSLength(token, size);
2062
+ }
2063
+ function resolveRadius2(token, closestSide, farthestSide, percentBase) {
2064
+ if (token === void 0 || token === "closest-side") return closestSide;
2065
+ if (token === "farthest-side") return farthestSide;
2066
+ const r = parseCSSLength(token, percentBase);
2067
+ return isNaN(r) ? void 0 : r;
2068
+ }
2069
+ function parseRadii(value, refW, refH, w, h) {
2070
+ const [hPart, vPart] = value.split("/");
2071
+ const hTokens = tokenize(hPart ?? "");
2072
+ const vTokens = vPart === void 0 ? hTokens : tokenize(vPart);
2073
+ if (hTokens.length < 1 || hTokens.length > 4 || vTokens.length < 1 || vTokens.length > 4) {
2074
+ return void 0;
2075
+ }
2076
+ const hs = expandSides(hTokens).map((t) => parseCSSLength(t, refW));
2077
+ const vs = expandSides(vTokens).map((t) => parseCSSLength(t, refH));
2078
+ if ([...hs, ...vs].some(isNaN)) return void 0;
2079
+ const radii = hs.map((rx, i) => [
2080
+ Math.max(0, rx),
2081
+ Math.max(0, vs[i])
2082
+ ]);
2083
+ if (radii.every(([rx, ry]) => rx <= 0 || ry <= 0)) return null;
2084
+ return normalizeRadii(radii, w, h);
2085
+ }
2086
+ function normalizeRadii(radii, w, h) {
2087
+ const [tl, tr, br, bl] = radii;
2088
+ let f = 1;
2089
+ const consider = (sum, length) => {
2090
+ if (sum > 0 && length / sum < f) f = length / sum;
2091
+ };
2092
+ consider(tl[0] + tr[0], w);
2093
+ consider(bl[0] + br[0], w);
2094
+ consider(tl[1] + bl[1], h);
2095
+ consider(tr[1] + br[1], h);
2096
+ if (f >= 1) return radii;
2097
+ return radii.map(([rx, ry]) => [rx * f, ry * f]);
2098
+ }
2099
+ function roundedRectPath(p, x, y, w, h, radii) {
2100
+ const [tl, tr, br, bl] = radii;
2101
+ const round = (r) => r[0] > 0 && r[1] > 0;
2102
+ const HALF = Math.PI / 2;
2103
+ p.moveTo(x + (round(tl) ? tl[0] : 0), y);
2104
+ p.lineTo(x + w - (round(tr) ? tr[0] : 0), y);
2105
+ if (round(tr)) {
2106
+ p.ellipse(x + w - tr[0], y + tr[1], tr[0], tr[1], 0, -HALF, 0);
2107
+ }
2108
+ p.lineTo(x + w, y + h - (round(br) ? br[1] : 0));
2109
+ if (round(br)) {
2110
+ p.ellipse(x + w - br[0], y + h - br[1], br[0], br[1], 0, 0, HALF);
2111
+ }
2112
+ p.lineTo(x + (round(bl) ? bl[0] : 0), y + h);
2113
+ if (round(bl)) {
2114
+ p.ellipse(x + bl[0], y + h - bl[1], bl[0], bl[1], 0, HALF, Math.PI);
2115
+ }
2116
+ p.lineTo(x, y + (round(tl) ? tl[1] : 0));
2117
+ if (round(tl)) {
2118
+ p.ellipse(x + tl[0], y + tl[1], tl[0], tl[1], 0, Math.PI, 3 * HALF);
2119
+ }
2120
+ p.closePath();
2121
+ }
2122
+ function expandSides(values) {
2123
+ const [a, b, c, d] = values;
2124
+ switch (values.length) {
2125
+ case 1:
2126
+ return [a, a, a, a];
2127
+ case 2:
2128
+ return [a, b, a, b];
2129
+ case 3:
2130
+ return [a, b, c, b];
2131
+ default:
2132
+ return [a, b, c, d];
2133
+ }
2134
+ }
2135
+ function splitRound(args) {
2136
+ const m = args.match(/^(.*?)\bround\b(.*)$/s);
2137
+ if (!m) return [args, void 0];
2138
+ return [m[1], m[2]];
2139
+ }
2140
+ function splitAt(args) {
2141
+ const m = args.match(/^(.*?)\bat\b(.*)$/s);
2142
+ if (!m) return [args, void 0];
2143
+ return [m[1], m[2]];
2144
+ }
2145
+ function splitArgs(content) {
2146
+ const parts = [];
2147
+ let current = "";
2148
+ let depth = 0;
2149
+ let quote = null;
2150
+ for (const char of content) {
2151
+ if (quote) {
2152
+ if (char === quote) quote = null;
2153
+ current += char;
2154
+ continue;
2155
+ }
2156
+ if (char === '"' || char === "'") {
2157
+ quote = char;
2158
+ current += char;
2159
+ continue;
2160
+ }
2161
+ if (char === "(") depth++;
2162
+ if (char === ")") depth--;
2163
+ if (char === "," && depth === 0) {
2164
+ parts.push(current);
2165
+ current = "";
2166
+ } else {
2167
+ current += char;
2168
+ }
2169
+ }
2170
+ if (current.trim()) parts.push(current);
2171
+ return parts;
2172
+ }
2173
+ function splitAtSlash(tokens) {
2174
+ if (tokens.length === 0) return [];
2175
+ const slash = tokens.indexOf("/");
2176
+ return slash === -1 ? [tokens] : [tokens.slice(0, slash), tokens.slice(slash + 1)];
2177
+ }
2178
+ function tokenize(value) {
2179
+ return value.trim().replace(/\//g, " / ").split(/\s+/).filter(Boolean);
2180
+ }
2181
+ function unquote(s) {
2182
+ if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
2183
+ return s.slice(1, -1);
2184
+ }
2185
+ return s;
2186
+ }
2187
+ function parseAngleDegrees(value) {
2188
+ const n = parseFloat(value);
2189
+ if (isNaN(n)) return NaN;
2190
+ if (value.endsWith("deg")) return n;
2191
+ if (value.endsWith("grad")) return n * 0.9;
2192
+ if (value.endsWith("rad")) return n * 180 / Math.PI;
2193
+ if (value.endsWith("turn")) return n * 360;
2194
+ return n;
2195
+ }
2196
+ function fmt(n) {
2197
+ return String(Math.round(n * 1e3) / 1e3);
2198
+ }
2199
+ function fmtPoint(p) {
2200
+ return `${fmt(p[0])} ${fmt(p[1])}`;
2201
+ }
2202
+
2203
+ // src/jsx/draw/gradient.ts
2204
+ function createGradientFromCSS(ctx, cssGradient, x, y, width, height) {
2205
+ const trimmed = cssGradient.trim();
2206
+ if (trimmed.startsWith("linear-gradient")) {
2207
+ return parseLinearGradient(ctx, trimmed, x, y, width, height);
2208
+ }
2209
+ if (trimmed.startsWith("radial-gradient")) {
2210
+ return parseRadialGradient(ctx, trimmed, x, y, width, height);
2211
+ }
2212
+ return null;
2213
+ }
2214
+ function parseLinearGradient(ctx, css, x, y, width, height) {
2215
+ const match = css.match(/linear-gradient\((.*)\)/s);
2216
+ if (!match) return null;
2217
+ const content = match[1].trim();
2218
+ const parts = splitGradientArgs(content);
2219
+ let angle = 180;
2220
+ let colorStartIdx = 0;
2221
+ const first = parts[0]?.trim();
2222
+ if (first) {
2223
+ if (first.startsWith("to ")) {
2224
+ angle = directionToAngle(first);
2225
+ colorStartIdx = 1;
2226
+ } else if (first.endsWith("deg")) {
2227
+ angle = parseFloat(first);
2228
+ colorStartIdx = 1;
2229
+ } else if (first.endsWith("turn")) {
2230
+ angle = parseFloat(first) * 360;
2231
+ colorStartIdx = 1;
2232
+ }
2233
+ }
2234
+ const rad = (angle - 90) * Math.PI / 180;
2235
+ const cx = x + width / 2;
2236
+ const cy = y + height / 2;
2237
+ const halfDiag = Math.abs(width * Math.cos(rad)) / 2 + Math.abs(height * Math.sin(rad)) / 2;
2238
+ const x0 = cx - halfDiag * Math.cos(rad);
2239
+ const y0 = cy - halfDiag * Math.sin(rad);
2240
+ const x1 = cx + halfDiag * Math.cos(rad);
2241
+ const y1 = cy + halfDiag * Math.sin(rad);
2242
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
2243
+ const stops = parts.slice(colorStartIdx);
2244
+ addColorStops(gradient, stops);
2245
+ return gradient;
2246
+ }
2247
+ function parseRadialGradient(ctx, css, x, y, width, height) {
2248
+ const match = css.match(/radial-gradient\((.*)\)/s);
2249
+ if (!match) return null;
2250
+ const content = match[1].trim();
2251
+ const parts = splitGradientArgs(content);
2252
+ const cx = x + width / 2;
2253
+ const cy = y + height / 2;
2254
+ const radius = Math.max(width, height) / 2;
2255
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
2256
+ let colorStartIdx = 0;
2257
+ const first = parts[0]?.trim() ?? "";
2258
+ if (first.startsWith("circle") || first.startsWith("ellipse") || first.startsWith("closest") || first.startsWith("farthest")) {
2259
+ colorStartIdx = 1;
2260
+ }
2261
+ addColorStops(gradient, parts.slice(colorStartIdx));
2262
+ return gradient;
2263
+ }
2264
+ function addColorStops(gradient, stops) {
2265
+ if (stops.length === 0) return;
2266
+ for (let i = 0; i < stops.length; i++) {
2267
+ const stop = stops[i].trim();
2268
+ const percentMatch = stop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);
2269
+ if (percentMatch) {
2270
+ const color = percentMatch[1];
2271
+ const pos = percentMatch[2];
2272
+ const offset = pos.endsWith("%") ? parseFloat(pos) / 100 : parseFloat(pos);
2273
+ gradient.addColorStop(Math.max(0, Math.min(1, offset)), color);
2274
+ } else {
2275
+ const offset = stops.length === 1 ? 0.5 : i / (stops.length - 1);
2276
+ gradient.addColorStop(offset, stop);
2277
+ }
2278
+ }
2279
+ }
2280
+ function directionToAngle(dir) {
2281
+ const map = {
2282
+ "to top": 0,
2283
+ "to right": 90,
2284
+ "to bottom": 180,
2285
+ "to left": 270,
2286
+ "to top right": 45,
2287
+ "to top left": 315,
2288
+ "to bottom right": 135,
2289
+ "to bottom left": 225
2290
+ };
2291
+ return map[dir] ?? 180;
2292
+ }
2293
+ function splitGradientArgs(content) {
2294
+ const parts = [];
2295
+ let current = "";
2296
+ let parenDepth = 0;
2297
+ for (const char of content) {
2298
+ if (char === "(") parenDepth++;
2299
+ if (char === ")") parenDepth--;
2300
+ if (char === "," && parenDepth === 0) {
2301
+ parts.push(current);
2302
+ current = "";
2303
+ } else {
2304
+ current += char;
1320
2305
  }
1321
- return;
1322
2306
  }
1323
- if (hasRoundedCorners) {
1324
- const maxR = Math.min(width, height) / 2;
1325
- const tl = Math.min(borderRadius.topLeft, maxR);
1326
- const tr = Math.min(borderRadius.topRight, maxR);
1327
- const br = Math.min(borderRadius.bottomRight, maxR);
1328
- const bl = Math.min(borderRadius.bottomLeft, maxR);
1329
- const top = y + tw / 2;
1330
- const right = x + width - rw / 2;
1331
- const bottom = y + height - bw / 2;
1332
- const left = x + lw / 2;
1333
- const eTL = Math.max(0, tl - Math.max(tw, lw) / 2);
1334
- const eTR = Math.max(0, tr - Math.max(tw, rw) / 2);
1335
- const eBR = Math.max(0, br - Math.max(bw, rw) / 2);
1336
- const eBL = Math.max(0, bl - Math.max(bw, lw) / 2);
1337
- if (tw > 0) {
1338
- ctx.strokeStyle = tc;
1339
- ctx.lineWidth = tw;
1340
- ctx.beginPath();
1341
- if (eTL > 0 && lw === 0) {
1342
- ctx.moveTo(left, top + eTL);
1343
- ctx.arcTo(left, top, left + eTL, top, eTL);
1344
- } else if (eTL > 0) {
1345
- ctx.moveTo(left + eTL, top);
1346
- } else {
1347
- ctx.moveTo(x, top);
1348
- }
1349
- if (eTR > 0) {
1350
- ctx.lineTo(right - eTR, top);
1351
- ctx.arcTo(right, top, right, top + eTR, eTR);
1352
- } else {
1353
- ctx.lineTo(x + width, top);
1354
- }
1355
- ctx.stroke();
1356
- }
1357
- if (rw > 0) {
1358
- ctx.strokeStyle = rc;
1359
- ctx.lineWidth = rw;
1360
- ctx.beginPath();
1361
- if (eTR > 0 && tw === 0) {
1362
- ctx.moveTo(right - eTR, top);
1363
- ctx.arcTo(right, top, right, top + eTR, eTR);
1364
- } else if (eTR > 0) {
1365
- ctx.moveTo(right, top + eTR);
1366
- } else {
1367
- ctx.moveTo(right, y);
1368
- }
1369
- if (eBR > 0) {
1370
- ctx.lineTo(right, bottom - eBR);
1371
- ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1372
- } else {
1373
- ctx.lineTo(right, y + height);
1374
- }
1375
- ctx.stroke();
1376
- }
1377
- if (bw > 0) {
1378
- ctx.strokeStyle = bc;
1379
- ctx.lineWidth = bw;
1380
- ctx.beginPath();
1381
- if (eBR > 0 && rw === 0) {
1382
- ctx.moveTo(right, bottom - eBR);
1383
- ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1384
- } else if (eBR > 0) {
1385
- ctx.moveTo(right - eBR, bottom);
1386
- } else {
1387
- ctx.moveTo(x + width, bottom);
1388
- }
1389
- if (eBL > 0) {
1390
- ctx.lineTo(left + eBL, bottom);
1391
- ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1392
- } else {
1393
- ctx.lineTo(x, bottom);
1394
- }
1395
- ctx.stroke();
1396
- }
1397
- if (lw > 0) {
1398
- ctx.strokeStyle = lc;
1399
- ctx.lineWidth = lw;
1400
- ctx.beginPath();
1401
- if (eBL > 0 && bw === 0) {
1402
- ctx.moveTo(left + eBL, bottom);
1403
- ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1404
- } else if (eBL > 0) {
1405
- ctx.moveTo(left, bottom - eBL);
1406
- } else {
1407
- ctx.moveTo(left, y + height);
1408
- }
1409
- if (eTL > 0) {
1410
- ctx.lineTo(left, top + eTL);
1411
- ctx.arcTo(left, top, left + eTL, top, eTL);
1412
- } else {
1413
- ctx.lineTo(left, y);
1414
- }
1415
- ctx.stroke();
1416
- }
2307
+ if (current.trim()) parts.push(current);
2308
+ return parts;
2309
+ }
2310
+
2311
+ // src/jsx/draw/object-fit.ts
2312
+ function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
2313
+ const imgRatio = imgW / imgH;
2314
+ const boxRatio = boxW / boxH;
2315
+ let sx, sy, sw, sh;
2316
+ if (imgRatio > boxRatio) {
2317
+ sh = imgH;
2318
+ sw = imgH * boxRatio;
2319
+ sx = (imgW - sw) / 2;
2320
+ sy = 0;
1417
2321
  } else {
1418
- if (tw > 0) {
1419
- ctx.strokeStyle = tc;
1420
- ctx.lineWidth = tw;
1421
- ctx.beginPath();
1422
- ctx.moveTo(x, y + tw / 2);
1423
- ctx.lineTo(x + width, y + tw / 2);
1424
- ctx.stroke();
1425
- }
1426
- if (rw > 0) {
1427
- ctx.strokeStyle = rc;
1428
- ctx.lineWidth = rw;
1429
- ctx.beginPath();
1430
- ctx.moveTo(x + width - rw / 2, y);
1431
- ctx.lineTo(x + width - rw / 2, y + height);
1432
- ctx.stroke();
1433
- }
1434
- if (bw > 0) {
1435
- ctx.strokeStyle = bc;
1436
- ctx.lineWidth = bw;
1437
- ctx.beginPath();
1438
- ctx.moveTo(x, y + height - bw / 2);
1439
- ctx.lineTo(x + width, y + height - bw / 2);
1440
- ctx.stroke();
1441
- }
1442
- if (lw > 0) {
1443
- ctx.strokeStyle = lc;
1444
- ctx.lineWidth = lw;
1445
- ctx.beginPath();
1446
- ctx.moveTo(x + lw / 2, y);
1447
- ctx.lineTo(x + lw / 2, y + height);
1448
- ctx.stroke();
1449
- }
2322
+ sw = imgW;
2323
+ sh = imgW / boxRatio;
2324
+ sx = 0;
2325
+ sy = (imgH - sh) / 2;
1450
2326
  }
2327
+ return { sx, sy, sw, sh, dx: boxX, dy: boxY, dw: boxW, dh: boxH };
1451
2328
  }
1452
- var BOX_SHADOW_RE = /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/;
1453
- function boxShadowExtent(boxShadow) {
1454
- const parts = boxShadow.match(BOX_SHADOW_RE);
1455
- if (!parts) return 0;
1456
- const offsetX = parseFloat(parts[1]);
1457
- const offsetY = parseFloat(parts[2]);
1458
- const blur = parseFloat(parts[3]);
1459
- return blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
2329
+ function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
2330
+ const imgRatio = imgW / imgH;
2331
+ const boxRatio = boxW / boxH;
2332
+ let dw, dh;
2333
+ if (imgRatio > boxRatio) {
2334
+ dw = boxW;
2335
+ dh = boxW / imgRatio;
2336
+ } else {
2337
+ dh = boxH;
2338
+ dw = boxH * imgRatio;
2339
+ }
2340
+ const dx = boxX + (boxW - dw) / 2;
2341
+ const dy = boxY + (boxH - dh) / 2;
2342
+ return { sx: 0, sy: 0, sw: imgW, sh: imgH, dx, dy, dw, dh };
1460
2343
  }
1461
- function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1462
- const parts = boxShadow.match(BOX_SHADOW_RE);
1463
- if (!parts) return;
1464
- const offsetX = parseFloat(parts[1]);
1465
- const offsetY = parseFloat(parts[2]);
1466
- const blur = parseFloat(parts[3]);
1467
- const color = parts[5].trim();
1468
- const radii = [
1469
- borderRadius.topLeft,
1470
- borderRadius.topRight,
1471
- borderRadius.bottomRight,
1472
- borderRadius.bottomLeft
1473
- ];
1474
- const margin = blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
1475
- ctx.save();
1476
- ctx.beginPath();
1477
- ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
1478
- ctx.roundRect(x, y, width, height, radii);
1479
- ctx.clip("evenodd");
1480
- ctx.filter = `blur(${blur / 2}px)`;
1481
- ctx.translate(offsetX, offsetY);
1482
- ctx.fillStyle = color;
1483
- ctx.beginPath();
1484
- ctx.roundRect(x, y, width, height, radii);
1485
- ctx.fill();
1486
- ctx.restore();
2344
+
2345
+ // src/jsx/draw/image.ts
2346
+ async function drawImage(ctx, src, x, y, width, height, context, style) {
2347
+ const image = await cachedLoadImage(
2348
+ context.imageCache,
2349
+ src,
2350
+ context.userAgent
2351
+ );
2352
+ const objectFit = style?.objectFit ?? "fill";
2353
+ if (objectFit === "fill") {
2354
+ ctx.drawImage(image, x, y, width, height);
2355
+ return;
2356
+ }
2357
+ const imgW = image.width;
2358
+ const imgH = image.height;
2359
+ if (objectFit === "contain") {
2360
+ const r = computeContain(imgW, imgH, x, y, width, height);
2361
+ ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
2362
+ } else if (objectFit === "cover") {
2363
+ const r = computeCover(imgW, imgH, x, y, width, height);
2364
+ ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
2365
+ } else if (objectFit === "none") {
2366
+ const dx = x + (width - imgW) / 2;
2367
+ const dy = y + (height - imgH) / 2;
2368
+ ctx.drawImage(image, dx, dy);
2369
+ } else if (objectFit === "scale-down") {
2370
+ if (imgW <= width && imgH <= height) {
2371
+ const dx = x + (width - imgW) / 2;
2372
+ const dy = y + (height - imgH) / 2;
2373
+ ctx.drawImage(image, dx, dy);
2374
+ } else {
2375
+ const r = computeContain(imgW, imgH, x, y, width, height);
2376
+ ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
2377
+ }
2378
+ }
1487
2379
  }
1488
2380
 
1489
2381
  // src/jsx/draw/svg/style.ts
@@ -1637,7 +2529,7 @@ function applySvgTransform(ctx, transform) {
1637
2529
  }
1638
2530
 
1639
2531
  // src/jsx/draw/svg/path.ts
1640
- import { Path2D } from "@napi-rs/canvas";
2532
+ import { Path2D as Path2D2 } from "@napi-rs/canvas";
1641
2533
  function pathBBox(d) {
1642
2534
  let minX = Infinity;
1643
2535
  let minY = Infinity;
@@ -1798,14 +2690,14 @@ function buildPath(child, vbW, vbH) {
1798
2690
  case "path": {
1799
2691
  const d = props.d;
1800
2692
  if (!d) return void 0;
1801
- return new Path2D(d);
2693
+ return new Path2D2(d);
1802
2694
  }
1803
2695
  case "circle": {
1804
2696
  const cx = svgLength(props.cx, vbW);
1805
2697
  const cy = svgLength(props.cy, vbH);
1806
2698
  const r = svgLength(props.r, Math.min(vbW, vbH));
1807
2699
  if (r <= 0) return void 0;
1808
- const p = new Path2D();
2700
+ const p = new Path2D2();
1809
2701
  p.arc(cx, cy, r, 0, Math.PI * 2);
1810
2702
  return p;
1811
2703
  }
@@ -1821,7 +2713,7 @@ function buildPath(child, vbW, vbH) {
1821
2713
  let ry = ryRaw >= 0 ? ryRaw : rx;
1822
2714
  rx = Math.min(rx, w / 2);
1823
2715
  ry = Math.min(ry, h / 2);
1824
- const p = new Path2D();
2716
+ const p = new Path2D2();
1825
2717
  if (rx > 0 || ry > 0) {
1826
2718
  p.roundRect(x, y, w, h, [rx]);
1827
2719
  } else {
@@ -1835,14 +2727,14 @@ function buildPath(child, vbW, vbH) {
1835
2727
  const rx = svgLength(props.rx, vbW);
1836
2728
  const ry = svgLength(props.ry, vbH);
1837
2729
  if (rx <= 0 || ry <= 0) return void 0;
1838
- const p = new Path2D();
2730
+ const p = new Path2D2();
1839
2731
  p.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
1840
2732
  return p;
1841
2733
  }
1842
2734
  case "polygon": {
1843
2735
  const points = parsePoints(props.points);
1844
2736
  if (points.length < 2) return void 0;
1845
- const p = new Path2D();
2737
+ const p = new Path2D2();
1846
2738
  p.moveTo(points[0][0], points[0][1]);
1847
2739
  for (let i = 1; i < points.length; i++) {
1848
2740
  p.lineTo(points[i][0], points[i][1]);
@@ -2026,10 +2918,10 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
2026
2918
  }
2027
2919
 
2028
2920
  // src/jsx/draw/svg/shapes.ts
2029
- import { Path2D as Path2D3 } from "@napi-rs/canvas";
2921
+ import { Path2D as Path2D4 } from "@napi-rs/canvas";
2030
2922
 
2031
2923
  // src/jsx/draw/svg/gradients.ts
2032
- import { Path2D as Path2D2 } from "@napi-rs/canvas";
2924
+ import { Path2D as Path2D3 } from "@napi-rs/canvas";
2033
2925
  import parseCssColor from "parse-css-color";
2034
2926
  function parseFrac(value, fallback) {
2035
2927
  if (value == null) return fallback;
@@ -2117,7 +3009,7 @@ function fillWithSvgGradient(ctx, def, bbox, path, fillRule, viewport) {
2117
3009
  const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
2118
3010
  addGradientStops(gradient, def);
2119
3011
  ctx.fillStyle = gradient;
2120
- const unitPath = new Path2D2();
3012
+ const unitPath = new Path2D3();
2121
3013
  const invTransform = {
2122
3014
  a: 1 / bbox.width,
2123
3015
  b: 0,
@@ -2239,7 +3131,7 @@ function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
2239
3131
  function drawPath(ctx, props, inherited, color, defs) {
2240
3132
  const d = props.d;
2241
3133
  if (!d) return;
2242
- const path = new Path2D3(d);
3134
+ const path = new Path2D4(d);
2243
3135
  const bbox = pathBBox(d);
2244
3136
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2245
3137
  }
@@ -2248,7 +3140,7 @@ function drawCircle(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2248
3140
  const cy = svgLength(props.cy, vbH);
2249
3141
  const r = svgLength(props.r, Math.min(vbW, vbH));
2250
3142
  if (r <= 0) return;
2251
- const path = new Path2D3();
3143
+ const path = new Path2D4();
2252
3144
  path.arc(cx, cy, r, 0, Math.PI * 2);
2253
3145
  const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
2254
3146
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
@@ -2265,7 +3157,7 @@ function drawSvgRect(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2265
3157
  let ry = ryRaw >= 0 ? ryRaw : rx;
2266
3158
  rx = Math.min(rx, w / 2);
2267
3159
  ry = Math.min(ry, h / 2);
2268
- const path = new Path2D3();
3160
+ const path = new Path2D4();
2269
3161
  if (rx > 0 || ry > 0) {
2270
3162
  path.roundRect(x, y, w, h, [rx]);
2271
3163
  } else {
@@ -2279,7 +3171,7 @@ function drawLine(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2279
3171
  const y1 = svgLength(props.y1, vbH);
2280
3172
  const x2 = svgLength(props.x2, vbW);
2281
3173
  const y2 = svgLength(props.y2, vbH);
2282
- const path = new Path2D3();
3174
+ const path = new Path2D4();
2283
3175
  path.moveTo(x1, y1);
2284
3176
  path.lineTo(x2, y2);
2285
3177
  const bbox = {
@@ -2296,7 +3188,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2296
3188
  const rx = svgLength(props.rx, vbW);
2297
3189
  const ry = svgLength(props.ry, vbH);
2298
3190
  if (rx <= 0 || ry <= 0) return;
2299
- const path = new Path2D3();
3191
+ const path = new Path2D4();
2300
3192
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
2301
3193
  const bbox = {
2302
3194
  x: cx - rx,
@@ -2309,7 +3201,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2309
3201
  function drawPolygon(ctx, props, inherited, color, defs) {
2310
3202
  const points = parsePoints(props.points);
2311
3203
  if (points.length < 2) return;
2312
- const path = new Path2D3();
3204
+ const path = new Path2D4();
2313
3205
  path.moveTo(points[0][0], points[0][1]);
2314
3206
  for (let i = 1; i < points.length; i++) {
2315
3207
  path.lineTo(points[i][0], points[i][1]);
@@ -2321,7 +3213,7 @@ function drawPolygon(ctx, props, inherited, color, defs) {
2321
3213
  function drawPolyline(ctx, props, inherited, color, defs) {
2322
3214
  const points = parsePoints(props.points);
2323
3215
  if (points.length < 2) return;
2324
- const path = new Path2D3();
3216
+ const path = new Path2D4();
2325
3217
  path.moveTo(points[0][0], points[0][1]);
2326
3218
  for (let i = 1; i < points.length; i++) {
2327
3219
  path.lineTo(points[i][0], points[i][1]);
@@ -2824,11 +3716,12 @@ async function drawNode(ctx, node, parentX, parentY, context, emojiStyle) {
2824
3716
  if (opacity <= 0) return;
2825
3717
  const scaleInfo = style.transform ? extractScale(style.transform) : null;
2826
3718
  const hasOtherTransforms = scaleInfo !== null && scaleInfo.remaining.length > 0;
2827
- if (scaleInfo && (scaleInfo.sx !== 1 || scaleInfo.sy !== 1) && !hasOtherTransforms) {
3719
+ const subtree = scaleInfo && (scaleInfo.sx !== 1 || scaleInfo.sy !== 1) && !hasOtherTransforms ? scanSubtree(node) : null;
3720
+ if (scaleInfo && subtree && !subtree.hasBackdropFilter) {
2828
3721
  const sx = scaleInfo.sx;
2829
3722
  const sy = scaleInfo.sy;
2830
3723
  const transformWithoutScale = scaleInfo.remaining;
2831
- const bleed = Math.max(1, computeOverflowBleed(node));
3724
+ const bleed = Math.max(1, subtree.overflowBleed);
2832
3725
  const qx = Math.max(1, Math.ceil(Math.abs(sx)));
2833
3726
  const qy = Math.max(1, Math.ceil(Math.abs(sy)));
2834
3727
  const bufW = Math.ceil((width + 2 * bleed) * qx);
@@ -2900,23 +3793,32 @@ function extractScale(transform) {
2900
3793
  const remaining = transform.replace(fullMatch, "").trim();
2901
3794
  return { sx, sy, remaining };
2902
3795
  }
2903
- function computeOverflowBleed(node) {
2904
- let bleed = 0;
3796
+ function scanSubtree(node) {
3797
+ let overflowBleed = 0;
3798
+ let hasBackdropFilter = false;
2905
3799
  const visit = (n) => {
2906
3800
  if (n.style.display === "none") return;
2907
3801
  if ((n.style.opacity ?? 1) <= 0) return;
2908
3802
  const fontSize = typeof n.style.fontSize === "number" ? n.style.fontSize : 0;
2909
3803
  if (n.textContent !== void 0 && n.textContent !== "" && fontSize > 0) {
2910
3804
  const letterSpacing = typeof n.style.letterSpacing === "number" ? n.style.letterSpacing : 0;
2911
- bleed = Math.max(bleed, fontSize + Math.max(0, -letterSpacing));
3805
+ overflowBleed = Math.max(
3806
+ overflowBleed,
3807
+ fontSize + Math.max(0, -letterSpacing)
3808
+ );
2912
3809
  }
2913
3810
  if (n.style.boxShadow) {
2914
- bleed = Math.max(bleed, boxShadowExtent(n.style.boxShadow));
3811
+ overflowBleed = Math.max(
3812
+ overflowBleed,
3813
+ boxShadowExtent(n.style.boxShadow)
3814
+ );
2915
3815
  }
3816
+ const filter = n.style.backdropFilter;
3817
+ if (filter && filter.trim() !== "none") hasBackdropFilter = true;
2916
3818
  for (const child of n.children) visit(child);
2917
3819
  };
2918
3820
  visit(node);
2919
- return bleed;
3821
+ return { overflowBleed, hasBackdropFilter };
2920
3822
  }
2921
3823
  async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emojiStyle, context, overrideTransform) {
2922
3824
  const x = parentX + node.x + offsetX;
@@ -2944,7 +3846,21 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
2944
3846
  style.transformOrigin
2945
3847
  );
2946
3848
  }
3849
+ if (style.clipPath) {
3850
+ applyClipPath(ctx, style.clipPath, style, x, y, width, height);
3851
+ }
2947
3852
  const borderRadius = getBorderRadiusFromStyle(style, width, height);
3853
+ if (style.backdropFilter) {
3854
+ drawBackdropFilter(
3855
+ ctx,
3856
+ style.backdropFilter,
3857
+ x,
3858
+ y,
3859
+ width,
3860
+ height,
3861
+ borderRadius
3862
+ );
3863
+ }
2948
3864
  if (style.boxShadow) {
2949
3865
  drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
2950
3866
  }
@@ -3370,6 +4286,15 @@ function expandStyle(raw, fontFamilies) {
3370
4286
  }
3371
4287
  delete style.WebkitTextStroke;
3372
4288
  }
4289
+ if (style.WebkitBackdropFilter !== void 0) {
4290
+ if (style.backdropFilter === void 0)
4291
+ style.backdropFilter = style.WebkitBackdropFilter;
4292
+ delete style.WebkitBackdropFilter;
4293
+ }
4294
+ if (style.WebkitClipPath !== void 0) {
4295
+ if (style.clipPath === void 0) style.clipPath = style.WebkitClipPath;
4296
+ delete style.WebkitClipPath;
4297
+ }
3373
4298
  if (typeof style.fontFamily === "string") {
3374
4299
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
3375
4300
  if (fontFamilies) {
@@ -4087,9 +5012,18 @@ function createCanvas3(width, height) {
4087
5012
  }
4088
5013
  export {
4089
5014
  Canvas,
5015
+ DOMMatrix,
5016
+ DOMPoint,
5017
+ DOMRect,
5018
+ FillType,
4090
5019
  GlobalFonts2 as GlobalFonts,
4091
5020
  Image,
5021
+ ImageData,
4092
5022
  LottieAnimation,
5023
+ Path2D5 as Path2D,
5024
+ PathOp,
5025
+ StrokeCap,
5026
+ StrokeJoin,
4093
5027
  createCanvas3 as createCanvas,
4094
5028
  findLargestUsableFontSize,
4095
5029
  loadImage,