@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/README.md +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1314 -398
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
GlobalFonts as GlobalFonts2,
|
|
6
6
|
Image,
|
|
7
7
|
ImageData,
|
|
8
|
-
Path2D as
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" ?
|
|
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 =
|
|
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/
|
|
1239
|
-
function
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
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
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const
|
|
1255
|
-
|
|
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 (
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2921
|
+
import { Path2D as Path2D4 } from "@napi-rs/canvas";
|
|
2039
2922
|
|
|
2040
2923
|
// src/jsx/draw/svg/gradients.ts
|
|
2041
|
-
import { Path2D as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
2913
|
-
let
|
|
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
|
-
|
|
3805
|
+
overflowBleed = Math.max(
|
|
3806
|
+
overflowBleed,
|
|
3807
|
+
fontSize + Math.max(0, -letterSpacing)
|
|
3808
|
+
);
|
|
2921
3809
|
}
|
|
2922
3810
|
if (n.style.boxShadow) {
|
|
2923
|
-
|
|
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
|
|
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
|
-
|
|
5023
|
+
Path2D5 as Path2D,
|
|
4108
5024
|
PathOp,
|
|
4109
5025
|
StrokeCap,
|
|
4110
5026
|
StrokeJoin,
|