@effing/canvas 0.40.2 → 0.41.1

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
@@ -5,7 +5,7 @@ import {
5
5
  GlobalFonts as GlobalFonts2,
6
6
  Image,
7
7
  ImageData,
8
- Path2D as Path2D4,
8
+ Path2D as Path2D5,
9
9
  DOMMatrix,
10
10
  DOMPoint,
11
11
  DOMRect,
@@ -68,6 +68,30 @@ function renderLottieFrame(ctx, animation, frame) {
68
68
  animation.render(ctx);
69
69
  }
70
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
+
71
95
  // src/jsx/style/compute.ts
72
96
  var ROOT_FONT_SIZE = 16;
73
97
  var DEFAULT_FONT_FAMILY = "Helvetica, Arial, sans-serif";
@@ -258,29 +282,31 @@ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_
258
282
  style[prop] = resolved;
259
283
  }
260
284
  }
261
- if (style.transform) {
262
- style.transform = resolveTransformUnits(
263
- style.transform,
264
- viewportWidth,
265
- viewportHeight,
266
- fontSize,
267
- rootFontSize
268
- );
269
- }
270
- if (style.transformOrigin) {
271
- style.transformOrigin = resolveTransformUnits(
272
- 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,
273
290
  viewportWidth,
274
291
  viewportHeight,
275
292
  fontSize,
276
- rootFontSize
293
+ rootFontSize,
294
+ suffix
277
295
  );
278
296
  }
279
297
  return style;
280
298
  }
