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