281
- 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 = "") {
282
307
  return transform.replace(
283
- /(-?\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,
284
310
  (match) => {
285
311
  const resolved = resolveUnit(
286
312
  match,
@@ -289,7 +315,7 @@ function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSiz
289
315
  fontSize,
290
316
  rootFontSize
291
317
  );
292
- return typeof resolved === "number" ? String(resolved) : match;
318
+ return typeof resolved === "number" ? `${formatCSSNumber(resolved)}${suffix}` : match;
293
319
  }
294
320
  );
295
321
  }
@@ -1007,7 +1033,7 @@ function applyClip(ctx, x, y, width, height, borderRadius) {
1007
1033
  ctx.clip();
1008
1034
  }
1009
1035
  function roundedRect(ctx, x, y, w, h, tl, tr, br, bl) {
1010
- const maxR = Math.min(w, h) / 2;
1036
+ const maxR = maxCornerRadius(w, h);
1011
1037
  tl = Math.min(tl, maxR);
1012
1038
  tr = Math.min(tr, maxR);
1013
1039
  br = Math.min(br, maxR);
@@ -1023,188 +1049,13 @@ function roundedRect(ctx, x, y, w, h, tl, tr, br, bl) {
1023
1049
  if (tl > 0) ctx.arcTo(x, y, x + tl, y, tl);
1024
1050
  ctx.closePath();
1025
1051
  }
1052
+ function maxCornerRadius(w, h) {
1053
+ return Math.min(w, h) / 2;
1054
+ }
1026
1055
  function hasRadius(r) {
1027
1056
  return r.topLeft > 0 || r.topRight > 0 || r.bottomRight > 0 || r.bottomLeft > 0;
1028
1057
  }
1029
1058
 
1030
- // src/jsx/draw/gradient.ts
1031
- function createGradientFromCSS(ctx, cssGradient, x, y, width, height) {
1032
- const trimmed = cssGradient.trim();
1033
- if (trimmed.startsWith("linear-gradient")) {
1034
- return parseLinearGradient(ctx, trimmed, x, y, width, height);
1035
- }
1036
- if (trimmed.startsWith("radial-gradient")) {
1037
- return parseRadialGradient(ctx, trimmed, x, y, width, height);
1038
- }
1039
- return null;
1040
- }
1041
- function parseLinearGradient(ctx, css, x, y, width, height) {
1042
- const match = css.match(/linear-gradient\((.*)\)/s);
1043
- if (!match) return null;
1044
- const content = match[1].trim();
1045
- const parts = splitGradientArgs(content);
1046
- let angle = 180;
1047
- let colorStartIdx = 0;
1048
- const first = parts[0]?.trim();
1049
- if (first) {
1050
- if (first.startsWith("to ")) {
1051
- angle = directionToAngle(first);
1052
- colorStartIdx = 1;
1053
- } else if (first.endsWith("deg")) {
1054
- angle = parseFloat(first);
1055
- colorStartIdx = 1;
1056
- } else if (first.endsWith("turn")) {
1057
- angle = parseFloat(first) * 360;
1058
- colorStartIdx = 1;
1059
- }
1060
- }
1061
- const rad = (angle - 90) * Math.PI / 180;
1062
- const cx = x + width / 2;
1063
- const cy = y + height / 2;
1064
- const halfDiag = Math.abs(width * Math.cos(rad)) / 2 + Math.abs(height * Math.sin(rad)) / 2;
1065
- const x0 = cx - halfDiag * Math.cos(rad);
1066
- const y0 = cy - halfDiag * Math.sin(rad);
1067
- const x1 = cx + halfDiag * Math.cos(rad);
1068
- const y1 = cy + halfDiag * Math.sin(rad);
1069
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
1070
- const stops = parts.slice(colorStartIdx);
1071
- addColorStops(gradient, stops);
1072
- return gradient;
1073
- }
1074
- function parseRadialGradient(ctx, css, x, y, width, height) {
1075
- const match = css.match(/radial-gradient\((.*)\)/s);
1076
- if (!match) return null;
1077
- const content = match[1].trim();
1078
- const parts = splitGradientArgs(content);
1079
- const cx = x + width / 2;
1080
- const cy = y + height / 2;
1081
- const radius = Math.max(width, height) / 2;
1082
- const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
1083
- let colorStartIdx = 0;
1084
- const first = parts[0]?.trim() ?? "";
1085
- if (first.startsWith("circle") || first.startsWith("ellipse") || first.startsWith("closest") || first.startsWith("farthest")) {
1086
- colorStartIdx = 1;
1087
- }
1088
- addColorStops(gradient, parts.slice(colorStartIdx));
1089
- return gradient;
1090
- }
1091
- function addColorStops(gradient, stops) {
1092
- if (stops.length === 0) return;
1093
- for (let i = 0; i < stops.length; i++) {
1094
- const stop = stops[i].trim();
1095
- const percentMatch = stop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);
1096
- if (percentMatch) {
1097
- const color = percentMatch[1];
1098
- const pos = percentMatch[2];
1099
- const offset = pos.endsWith("%") ? parseFloat(pos) / 100 : parseFloat(pos);
1100
- gradient.addColorStop(Math.max(0, Math.min(1, offset)), color);
1101
- } else {
1102
- const offset = stops.length === 1 ? 0.5 : i / (stops.length - 1);
1103
- gradient.addColorStop(offset, stop);
1104
- }
1105
- }
1106
- }
1107
- function directionToAngle(dir) {
1108
- const map = {
1109
- "to top": 0,
1110
- "to right": 90,
1111
- "to bottom": 180,
1112
- "to left": 270,
1113
- "to top right": 45,
1114
- "to top left": 315,
1115
- "to bottom right": 135,
1116
- "to bottom left": 225
1117
- };
1118
- return map[dir] ?? 180;
1119
- }
1120
- function splitGradientArgs(content) {
1121
- const parts = [];
1122
- let current = "";
1123
- let parenDepth = 0;
1124
- for (const char of content) {
1125
- if (char === "(") parenDepth++;
1126
- if (char === ")") parenDepth--;
1127
- if (char === "," && parenDepth === 0) {
1128
- parts.push(current);
1129
- current = "";
1130
- } else {
1131
- current += char;
1132
- }
1133
- }
1134
- if (current.trim()) parts.push(current);
1135
- return parts;
1136
- }
1137
-
1138
- // src/jsx/draw/object-fit.ts
1139
- function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
1140
- const imgRatio = imgW / imgH;
1141
- const boxRatio = boxW / boxH;
1142
- let sx, sy, sw, sh;
1143
- if (imgRatio > boxRatio) {
1144
- sh = imgH;
1145
- sw = imgH * boxRatio;
1146
- sx = (imgW - sw) / 2;
1147
- sy = 0;
1148
- } else {
1149
- sw = imgW;
1150
- sh = imgW / boxRatio;
1151
- sx = 0;
1152
- sy = (imgH - sh) / 2;
1153
- }
1154
- return { sx, sy, sw, sh, dx: boxX, dy: boxY, dw: boxW, dh: boxH };
1155
- }
1156
- function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
1157
- const imgRatio = imgW / imgH;
1158
- const boxRatio = boxW / boxH;
1159
- let dw, dh;
1160
- if (imgRatio > boxRatio) {
1161
- dw = boxW;
1162
- dh = boxW / imgRatio;
1163
- } else {
1164
- dh = boxH;
1165
- dw = boxH * imgRatio;
1166
- }
1167
- const dx = boxX + (boxW - dw) / 2;
1168
- const dy = boxY + (boxH - dh) / 2;
1169
- return { sx: 0, sy: 0, sw: imgW, sh: imgH, dx, dy, dw, dh };
1170
- }
1171
-
1172
- // src/jsx/draw/image.ts
1173
- async function drawImage(ctx, src, x, y, width, height, context, style) {
1174
- const image = await cachedLoadImage(
1175
- context.imageCache,
1176
- src,
1177
- context.userAgent
1178
- );
1179
- const objectFit = style?.objectFit ?? "fill";
1180
- if (objectFit === "fill") {
1181
- ctx.drawImage(image, x, y, width, height);
1182
- return;
1183
- }
1184
- const imgW = image.width;
1185
- const imgH = image.height;
1186
- if (objectFit === "contain") {
1187
- const r = computeContain(imgW, imgH, x, y, width, height);
1188
- ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
1189
- } else if (objectFit === "cover") {
1190
- const r = computeCover(imgW, imgH, x, y, width, height);
1191
- ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
1192
- } else if (objectFit === "none") {
1193
- const dx = x + (width - imgW) / 2;
1194
- const dy = y + (height - imgH) / 2;
1195
- ctx.drawImage(image, dx, dy);
1196
- } else if (objectFit === "scale-down") {
1197
- if (imgW <= width && imgH <= height) {
1198
- const dx = x + (width - imgW) / 2;
1199
- const dy = y + (height - imgH) / 2;
1200
- ctx.drawImage(image, dx, dy);
1201
- } else {
1202
- const r = computeContain(imgW, imgH, x, y, width, height);
1203
- ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
1204
- }
1205
- }
1206
- }
1207
-
1208
1059
  // src/jsx/draw/offscreen.ts
1209
1060
  import { createCanvas as createCanvas2 } from "@napi-rs/canvas";
1210
1061
  var canvasPool = /* @__PURE__ */ new Map();
@@ -1235,26 +1086,169 @@ function releaseOffscreen(canvas) {
1235
1086
  stack.push(new WeakRef(canvas));
1236
1087
  }
1237
1088
 
1238
- // src/jsx/draw/utils.ts
1239
- function parseCSSLength(value, referenceSize) {
1240
- if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
1241
- 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;
1242
1198
  }
1243
- function toNumber(v) {
1244
- if (typeof v === "number") return v;
1245
- if (v === void 0 || v === null) return 0;
1246
- const n = parseFloat(String(v));
1247
- 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
+ };
1248
1228
  }
1249
- function resolveBoxValue(v, referenceWidth) {
1250
- if (typeof v === "number") return v;
1251
- if (v === void 0 || v === null) return 0;
1252
- const s = String(v);
1253
- if (s.endsWith("%")) return parseFloat(s) / 100 * referenceWidth;
1254
- const n = parseFloat(s);
1255
- 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;
1241
+ }
1242
+ function scaleFilterLengths(filter, scale) {
1243
+ return filter.replace(
1244
+ /(?<![\w.])(-?\d*\.?\d+)px/g,
1245
+ (_, n) => `${formatCSSNumber(parseFloat(n) * scale)}px`
1246
+ );
1256
1247
  }
1257
1248
 
1249
+ // src/jsx/draw/clip-path.ts
1250
+ import { Path2D } from "@napi-rs/canvas";
1251
+
1258
1252
  // src/jsx/draw/rect.ts
1259
1253
  function drawRect(ctx, x, y, width, height, style) {
1260
1254
  const borderRadius = getBorderRadiusFromStyle(style, width, height);
@@ -1325,174 +1319,1063 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
1325
1319
  );
1326
1320
  ctx.stroke();
1327
1321
  } else {
1328
- 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;
1329
2305
  }
1330
- return;
1331
2306
  }
1332
- if (hasRoundedCorners) {
1333
- const maxR = Math.min(width, height) / 2;
1334
- const tl = Math.min(borderRadius.topLeft, maxR);
1335
- const tr = Math.min(borderRadius.topRight, maxR);
1336
- const br = Math.min(borderRadius.bottomRight, maxR);
1337
- const bl = Math.min(borderRadius.bottomLeft, maxR);
1338
- const top = y + tw / 2;
1339
- const right = x + width - rw / 2;
1340
- const bottom = y + height - bw / 2;
1341
- const left = x + lw / 2;
1342
- const eTL = Math.max(0, tl - Math.max(tw, lw) / 2);
1343
- const eTR = Math.max(0, tr - Math.max(tw, rw) / 2);
1344
- const eBR = Math.max(0, br - Math.max(bw, rw) / 2);
1345
- const eBL = Math.max(0, bl - Math.max(bw, lw) / 2);
1346
- if (tw > 0) {
1347
- ctx.strokeStyle = tc;
1348
- ctx.lineWidth = tw;
1349
- ctx.beginPath();
1350
- if (eTL > 0 && lw === 0) {
1351
- ctx.moveTo(left, top + eTL);
1352
- ctx.arcTo(left, top, left + eTL, top, eTL);
1353
- } else if (eTL > 0) {
1354
- ctx.moveTo(left + eTL, top);
1355
- } else {
1356
- ctx.moveTo(x, top);
1357
- }
1358
- if (eTR > 0) {
1359
- ctx.lineTo(right - eTR, top);
1360
- ctx.arcTo(right, top, right, top + eTR, eTR);
1361
- } else {
1362
- ctx.lineTo(x + width, top);
1363
- }
1364
- ctx.stroke();
1365
- }
1366
- if (rw > 0) {
1367
- ctx.strokeStyle = rc;
1368
- ctx.lineWidth = rw;
1369
- ctx.beginPath();
1370
- if (eTR > 0 && tw === 0) {
1371
- ctx.moveTo(right - eTR, top);
1372
- ctx.arcTo(right, top, right, top + eTR, eTR);
1373
- } else if (eTR > 0) {
1374
- ctx.moveTo(right, top + eTR);
1375
- } else {
1376
- ctx.moveTo(right, y);
1377
- }
1378
- if (eBR > 0) {
1379
- ctx.lineTo(right, bottom - eBR);
1380
- ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1381
- } else {
1382
- ctx.lineTo(right, y + height);
1383
- }
1384
- ctx.stroke();
1385
- }
1386
- if (bw > 0) {
1387
- ctx.strokeStyle = bc;
1388
- ctx.lineWidth = bw;
1389
- ctx.beginPath();
1390
- if (eBR > 0 && rw === 0) {
1391
- ctx.moveTo(right, bottom - eBR);
1392
- ctx.arcTo(right, bottom, right - eBR, bottom, eBR);
1393
- } else if (eBR > 0) {
1394
- ctx.moveTo(right - eBR, bottom);
1395
- } else {
1396
- ctx.moveTo(x + width, bottom);
1397
- }
1398
- if (eBL > 0) {
1399
- ctx.lineTo(left + eBL, bottom);
1400
- ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1401
- } else {
1402
- ctx.lineTo(x, bottom);
1403
- }
1404
- ctx.stroke();
1405
- }
1406
- if (lw > 0) {
1407
- ctx.strokeStyle = lc;
1408
- ctx.lineWidth = lw;
1409
- ctx.beginPath();
1410
- if (eBL > 0 && bw === 0) {
1411
- ctx.moveTo(left + eBL, bottom);
1412
- ctx.arcTo(left, bottom, left, bottom - eBL, eBL);
1413
- } else if (eBL > 0) {
1414
- ctx.moveTo(left, bottom - eBL);
1415
- } else {
1416
- ctx.moveTo(left, y + height);
1417
- }
1418
- if (eTL > 0) {
1419
- ctx.lineTo(left, top + eTL);
1420
- ctx.arcTo(left, top, left + eTL, top, eTL);
1421
- } else {
1422
- ctx.lineTo(left, y);
1423
- }
1424
- ctx.stroke();
1425
- }
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;
1426
2321
  } else {
1427
- if (tw > 0) {
1428
- ctx.strokeStyle = tc;
1429
- ctx.lineWidth = tw;
1430
- ctx.beginPath();
1431
- ctx.moveTo(x, y + tw / 2);
1432
- ctx.lineTo(x + width, y + tw / 2);
1433
- ctx.stroke();
1434
- }
1435
- if (rw > 0) {
1436
- ctx.strokeStyle = rc;
1437
- ctx.lineWidth = rw;
1438
- ctx.beginPath();
1439
- ctx.moveTo(x + width - rw / 2, y);
1440
- ctx.lineTo(x + width - rw / 2, y + height);
1441
- ctx.stroke();
1442
- }
1443
- if (bw > 0) {
1444
- ctx.strokeStyle = bc;
1445
- ctx.lineWidth = bw;
1446
- ctx.beginPath();
1447
- ctx.moveTo(x, y + height - bw / 2);
1448
- ctx.lineTo(x + width, y + height - bw / 2);
1449
- ctx.stroke();
1450
- }
1451
- if (lw > 0) {
1452
- ctx.strokeStyle = lc;
1453
- ctx.lineWidth = lw;
1454
- ctx.beginPath();
1455
- ctx.moveTo(x + lw / 2, y);
1456
- ctx.lineTo(x + lw / 2, y + height);
1457
- ctx.stroke();
1458
- }
2322
+ sw = imgW;
2323
+ sh = imgW / boxRatio;
2324
+ sx = 0;
2325
+ sy = (imgH - sh) / 2;
1459
2326
  }
2327
+ return { sx, sy, sw, sh, dx: boxX, dy: boxY, dw: boxW, dh: boxH };
1460
2328
  }
1461
- var BOX_SHADOW_RE = /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/;
1462
- function boxShadowExtent(boxShadow) {
1463
- const parts = boxShadow.match(BOX_SHADOW_RE);
1464
- if (!parts) return 0;
1465
- const offsetX = parseFloat(parts[1]);
1466
- const offsetY = parseFloat(parts[2]);
1467
- const blur = parseFloat(parts[3]);
1468
- 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 };
1469
2343
  }
1470
- function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1471
- const parts = boxShadow.match(BOX_SHADOW_RE);
1472
- if (!parts) return;
1473
- const offsetX = parseFloat(parts[1]);
1474
- const offsetY = parseFloat(parts[2]);
1475
- const blur = parseFloat(parts[3]);
1476
- const color = parts[5].trim();
1477
- const radii = [
1478
- borderRadius.topLeft,
1479
- borderRadius.topRight,
1480
- borderRadius.bottomRight,
1481
- borderRadius.bottomLeft
1482
- ];
1483
- const margin = blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
1484
- ctx.save();
1485
- ctx.beginPath();
1486
- ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
1487
- ctx.roundRect(x, y, width, height, radii);
1488
- ctx.clip("evenodd");
1489
- ctx.filter = `blur(${blur / 2}px)`;
1490
- ctx.translate(offsetX, offsetY);
1491
- ctx.fillStyle = color;
1492
- ctx.beginPath();
1493
- ctx.roundRect(x, y, width, height, radii);
1494
- ctx.fill();
1495
- 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
+ }
1496
2379
  }
1497
2380
 
1498
2381
  // src/jsx/draw/svg/style.ts
@@ -1646,7 +2529,7 @@ function applySvgTransform(ctx, transform) {
1646
2529
  }
1647
2530
 
1648
2531
  // src/jsx/draw/svg/path.ts
1649
- import { Path2D } from "@napi-rs/canvas";
2532
+ import { Path2D as Path2D2 } from "@napi-rs/canvas";
1650
2533
  function pathBBox(d) {
1651
2534
  let minX = Infinity;
1652
2535
  let minY = Infinity;
@@ -1807,14 +2690,14 @@ function buildPath(child, vbW, vbH) {
1807
2690
  case "path": {
1808
2691
  const d = props.d;
1809
2692
  if (!d) return void 0;
1810
- return new Path2D(d);
2693
+ return new Path2D2(d);
1811
2694
  }
1812
2695
  case "circle": {
1813
2696
  const cx = svgLength(props.cx, vbW);
1814
2697
  const cy = svgLength(props.cy, vbH);
1815
2698
  const r = svgLength(props.r, Math.min(vbW, vbH));
1816
2699
  if (r <= 0) return void 0;
1817
- const p = new Path2D();
2700
+ const p = new Path2D2();
1818
2701
  p.arc(cx, cy, r, 0, Math.PI * 2);
1819
2702
  return p;
1820
2703
  }
@@ -1830,7 +2713,7 @@ function buildPath(child, vbW, vbH) {
1830
2713
  let ry = ryRaw >= 0 ? ryRaw : rx;
1831
2714
  rx = Math.min(rx, w / 2);
1832
2715
  ry = Math.min(ry, h / 2);
1833
- const p = new Path2D();
2716
+ const p = new Path2D2();
1834
2717
  if (rx > 0 || ry > 0) {
1835
2718
  p.roundRect(x, y, w, h, [rx]);
1836
2719
  } else {
@@ -1844,14 +2727,14 @@ function buildPath(child, vbW, vbH) {
1844
2727
  const rx = svgLength(props.rx, vbW);
1845
2728
  const ry = svgLength(props.ry, vbH);
1846
2729
  if (rx <= 0 || ry <= 0) return void 0;
1847
- const p = new Path2D();
2730
+ const p = new Path2D2();
1848
2731
  p.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
1849
2732
  return p;
1850
2733
  }
1851
2734
  case "polygon": {
1852
2735
  const points = parsePoints(props.points);
1853
2736
  if (points.length < 2) return void 0;
1854
- const p = new Path2D();
2737
+ const p = new Path2D2();
1855
2738
  p.moveTo(points[0][0], points[0][1]);
1856
2739
  for (let i = 1; i < points.length; i++) {
1857
2740
  p.lineTo(points[i][0], points[i][1]);
@@ -2035,10 +2918,10 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
2035
2918
  }
2036
2919
 
2037
2920
  // src/jsx/draw/svg/shapes.ts
2038
- import { Path2D as Path2D3 } from "@napi-rs/canvas";
2921
+ import { Path2D as Path2D4 } from "@napi-rs/canvas";
2039
2922
 
2040
2923
  // src/jsx/draw/svg/gradients.ts
2041
- import { Path2D as Path2D2 } from "@napi-rs/canvas";
2924
+ import { Path2D as Path2D3 } from "@napi-rs/canvas";
2042
2925
  import parseCssColor from "parse-css-color";
2043
2926
  function parseFrac(value, fallback) {
2044
2927
  if (value == null) return fallback;
@@ -2126,7 +3009,7 @@ function fillWithSvgGradient(ctx, def, bbox, path, fillRule, viewport) {
2126
3009
  const gradient = ctx.createRadialGradient(fxF, fyF, 0, cxF, cyF, rF);
2127
3010
  addGradientStops(gradient, def);
2128
3011
  ctx.fillStyle = gradient;
2129
- const unitPath = new Path2D2();
3012
+ const unitPath = new Path2D3();
2130
3013
  const invTransform = {
2131
3014
  a: 1 / bbox.width,
2132
3015
  b: 0,
@@ -2248,7 +3131,7 @@ function applyStroke(ctx, props, path, inherited, color, defs, bbox) {
2248
3131
  function drawPath(ctx, props, inherited, color, defs) {
2249
3132
  const d = props.d;
2250
3133
  if (!d) return;
2251
- const path = new Path2D3(d);
3134
+ const path = new Path2D4(d);
2252
3135
  const bbox = pathBBox(d);
2253
3136
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
2254
3137
  }
@@ -2257,7 +3140,7 @@ function drawCircle(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2257
3140
  const cy = svgLength(props.cy, vbH);
2258
3141
  const r = svgLength(props.r, Math.min(vbW, vbH));
2259
3142
  if (r <= 0) return;
2260
- const path = new Path2D3();
3143
+ const path = new Path2D4();
2261
3144
  path.arc(cx, cy, r, 0, Math.PI * 2);
2262
3145
  const bbox = { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
2263
3146
  applyFillAndStroke(ctx, props, path, inherited, color, defs, bbox);
@@ -2274,7 +3157,7 @@ function drawSvgRect(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2274
3157
  let ry = ryRaw >= 0 ? ryRaw : rx;
2275
3158
  rx = Math.min(rx, w / 2);
2276
3159
  ry = Math.min(ry, h / 2);
2277
- const path = new Path2D3();
3160
+ const path = new Path2D4();
2278
3161
  if (rx > 0 || ry > 0) {
2279
3162
  path.roundRect(x, y, w, h, [rx]);
2280
3163
  } else {
@@ -2288,7 +3171,7 @@ function drawLine(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2288
3171
  const y1 = svgLength(props.y1, vbH);
2289
3172
  const x2 = svgLength(props.x2, vbW);
2290
3173
  const y2 = svgLength(props.y2, vbH);
2291
- const path = new Path2D3();
3174
+ const path = new Path2D4();
2292
3175
  path.moveTo(x1, y1);
2293
3176
  path.lineTo(x2, y2);
2294
3177
  const bbox = {
@@ -2305,7 +3188,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2305
3188
  const rx = svgLength(props.rx, vbW);
2306
3189
  const ry = svgLength(props.ry, vbH);
2307
3190
  if (rx <= 0 || ry <= 0) return;
2308
- const path = new Path2D3();
3191
+ const path = new Path2D4();
2309
3192
  path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
2310
3193
  const bbox = {
2311
3194
  x: cx - rx,
@@ -2318,7 +3201,7 @@ function drawEllipse(ctx, props, inherited, color, defs, vbW = 0, vbH = 0) {
2318
3201
  function drawPolygon(ctx, props, inherited, color, defs) {
2319
3202
  const points = parsePoints(props.points);
2320
3203
  if (points.length < 2) return;
2321
- const path = new Path2D3();
3204
+ const path = new Path2D4();
2322
3205
  path.moveTo(points[0][0], points[0][1]);
2323
3206
  for (let i = 1; i < points.length; i++) {
2324
3207
  path.lineTo(points[i][0], points[i][1]);
@@ -2330,7 +3213,7 @@ function drawPolygon(ctx, props, inherited, color, defs) {
2330
3213
  function drawPolyline(ctx, props, inherited, color, defs) {
2331
3214
  const points = parsePoints(props.points);
2332
3215
  if (points.length < 2) return;
2333
- const path = new Path2D3();
3216
+ const path = new Path2D4();
2334
3217
  path.moveTo(points[0][0], points[0][1]);
2335
3218
  for (let i = 1; i < points.length; i++) {
2336
3219
  path.lineTo(points[i][0], points[i][1]);
@@ -2833,11 +3716,12 @@ async function drawNode(ctx, node, parentX, parentY, context, emojiStyle) {
2833
3716
  if (opacity <= 0) return;
2834
3717
  const scaleInfo = style.transform ? extractScale(style.transform) : null;
2835
3718
  const hasOtherTransforms = scaleInfo !== null && scaleInfo.remaining.length > 0;
2836
- 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) {
2837
3721
  const sx = scaleInfo.sx;
2838
3722
  const sy = scaleInfo.sy;
2839
3723
  const transformWithoutScale = scaleInfo.remaining;
2840
- const bleed = Math.max(1, computeOverflowBleed(node));
3724
+ const bleed = Math.max(1, Math.ceil(subtree.overflowBleed));
2841
3725
  const qx = Math.max(1, Math.ceil(Math.abs(sx)));
2842
3726
  const qy = Math.max(1, Math.ceil(Math.abs(sy)));
2843
3727
  const bufW = Math.ceil((width + 2 * bleed) * qx);
@@ -2909,23 +3793,32 @@ function extractScale(transform) {
2909
3793
  const remaining = transform.replace(fullMatch, "").trim();
2910
3794
  return { sx, sy, remaining };
2911
3795
  }
2912
- function computeOverflowBleed(node) {
2913
- let bleed = 0;
3796
+ function scanSubtree(node) {
3797
+ let overflowBleed = 0;
3798
+ let hasBackdropFilter = false;
2914
3799
  const visit = (n) => {
2915
3800
  if (n.style.display === "none") return;
2916
3801
  if ((n.style.opacity ?? 1) <= 0) return;
2917
3802
  const fontSize = typeof n.style.fontSize === "number" ? n.style.fontSize : 0;
2918
3803
  if (n.textContent !== void 0 && n.textContent !== "" && fontSize > 0) {
2919
3804
  const letterSpacing = typeof n.style.letterSpacing === "number" ? n.style.letterSpacing : 0;
2920
- bleed = Math.max(bleed, fontSize + Math.max(0, -letterSpacing));
3805
+ overflowBleed = Math.max(
3806
+ overflowBleed,
3807
+ fontSize + Math.max(0, -letterSpacing)
3808
+ );
2921
3809
  }
2922
3810
  if (n.style.boxShadow) {
2923
- bleed = Math.max(bleed, boxShadowExtent(n.style.boxShadow));
3811
+ overflowBleed = Math.max(
3812
+ overflowBleed,
3813
+ boxShadowExtent(n.style.boxShadow)
3814
+ );
2924
3815
  }
3816
+ const filter = n.style.backdropFilter;
3817
+ if (filter && filter.trim() !== "none") hasBackdropFilter = true;
2925
3818
  for (const child of n.children) visit(child);
2926
3819
  };
2927
3820
  visit(node);
2928
- return bleed;
3821
+ return { overflowBleed, hasBackdropFilter };
2929
3822
  }
2930
3823
  async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emojiStyle, context, overrideTransform) {
2931
3824
  const x = parentX + node.x + offsetX;
@@ -2953,7 +3846,21 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
2953
3846
  style.transformOrigin
2954
3847
  );
2955
3848
  }
3849
+ if (style.clipPath) {
3850
+ applyClipPath(ctx, style.clipPath, style, x, y, width, height);
3851
+ }
2956
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
+ }
2957
3864
  if (style.boxShadow) {
2958
3865
  drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
2959
3866
  }
@@ -3379,6 +4286,15 @@ function expandStyle(raw, fontFamilies) {
3379
4286
  }
3380
4287
  delete style.WebkitTextStroke;
3381
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
+ }
3382
4298
  if (typeof style.fontFamily === "string") {
3383
4299
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
3384
4300
  if (fontFamilies) {
@@ -4104,7 +5020,7 @@ export {
4104
5020
  Image,
4105
5021
  ImageData,
4106
5022
  LottieAnimation,
4107
- Path2D4 as Path2D,
5023
+ Path2D5 as Path2D,
4108
5024
  PathOp,
4109
5025
  StrokeCap,
4110
5026
  StrokeJoin,