@pixerate/editor 0.40.0 → 0.42.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/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/animation/index.cjs +4 -2
- package/dist/animation/index.cjs.map +1 -1
- package/dist/animation/index.js +8 -6
- package/dist/canvas/index.cjs +26 -24
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +2 -2
- package/dist/canvas/index.d.ts +2 -2
- package/dist/canvas/index.js +8 -6
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-MR3Z4OQI.cjs +66 -0
- package/dist/chunk-MR3Z4OQI.cjs.map +1 -0
- package/dist/{chunk-XHRZSB77.cjs → chunk-O4ONBPZW.cjs} +5 -64
- package/dist/chunk-O4ONBPZW.cjs.map +1 -0
- package/dist/{chunk-W7BOBWZD.js → chunk-SHOVAHUB.js} +4 -63
- package/dist/chunk-SHOVAHUB.js.map +1 -0
- package/dist/chunk-ZRJMJIQU.js +66 -0
- package/dist/chunk-ZRJMJIQU.js.map +1 -0
- package/dist/{pathfinder-DKRcXiLS.d.cts → pathfinder-BSW2ZTO6.d.cts} +1 -1
- package/dist/{pathfinder-D98S2lV5.d.ts → pathfinder-D7hXenzu.d.ts} +1 -1
- package/dist/vector/index.cjs +3070 -389
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +865 -16
- package/dist/vector/index.d.ts +865 -16
- package/dist/vector/index.js +2837 -156
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-W7BOBWZD.js.map +0 -1
- package/dist/chunk-XHRZSB77.cjs.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
hatchPath,
|
|
19
19
|
isStraightCubic,
|
|
20
20
|
normalizeBezierPath,
|
|
21
|
+
offsetPath,
|
|
21
22
|
outlineStroke,
|
|
22
23
|
pathBoolean,
|
|
23
24
|
pathToContours,
|
|
@@ -39,10 +40,20 @@ import {
|
|
|
39
40
|
trimSegments,
|
|
40
41
|
windingNumber
|
|
41
42
|
} from "../chunk-3LHWTGHW.js";
|
|
43
|
+
import {
|
|
44
|
+
mixColorsOklab
|
|
45
|
+
} from "../chunk-ZRJMJIQU.js";
|
|
46
|
+
import {
|
|
47
|
+
evaluateEasing
|
|
48
|
+
} from "../chunk-UB2OY7FD.js";
|
|
49
|
+
import {
|
|
50
|
+
hexToRgba
|
|
51
|
+
} from "../chunk-TGD52FEZ.js";
|
|
42
52
|
|
|
43
53
|
// src/vector/types.ts
|
|
44
54
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
45
55
|
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
56
|
+
var VECTOR_BLEND_SPACINGS = ["steps", "distance", "smoothColor"];
|
|
46
57
|
var VECTOR_TOOLS = [
|
|
47
58
|
"select",
|
|
48
59
|
"direct",
|
|
@@ -58,10 +69,13 @@ var VECTOR_TOOLS = [
|
|
|
58
69
|
"eraser",
|
|
59
70
|
"width",
|
|
60
71
|
"shapeBuilder",
|
|
61
|
-
"paintBucket"
|
|
72
|
+
"paintBucket",
|
|
73
|
+
"blend"
|
|
62
74
|
];
|
|
63
75
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
64
76
|
var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
|
|
77
|
+
var VECTOR_EFFECT_TYPES = ["trim", "roundCorners", "puckerBloat", "twist", "zigzag", "wiggle", "offset", "merge", "repeater"];
|
|
78
|
+
var VECTOR_MERGE_MODES = ["merge", "add", "subtract", "intersect", "exclude"];
|
|
65
79
|
|
|
66
80
|
// src/vector/corners.ts
|
|
67
81
|
var EPS = 1e-9;
|
|
@@ -1031,8 +1045,872 @@ function vectorStrokeReach(style) {
|
|
|
1031
1045
|
return reach;
|
|
1032
1046
|
}
|
|
1033
1047
|
|
|
1034
|
-
// src/vector/
|
|
1048
|
+
// src/vector/pathEffects.ts
|
|
1035
1049
|
var EPS2 = 1e-9;
|
|
1050
|
+
var degenerate = (s) => s.every((p) => Math.abs(p[0] - s[0][0]) < EPS2 && Math.abs(p[1] - s[0][1]) < EPS2);
|
|
1051
|
+
function raise(seg) {
|
|
1052
|
+
if (seg.length !== 3) return seg.map((p) => [p[0], p[1]]);
|
|
1053
|
+
const [p0, p1, p2] = seg;
|
|
1054
|
+
return [p0, [p0[0] + 2 / 3 * (p1[0] - p0[0]), p0[1] + 2 / 3 * (p1[1] - p0[1])], [p2[0] + 2 / 3 * (p1[0] - p2[0]), p2[1] + 2 / 3 * (p1[1] - p2[1])], p2];
|
|
1055
|
+
}
|
|
1056
|
+
function subsOf(path) {
|
|
1057
|
+
return pathToContours(path).map((c) => ({ segs: closeContour(c).filter((s) => !degenerate(s)).map(raise), closed: c.closed })).filter((s) => s.segs.length > 0);
|
|
1058
|
+
}
|
|
1059
|
+
var subsToPath = (subs) => contoursToPath(subs.map((s) => ({ segments: s.segs, closed: s.closed })));
|
|
1060
|
+
var unit2 = (x, y) => {
|
|
1061
|
+
const l = Math.hypot(x, y);
|
|
1062
|
+
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1063
|
+
};
|
|
1064
|
+
function startTangent(s) {
|
|
1065
|
+
for (let i = 1; i < s.length; i++) {
|
|
1066
|
+
const u = unit2(s[i][0] - s[0][0], s[i][1] - s[0][1]);
|
|
1067
|
+
if (u) return u;
|
|
1068
|
+
}
|
|
1069
|
+
return [1, 0];
|
|
1070
|
+
}
|
|
1071
|
+
function endTangent(s) {
|
|
1072
|
+
const end = s[s.length - 1];
|
|
1073
|
+
for (let i = s.length - 2; i >= 0; i--) {
|
|
1074
|
+
const u = unit2(end[0] - s[i][0], end[1] - s[i][1]);
|
|
1075
|
+
if (u) return u;
|
|
1076
|
+
}
|
|
1077
|
+
return [1, 0];
|
|
1078
|
+
}
|
|
1079
|
+
function tangentAt(s, t) {
|
|
1080
|
+
if (t <= 1e-9) return startTangent(s);
|
|
1081
|
+
if (t >= 1 - 1e-9) return endTangent(s);
|
|
1082
|
+
const d = segmentDerivative(s, t);
|
|
1083
|
+
return unit2(d[0], d[1]) ?? startTangent(s);
|
|
1084
|
+
}
|
|
1085
|
+
var perp = (v2) => [-v2[1], v2[0]];
|
|
1086
|
+
function effectPathLength(path) {
|
|
1087
|
+
let total = 0;
|
|
1088
|
+
for (const s of subsOf(path)) for (const seg of s.segs) total += segmentLength(seg);
|
|
1089
|
+
return total;
|
|
1090
|
+
}
|
|
1091
|
+
function effectPathVertexCount(path) {
|
|
1092
|
+
return subsOf(path).reduce((n, s) => n + s.segs.length + (s.closed ? 0 : 1), 0);
|
|
1093
|
+
}
|
|
1094
|
+
var Measured = class {
|
|
1095
|
+
segs;
|
|
1096
|
+
lens;
|
|
1097
|
+
total;
|
|
1098
|
+
constructor(path) {
|
|
1099
|
+
this.segs = subsOf(path).flatMap((s) => s.segs);
|
|
1100
|
+
this.lens = this.segs.map((s) => segmentLength(s));
|
|
1101
|
+
this.total = this.lens.reduce((a, b) => a + b, 0);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Appends the part between lengths `sa` and `sb`, continuing the last piece when this one
|
|
1105
|
+
* starts where it ends (across the start of a closed path).
|
|
1106
|
+
*/
|
|
1107
|
+
append(sa, sb, out) {
|
|
1108
|
+
for (const piece of trimSegments(this.segs, sa, sb, this.lens)) {
|
|
1109
|
+
if (degenerate(piece)) continue;
|
|
1110
|
+
const last = out[out.length - 1]?.segs;
|
|
1111
|
+
const end = last?.[last.length - 1]?.at(-1);
|
|
1112
|
+
if (!end || Math.hypot(end[0] - piece[0][0], end[1] - piece[0][1]) > 1e-6) out.push({ segs: [piece], closed: false });
|
|
1113
|
+
else last.push(piece);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
function trimIntervals(start, end, offsetDegrees) {
|
|
1118
|
+
let s = start / 100;
|
|
1119
|
+
let e = end / 100;
|
|
1120
|
+
if (!Number.isFinite(s) || !Number.isFinite(e)) return null;
|
|
1121
|
+
if (s > e) [s, e] = [e, s];
|
|
1122
|
+
s = Math.min(1, Math.max(0, s));
|
|
1123
|
+
e = Math.min(1, Math.max(0, e));
|
|
1124
|
+
if (e - s >= 1 - 1e-9) return null;
|
|
1125
|
+
if (e - s <= 1e-9) return [];
|
|
1126
|
+
const off = Number.isFinite(offsetDegrees) ? (offsetDegrees / 360 % 1 + 1) % 1 : 0;
|
|
1127
|
+
s += off;
|
|
1128
|
+
e += off;
|
|
1129
|
+
if (e <= 1) return [[s, e]];
|
|
1130
|
+
if (s >= 1) return [[s - 1, e - 1]];
|
|
1131
|
+
return [
|
|
1132
|
+
[s, 1],
|
|
1133
|
+
[0, e - 1]
|
|
1134
|
+
];
|
|
1135
|
+
}
|
|
1136
|
+
function trimPaths(paths, start, end, offsetDegrees = 0) {
|
|
1137
|
+
const iv = trimIntervals(start, end, offsetDegrees);
|
|
1138
|
+
if (!iv) return paths.slice();
|
|
1139
|
+
return paths.map((p) => {
|
|
1140
|
+
const m = new Measured(p);
|
|
1141
|
+
const out = [];
|
|
1142
|
+
if (m.total > 0) for (const [a, b] of iv) m.append(a * m.total, b * m.total, out);
|
|
1143
|
+
return subsToPath(out);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function trimPathsIndividually(paths, start, end, offsetDegrees = 0) {
|
|
1147
|
+
const iv = trimIntervals(start, end, offsetDegrees);
|
|
1148
|
+
if (!iv) return paths.slice();
|
|
1149
|
+
const ms = paths.map((p) => new Measured(p));
|
|
1150
|
+
const total = ms.reduce((n, m) => n + m.total, 0);
|
|
1151
|
+
let base = 0;
|
|
1152
|
+
return ms.map((m) => {
|
|
1153
|
+
const out = [];
|
|
1154
|
+
for (const [a, b] of iv) {
|
|
1155
|
+
const la = Math.max(0, a * total - base);
|
|
1156
|
+
const lb = Math.min(m.total, b * total - base);
|
|
1157
|
+
if (lb > la) m.append(la, lb, out);
|
|
1158
|
+
}
|
|
1159
|
+
base += m.total;
|
|
1160
|
+
return subsToPath(out);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
function puckerBloatPaths(paths, amount) {
|
|
1164
|
+
const a = amount / 100;
|
|
1165
|
+
if (!a || !Number.isFinite(a)) return paths.slice();
|
|
1166
|
+
return paths.map((p) => {
|
|
1167
|
+
const verts = p.filter((c) => c.type !== "Z");
|
|
1168
|
+
if (!verts.length) return p;
|
|
1169
|
+
const cx = verts.reduce((s, c) => s + c.x, 0) / verts.length;
|
|
1170
|
+
const cy = verts.reduce((s, c) => s + c.y, 0) / verts.length;
|
|
1171
|
+
const mv = (x, y) => ({ x: x + (cx - x) * a, y: y + (cy - y) * a });
|
|
1172
|
+
const mvh = (x, y) => ({ x: x + (x - cx) * a, y: y + (y - cy) * a });
|
|
1173
|
+
const out = [];
|
|
1174
|
+
let prev = { x: 0, y: 0 };
|
|
1175
|
+
let start = { x: 0, y: 0 };
|
|
1176
|
+
const curve = (c1, c2, q) => {
|
|
1177
|
+
const h1 = mvh(c1.x, c1.y);
|
|
1178
|
+
const h2 = mvh(c2.x, c2.y);
|
|
1179
|
+
const e = mv(q.x, q.y);
|
|
1180
|
+
out.push({ type: "C", x1: h1.x, y1: h1.y, x2: h2.x, y2: h2.y, x: e.x, y: e.y });
|
|
1181
|
+
};
|
|
1182
|
+
for (const c of p) {
|
|
1183
|
+
switch (c.type) {
|
|
1184
|
+
case "M": {
|
|
1185
|
+
const e = mv(c.x, c.y);
|
|
1186
|
+
out.push({ type: "M", x: e.x, y: e.y });
|
|
1187
|
+
prev = start = { x: c.x, y: c.y };
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case "L":
|
|
1191
|
+
curve(prev, c, c);
|
|
1192
|
+
prev = { x: c.x, y: c.y };
|
|
1193
|
+
break;
|
|
1194
|
+
case "Q": {
|
|
1195
|
+
const c1 = { x: prev.x + 2 / 3 * (c.x1 - prev.x), y: prev.y + 2 / 3 * (c.y1 - prev.y) };
|
|
1196
|
+
const c2 = { x: c.x + 2 / 3 * (c.x1 - c.x), y: c.y + 2 / 3 * (c.y1 - c.y) };
|
|
1197
|
+
curve(c1, c2, c);
|
|
1198
|
+
prev = { x: c.x, y: c.y };
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
case "C":
|
|
1202
|
+
curve({ x: c.x1, y: c.y1 }, { x: c.x2, y: c.y2 }, c);
|
|
1203
|
+
prev = { x: c.x, y: c.y };
|
|
1204
|
+
break;
|
|
1205
|
+
case "Z":
|
|
1206
|
+
if (Math.hypot(prev.x - start.x, prev.y - start.y) > EPS2) curve(prev, start, start);
|
|
1207
|
+
out.push({ type: "Z" });
|
|
1208
|
+
prev = start;
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return out;
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
function roundCornerPaths(paths, radius) {
|
|
1216
|
+
if (!(radius > 0) || !Number.isFinite(radius)) return paths.slice();
|
|
1217
|
+
return paths.map((p) => subpathsToBezier(bezierToSubpaths(p).map((sp) => setSubpathCorners(sp, radius))));
|
|
1218
|
+
}
|
|
1219
|
+
function offsetPaths(paths, amount, join = "miter", miterLimit = 4, copies = 1, copyOffset = 0) {
|
|
1220
|
+
const n = Math.min(1e3, Math.max(1, Math.round(Number.isFinite(copies) ? copies : 1)));
|
|
1221
|
+
return paths.map((p) => {
|
|
1222
|
+
const out = [];
|
|
1223
|
+
for (let i = 0; i < n; i++) out.push(...offsetPath(p, amount * (i + 1 + copyOffset), { join, miterLimit }));
|
|
1224
|
+
return out;
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function twistPaths(paths, angle, center) {
|
|
1228
|
+
let rmax = 0;
|
|
1229
|
+
for (const p of paths) for (const c of p) if (c.type !== "Z") rmax = Math.max(rmax, Math.hypot(c.x - center.x, c.y - center.y));
|
|
1230
|
+
const a = angle * Math.PI / 180;
|
|
1231
|
+
if (!a || rmax <= 1e-9 || !Number.isFinite(a)) return paths.slice();
|
|
1232
|
+
const k = a / rmax;
|
|
1233
|
+
const rot = (v2, th) => {
|
|
1234
|
+
const s = Math.sin(th);
|
|
1235
|
+
const co = Math.cos(th);
|
|
1236
|
+
return [v2[0] * co - v2[1] * s, v2[0] * s + v2[1] * co];
|
|
1237
|
+
};
|
|
1238
|
+
const map = (p) => {
|
|
1239
|
+
const v2 = [p[0] - center.x, p[1] - center.y];
|
|
1240
|
+
const r = rot(v2, k * Math.hypot(v2[0], v2[1]));
|
|
1241
|
+
return [center.x + r[0], center.y + r[1]];
|
|
1242
|
+
};
|
|
1243
|
+
const jac = (p, dv) => {
|
|
1244
|
+
const v2 = [p[0] - center.x, p[1] - center.y];
|
|
1245
|
+
const r = Math.hypot(v2[0], v2[1]);
|
|
1246
|
+
let w = dv;
|
|
1247
|
+
if (r > 1e-12) {
|
|
1248
|
+
const f = k * (v2[0] * dv[0] + v2[1] * dv[1]) / r;
|
|
1249
|
+
w = [dv[0] - v2[1] * f, dv[1] + v2[0] * f];
|
|
1250
|
+
}
|
|
1251
|
+
return rot(w, k * r);
|
|
1252
|
+
};
|
|
1253
|
+
return paths.map((p) => {
|
|
1254
|
+
const out = [];
|
|
1255
|
+
for (const sub3 of subsOf(p)) {
|
|
1256
|
+
const segs = [];
|
|
1257
|
+
for (const seg of sub3.segs) {
|
|
1258
|
+
const cubic = seg.length === 2 ? [seg[0], seg[0], seg[1], seg[1]] : seg;
|
|
1259
|
+
const len2 = segmentLength(seg);
|
|
1260
|
+
const m = Math.min(64, Math.max(1, Math.ceil(Math.abs(a) * len2 / rmax / 0.15 + len2 / 50)));
|
|
1261
|
+
for (let i = 0; i < m; i++) {
|
|
1262
|
+
const q = subSegment(cubic, i / m, (i + 1) / m);
|
|
1263
|
+
const p0 = map(q[0]);
|
|
1264
|
+
const p3 = map(q[3]);
|
|
1265
|
+
const h1 = jac(q[0], [q[1][0] - q[0][0], q[1][1] - q[0][1]]);
|
|
1266
|
+
const h2 = jac(q[3], [q[2][0] - q[3][0], q[2][1] - q[3][1]]);
|
|
1267
|
+
segs.push([p0, [p0[0] + h1[0], p0[1] + h1[1]], [p3[0] + h2[0], p3[1] + h2[1]], p3]);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
out.push({ segs, closed: sub3.closed });
|
|
1271
|
+
}
|
|
1272
|
+
return subsToPath(out);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
function resample(sub3, m) {
|
|
1276
|
+
const segs = sub3.segs;
|
|
1277
|
+
const n = segs.length;
|
|
1278
|
+
const lens = segs.map((s) => segmentLength(s));
|
|
1279
|
+
const out = [];
|
|
1280
|
+
for (let j = 0; j < n; j++) {
|
|
1281
|
+
const piece = lens[j] / m;
|
|
1282
|
+
for (let k = 0; k < m; k++) {
|
|
1283
|
+
if (k === 0) {
|
|
1284
|
+
const tout = startTangent(segs[j]);
|
|
1285
|
+
const prev = sub3.closed || j > 0 ? (j + n - 1) % n : null;
|
|
1286
|
+
let tan = tout;
|
|
1287
|
+
if (prev !== null) {
|
|
1288
|
+
const tin = endTangent(segs[prev]);
|
|
1289
|
+
tan = unit2(tin[0] + tout[0], tin[1] + tout[1]) ?? tout;
|
|
1290
|
+
}
|
|
1291
|
+
out.push({ p: segs[j][0], tan, lenPrev: prev === null ? 0 : lens[prev] / m, lenNext: piece });
|
|
1292
|
+
} else {
|
|
1293
|
+
const t = segmentParamAtLength(segs[j], piece * k, lens[j]);
|
|
1294
|
+
out.push({ p: segmentPoint(segs[j], t), tan: tangentAt(segs[j], t), lenPrev: piece, lenNext: piece });
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (!sub3.closed) {
|
|
1299
|
+
const last = segs[n - 1];
|
|
1300
|
+
out.push({ p: last[last.length - 1], tan: endTangent(last), lenPrev: lens[n - 1] / m, lenNext: 0 });
|
|
1301
|
+
}
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
function emit(samples, disp, closed, smooth, handle) {
|
|
1305
|
+
const n = samples.length;
|
|
1306
|
+
if (!n) return null;
|
|
1307
|
+
const pt2 = (i) => [samples[i].p[0] + disp[i][0], samples[i].p[1] + disp[i][1]];
|
|
1308
|
+
const segs = [];
|
|
1309
|
+
const count = closed ? n : n - 1;
|
|
1310
|
+
for (let i = 0; i < count; i++) {
|
|
1311
|
+
const j = (i + 1) % n;
|
|
1312
|
+
const a = samples[i];
|
|
1313
|
+
const b = samples[j];
|
|
1314
|
+
const p = pt2(i);
|
|
1315
|
+
const q = pt2(j);
|
|
1316
|
+
if (smooth) segs.push([p, [p[0] + a.tan[0] * a.lenNext * handle, p[1] + a.tan[1] * a.lenNext * handle], [q[0] - b.tan[0] * b.lenPrev * handle, q[1] - b.tan[1] * b.lenPrev * handle], q]);
|
|
1317
|
+
else segs.push([p, q]);
|
|
1318
|
+
}
|
|
1319
|
+
if (!segs.length) return null;
|
|
1320
|
+
return { segs, closed };
|
|
1321
|
+
}
|
|
1322
|
+
function zigzagPaths(paths, size, ridges, smooth = false) {
|
|
1323
|
+
const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(ridges) ? ridges : 0))) + 1;
|
|
1324
|
+
return paths.map((p) => {
|
|
1325
|
+
const out = [];
|
|
1326
|
+
for (const sub3 of subsOf(p)) {
|
|
1327
|
+
const s = resample(sub3, m);
|
|
1328
|
+
const disp = s.map((x, i) => {
|
|
1329
|
+
const n = perp(x.tan);
|
|
1330
|
+
const d = i % 2 === 0 ? size : -size;
|
|
1331
|
+
return [n[0] * d, n[1] * d];
|
|
1332
|
+
});
|
|
1333
|
+
const e = emit(s, disp, sub3.closed, smooth, 0.36);
|
|
1334
|
+
if (e) out.push(e);
|
|
1335
|
+
}
|
|
1336
|
+
return subsToPath(out);
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
var MASK64 = (1n << 64n) - 1n;
|
|
1340
|
+
function splitmix(x) {
|
|
1341
|
+
x = x + 0x9e3779b97f4a7c15n & MASK64;
|
|
1342
|
+
let z = x;
|
|
1343
|
+
z = (z ^ z >> 30n) * 0xbf58476d1ce4e5b9n & MASK64;
|
|
1344
|
+
z = (z ^ z >> 27n) * 0x94d049bb133111ebn & MASK64;
|
|
1345
|
+
return z ^ z >> 31n;
|
|
1346
|
+
}
|
|
1347
|
+
function hash(seed, idx, t) {
|
|
1348
|
+
const h = splitmix(splitmix(splitmix(seed) ^ idx) ^ BigInt.asUintN(64, BigInt(t)));
|
|
1349
|
+
return Number(h >> 11n) / 2 ** 53 * 2 - 1;
|
|
1350
|
+
}
|
|
1351
|
+
function noise(seed, idx, t) {
|
|
1352
|
+
const i = Math.floor(t);
|
|
1353
|
+
const f = t - i;
|
|
1354
|
+
const p0 = hash(seed, idx, i - 1);
|
|
1355
|
+
const p1 = hash(seed, idx, i);
|
|
1356
|
+
const p2 = hash(seed, idx, i + 1);
|
|
1357
|
+
const p3 = hash(seed, idx, i + 2);
|
|
1358
|
+
const v2 = 0.5 * (2 * p1 + (-p0 + p2) * f + (2 * p0 - 5 * p1 + 4 * p2 - p3) * f * f + (-p0 + 3 * p1 - 3 * p2 + p3) * f * f * f);
|
|
1359
|
+
return Math.min(1, Math.max(-1, v2));
|
|
1360
|
+
}
|
|
1361
|
+
function wigglePaths(paths, w) {
|
|
1362
|
+
if (!w.size || !Number.isFinite(w.size)) return paths.slice();
|
|
1363
|
+
const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(w.detail) ? w.detail : 0))) + 1;
|
|
1364
|
+
const seed = BigInt.asUintN(64, BigInt(Math.round(Number.isFinite(w.seed) ? w.seed : 0)));
|
|
1365
|
+
const t = (Number.isFinite(w.time) ? w.time : 0) * (Number.isFinite(w.speed) ? w.speed : 0) + (Number.isFinite(w.phase) ? w.phase : 0) / 360;
|
|
1366
|
+
const corr = Math.min(1, Math.max(0, (Number.isFinite(w.correlation) ? w.correlation : 0) / 100));
|
|
1367
|
+
const sx = noise(seed, MASK64, t);
|
|
1368
|
+
const sy = noise(seed, MASK64 - 1n, t);
|
|
1369
|
+
let idx = 0;
|
|
1370
|
+
return paths.map((p) => {
|
|
1371
|
+
const out = [];
|
|
1372
|
+
for (const sub3 of subsOf(p)) {
|
|
1373
|
+
const s = resample(sub3, m);
|
|
1374
|
+
const disp = s.map(() => {
|
|
1375
|
+
const i = BigInt(idx);
|
|
1376
|
+
const ox = noise(seed, i * 2n, t + idx * 0.37);
|
|
1377
|
+
const oy = noise(seed, i * 2n + 1n, t + idx * 0.61);
|
|
1378
|
+
idx++;
|
|
1379
|
+
return [(sx * corr + ox * (1 - corr)) * w.size, (sy * corr + oy * (1 - corr)) * w.size];
|
|
1380
|
+
});
|
|
1381
|
+
const e = emit(s, disp, sub3.closed, w.smooth, 1 / 3);
|
|
1382
|
+
if (e) out.push(e);
|
|
1383
|
+
}
|
|
1384
|
+
return subsToPath(out);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
var MERGE_OPS = { add: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
|
|
1388
|
+
function mergePaths(paths, mode = "merge") {
|
|
1389
|
+
if (mode === "merge" || !(mode in MERGE_OPS)) return paths.flat();
|
|
1390
|
+
const inputs = paths.filter((p) => p.length > 0).map((path) => ({ path, fillRule: "nonzero" }));
|
|
1391
|
+
if (!inputs.length) return [];
|
|
1392
|
+
return pathBoolean(MERGE_OPS[mode], inputs).path;
|
|
1393
|
+
}
|
|
1394
|
+
function vectorRepeaterCopies(r, pivot) {
|
|
1395
|
+
const copies = Math.max(0, Number.isFinite(r.copies) ? Math.min(r.copies, 1e3) : 0);
|
|
1396
|
+
const n = Math.ceil(copies);
|
|
1397
|
+
const offset = Number.isFinite(r.offset) ? r.offset : 0;
|
|
1398
|
+
const so = (r.startOpacity ?? 100) / 100;
|
|
1399
|
+
const eo = (r.endOpacity ?? 100) / 100;
|
|
1400
|
+
const fract = copies - Math.floor(copies);
|
|
1401
|
+
const out = [];
|
|
1402
|
+
for (let i = 0; i < n; i++) {
|
|
1403
|
+
const k = i + offset;
|
|
1404
|
+
let m = translateMatrix(-pivot.x, -pivot.y);
|
|
1405
|
+
m = multiplyMatrices(m, scaleMatrix(Math.pow((r.scale?.x ?? 100) / 100, k), Math.pow((r.scale?.y ?? 100) / 100, k)));
|
|
1406
|
+
m = multiplyMatrices(m, rotateMatrix((r.rotation ?? 0) * k));
|
|
1407
|
+
m = multiplyMatrices(m, translateMatrix(pivot.x + (r.position?.x ?? 0) * k, pivot.y + (r.position?.y ?? 0) * k));
|
|
1408
|
+
const t = n > 1 ? i / (n - 1) : 0;
|
|
1409
|
+
const partial = i + 1 === n && fract > 0 ? fract : 1;
|
|
1410
|
+
out.push({ index: i, matrix: m, opacity: Math.min(1, Math.max(0, (so + (eo - so) * t) * partial)) });
|
|
1411
|
+
}
|
|
1412
|
+
return out;
|
|
1413
|
+
}
|
|
1414
|
+
function repeatPaths(paths, r, pivot) {
|
|
1415
|
+
return vectorRepeaterCopies(r, pivot).map((copy) => ({ copy, paths: paths.map((p) => transformBezierPath(p, copy.matrix)) }));
|
|
1416
|
+
}
|
|
1417
|
+
function pathsCenter(paths) {
|
|
1418
|
+
const b = bezierPathBounds(paths.flat());
|
|
1419
|
+
return b ? { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 } : { x: 0, y: 0 };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/vector/appearance.ts
|
|
1423
|
+
var VECTOR_EFFECT_DEFAULTS = {
|
|
1424
|
+
trim: { start: 0, end: 50, offset: 0, mode: "simultaneous" },
|
|
1425
|
+
roundCorners: { radius: 10 },
|
|
1426
|
+
puckerBloat: { amount: 30 },
|
|
1427
|
+
twist: { angle: 45, center: { x: 0, y: 0 } },
|
|
1428
|
+
zigzag: { size: 5, ridges: 4, smooth: false },
|
|
1429
|
+
wiggle: { size: 5, detail: 6, smooth: true, speed: 2, correlation: 50, phase: 0, seed: 1, time: 0 },
|
|
1430
|
+
offset: { amount: 5, join: "miter", miterLimit: 4, copies: 1, copyOffset: 0 },
|
|
1431
|
+
merge: { mode: "add" },
|
|
1432
|
+
repeater: {
|
|
1433
|
+
copies: 3,
|
|
1434
|
+
offset: 0,
|
|
1435
|
+
position: { x: 100, y: 0 },
|
|
1436
|
+
anchor: { x: 0, y: 0 },
|
|
1437
|
+
rotation: 0,
|
|
1438
|
+
scale: { x: 100, y: 100 },
|
|
1439
|
+
startOpacity: 100,
|
|
1440
|
+
endOpacity: 100,
|
|
1441
|
+
composite: "below"
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
var VECTOR_EFFECT_LABELS = {
|
|
1445
|
+
trim: "Trim Paths",
|
|
1446
|
+
roundCorners: "Round Corners",
|
|
1447
|
+
puckerBloat: "Pucker & Bloat",
|
|
1448
|
+
twist: "Twist",
|
|
1449
|
+
zigzag: "Zig Zag",
|
|
1450
|
+
wiggle: "Wiggle Paths",
|
|
1451
|
+
offset: "Offset Paths",
|
|
1452
|
+
merge: "Merge Paths",
|
|
1453
|
+
repeater: "Repeater"
|
|
1454
|
+
};
|
|
1455
|
+
var VECTOR_EFFECT_PARAM_SPECS = {
|
|
1456
|
+
trim: [
|
|
1457
|
+
{ key: "start", label: "Start %", kind: "number", min: 0, max: 100, step: 1, slider: true },
|
|
1458
|
+
{ key: "end", label: "End %", kind: "number", min: 0, max: 100, step: 1, slider: true },
|
|
1459
|
+
{ key: "offset", label: "Offset \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
|
|
1460
|
+
{
|
|
1461
|
+
key: "mode",
|
|
1462
|
+
label: "Paths",
|
|
1463
|
+
kind: "select",
|
|
1464
|
+
options: [
|
|
1465
|
+
{ value: "simultaneous", label: "Simultaneously" },
|
|
1466
|
+
{ value: "individual", label: "Individually" }
|
|
1467
|
+
]
|
|
1468
|
+
}
|
|
1469
|
+
],
|
|
1470
|
+
roundCorners: [{ key: "radius", label: "Radius", kind: "number", min: 0, max: 200, step: 1, slider: true }],
|
|
1471
|
+
puckerBloat: [{ key: "amount", label: "Amount %", kind: "number", min: -100, max: 100, step: 1, slider: true }],
|
|
1472
|
+
twist: [
|
|
1473
|
+
{ key: "angle", label: "Angle \xB0", kind: "number", min: -720, max: 720, step: 1, slider: true },
|
|
1474
|
+
{ key: "center", label: "Centre", kind: "vec2", step: 1 }
|
|
1475
|
+
],
|
|
1476
|
+
zigzag: [
|
|
1477
|
+
{ key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
|
|
1478
|
+
{ key: "ridges", label: "Ridges", kind: "number", min: 0, max: 50, step: 1, slider: true },
|
|
1479
|
+
{ key: "smooth", label: "Smooth", kind: "boolean" }
|
|
1480
|
+
],
|
|
1481
|
+
wiggle: [
|
|
1482
|
+
{ key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
|
|
1483
|
+
{ key: "detail", label: "Detail", kind: "number", min: 0, max: 50, step: 1, slider: true },
|
|
1484
|
+
{ key: "smooth", label: "Smooth", kind: "boolean" },
|
|
1485
|
+
{ key: "correlation", label: "Correlation %", kind: "number", min: 0, max: 100, step: 1, slider: true },
|
|
1486
|
+
{ key: "speed", label: "Speed", kind: "number", min: 0, step: 0.1 },
|
|
1487
|
+
{ key: "phase", label: "Phase \xB0", kind: "number", step: 1 },
|
|
1488
|
+
{ key: "time", label: "Time (s)", kind: "number", min: 0, max: 10, step: 0.05, slider: true },
|
|
1489
|
+
{ key: "seed", label: "Seed", kind: "number", step: 1 }
|
|
1490
|
+
],
|
|
1491
|
+
offset: [
|
|
1492
|
+
{ key: "amount", label: "Amount", kind: "number", min: -100, max: 100, step: 0.5, slider: true },
|
|
1493
|
+
{
|
|
1494
|
+
key: "join",
|
|
1495
|
+
label: "Join",
|
|
1496
|
+
kind: "select",
|
|
1497
|
+
options: [
|
|
1498
|
+
{ value: "miter", label: "Miter" },
|
|
1499
|
+
{ value: "round", label: "Round" },
|
|
1500
|
+
{ value: "bevel", label: "Bevel" }
|
|
1501
|
+
]
|
|
1502
|
+
},
|
|
1503
|
+
{ key: "miterLimit", label: "Miter limit", kind: "number", min: 1, step: 0.5 },
|
|
1504
|
+
{ key: "copies", label: "Copies", kind: "number", min: 1, max: 20, step: 1 },
|
|
1505
|
+
{ key: "copyOffset", label: "Copy offset", kind: "number", step: 0.5 }
|
|
1506
|
+
],
|
|
1507
|
+
merge: [
|
|
1508
|
+
{
|
|
1509
|
+
key: "mode",
|
|
1510
|
+
label: "Mode",
|
|
1511
|
+
kind: "select",
|
|
1512
|
+
options: [
|
|
1513
|
+
{ value: "merge", label: "Merge" },
|
|
1514
|
+
{ value: "add", label: "Add" },
|
|
1515
|
+
{ value: "subtract", label: "Subtract" },
|
|
1516
|
+
{ value: "intersect", label: "Intersect" },
|
|
1517
|
+
{ value: "exclude", label: "Exclude" }
|
|
1518
|
+
]
|
|
1519
|
+
}
|
|
1520
|
+
],
|
|
1521
|
+
repeater: [
|
|
1522
|
+
{ key: "copies", label: "Copies", kind: "number", min: 0, max: 50, step: 0.1, slider: true },
|
|
1523
|
+
{ key: "offset", label: "Offset", kind: "number", min: -10, max: 10, step: 0.1, slider: true },
|
|
1524
|
+
{ key: "position", label: "Position", kind: "vec2", step: 1 },
|
|
1525
|
+
{ key: "anchor", label: "Anchor", kind: "vec2", step: 1 },
|
|
1526
|
+
{ key: "rotation", label: "Rotation \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
|
|
1527
|
+
{ key: "scale", label: "Scale %", kind: "vec2", step: 1 },
|
|
1528
|
+
{ key: "startOpacity", label: "Start opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
|
|
1529
|
+
{ key: "endOpacity", label: "End opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
|
|
1530
|
+
{
|
|
1531
|
+
key: "composite",
|
|
1532
|
+
label: "Copies",
|
|
1533
|
+
kind: "select",
|
|
1534
|
+
options: [
|
|
1535
|
+
{ value: "below", label: "Below" },
|
|
1536
|
+
{ value: "above", label: "Above" }
|
|
1537
|
+
]
|
|
1538
|
+
}
|
|
1539
|
+
]
|
|
1540
|
+
};
|
|
1541
|
+
var isVec2 = (v2) => !!v2 && typeof v2 === "object" && Number.isFinite(v2.x) && Number.isFinite(v2.y);
|
|
1542
|
+
function normalizeVectorEffectParams(type, params, base) {
|
|
1543
|
+
const defaults = VECTOR_EFFECT_DEFAULTS[type];
|
|
1544
|
+
if (!defaults) return null;
|
|
1545
|
+
const out = { ...defaults, ...base ?? {} };
|
|
1546
|
+
for (const spec of VECTOR_EFFECT_PARAM_SPECS[type]) {
|
|
1547
|
+
const v2 = params?.[spec.key];
|
|
1548
|
+
if (v2 === void 0) continue;
|
|
1549
|
+
if (spec.kind === "number" && typeof v2 === "number" && Number.isFinite(v2)) out[spec.key] = Math.min(spec.max ?? Infinity, Math.max(spec.min ?? -Infinity, v2));
|
|
1550
|
+
else if (spec.kind === "boolean" && typeof v2 === "boolean") out[spec.key] = v2;
|
|
1551
|
+
else if (spec.kind === "vec2" && isVec2(v2)) out[spec.key] = { x: v2.x, y: v2.y };
|
|
1552
|
+
else if (spec.kind === "select" && spec.options.some((o) => o.value === v2)) out[spec.key] = v2;
|
|
1553
|
+
}
|
|
1554
|
+
return out;
|
|
1555
|
+
}
|
|
1556
|
+
function createVectorEffect(type, id, params) {
|
|
1557
|
+
const p = normalizeVectorEffectParams(type, params);
|
|
1558
|
+
return p ? { id, type, params: p } : null;
|
|
1559
|
+
}
|
|
1560
|
+
var isEnabled = (e) => e.enabled !== false && !!VECTOR_EFFECT_DEFAULTS[e.type];
|
|
1561
|
+
function activeVectorEffects(node) {
|
|
1562
|
+
return node.effects?.filter(isEnabled) ?? [];
|
|
1563
|
+
}
|
|
1564
|
+
function hasOwnVectorAppearance(node) {
|
|
1565
|
+
return node.type === "group" && !!node.liveBoolean || !!node.effects?.some(isEnabled);
|
|
1566
|
+
}
|
|
1567
|
+
var liveCache = /* @__PURE__ */ new WeakMap();
|
|
1568
|
+
function hasVectorAppearance(node) {
|
|
1569
|
+
let v2 = liveCache.get(node);
|
|
1570
|
+
if (v2 === void 0) {
|
|
1571
|
+
v2 = hasOwnVectorAppearance(node) || node.type === "group" && node.children.some(hasVectorAppearance);
|
|
1572
|
+
liveCache.set(node, v2);
|
|
1573
|
+
}
|
|
1574
|
+
return v2;
|
|
1575
|
+
}
|
|
1576
|
+
function isVectorLiveBoolean(node) {
|
|
1577
|
+
return node?.type === "group" && !!node.liveBoolean;
|
|
1578
|
+
}
|
|
1579
|
+
function vectorOperandOperation(group, child) {
|
|
1580
|
+
return child.booleanOperation ?? group.liveBoolean ?? "union";
|
|
1581
|
+
}
|
|
1582
|
+
var PATH_OPS = { union: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
|
|
1583
|
+
var FoldCache = class {
|
|
1584
|
+
constructor(cap) {
|
|
1585
|
+
this.cap = cap;
|
|
1586
|
+
}
|
|
1587
|
+
cap;
|
|
1588
|
+
map = /* @__PURE__ */ new Map();
|
|
1589
|
+
get(key) {
|
|
1590
|
+
const hit = this.map.get(key);
|
|
1591
|
+
if (hit) {
|
|
1592
|
+
this.map.delete(key);
|
|
1593
|
+
this.map.set(key, hit);
|
|
1594
|
+
}
|
|
1595
|
+
return hit;
|
|
1596
|
+
}
|
|
1597
|
+
set(key, path) {
|
|
1598
|
+
if (this.map.size >= this.cap) {
|
|
1599
|
+
let drop = Math.floor(this.map.size / 2);
|
|
1600
|
+
for (const k of this.map.keys()) {
|
|
1601
|
+
if (drop-- <= 0) break;
|
|
1602
|
+
this.map.delete(k);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
this.map.set(key, path);
|
|
1606
|
+
}
|
|
1607
|
+
clear() {
|
|
1608
|
+
this.map.clear();
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
var foldCache = new FoldCache(4096);
|
|
1612
|
+
var foldSteps = 0;
|
|
1613
|
+
function vectorLiveBooleanSteps() {
|
|
1614
|
+
return foldSteps;
|
|
1615
|
+
}
|
|
1616
|
+
function clearVectorLiveBooleanCache() {
|
|
1617
|
+
foldCache.clear();
|
|
1618
|
+
}
|
|
1619
|
+
var RollingHash = class _RollingHash {
|
|
1620
|
+
a = 2166136261;
|
|
1621
|
+
b = 16777619 ^ 1540483477;
|
|
1622
|
+
static view = new DataView(new ArrayBuffer(8));
|
|
1623
|
+
byte(v2) {
|
|
1624
|
+
this.a = Math.imul(this.a ^ v2 & 255, 16777619);
|
|
1625
|
+
this.b = Math.imul(this.b ^ v2 & 255, 1540483477) ^ this.b >>> 15;
|
|
1626
|
+
}
|
|
1627
|
+
num(n) {
|
|
1628
|
+
_RollingHash.view.setFloat64(0, n);
|
|
1629
|
+
for (let i = 0; i < 8; i++) this.byte(_RollingHash.view.getUint8(i));
|
|
1630
|
+
}
|
|
1631
|
+
path(p) {
|
|
1632
|
+
for (const c of p) {
|
|
1633
|
+
this.byte(c.type.charCodeAt(0));
|
|
1634
|
+
if (c.type === "Z") continue;
|
|
1635
|
+
if (c.type === "C") {
|
|
1636
|
+
this.num(c.x1);
|
|
1637
|
+
this.num(c.y1);
|
|
1638
|
+
this.num(c.x2);
|
|
1639
|
+
this.num(c.y2);
|
|
1640
|
+
} else if (c.type === "Q") {
|
|
1641
|
+
this.num(c.x1);
|
|
1642
|
+
this.num(c.y1);
|
|
1643
|
+
}
|
|
1644
|
+
this.num(c.x);
|
|
1645
|
+
this.num(c.y);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
key() {
|
|
1649
|
+
return `${(this.a >>> 0).toString(36)}.${(this.b >>> 0).toString(36)}`;
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
function foldVectorBooleanOperands(operands) {
|
|
1653
|
+
if (!operands.length) return [];
|
|
1654
|
+
if (operands.length === 1) return operands[0].path;
|
|
1655
|
+
const h = new RollingHash();
|
|
1656
|
+
const keys = operands.map((o, i) => {
|
|
1657
|
+
if (i > 0) h.byte(o.operation.charCodeAt(0) + o.operation.length);
|
|
1658
|
+
h.byte(o.fillRule === "evenodd" ? 1 : 0);
|
|
1659
|
+
h.path(o.path);
|
|
1660
|
+
h.byte(9);
|
|
1661
|
+
return h.key();
|
|
1662
|
+
});
|
|
1663
|
+
let start = 0;
|
|
1664
|
+
let acc = operands[0].path;
|
|
1665
|
+
for (let i = keys.length - 1; i >= 1; i--) {
|
|
1666
|
+
const hit = foldCache.get(keys[i]);
|
|
1667
|
+
if (hit) {
|
|
1668
|
+
if (i === keys.length - 1) return hit;
|
|
1669
|
+
start = i;
|
|
1670
|
+
acc = hit;
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
let accRule = start === 0 ? operands[0].fillRule : "nonzero";
|
|
1675
|
+
for (let i = start + 1; i < operands.length; i++) {
|
|
1676
|
+
const o = operands[i];
|
|
1677
|
+
foldSteps++;
|
|
1678
|
+
const r = pathBoolean(PATH_OPS[o.operation] ?? "union", [
|
|
1679
|
+
{ path: acc, fillRule: accRule },
|
|
1680
|
+
{ path: o.path, fillRule: o.fillRule }
|
|
1681
|
+
]);
|
|
1682
|
+
if (r.error) return [];
|
|
1683
|
+
acc = r.path;
|
|
1684
|
+
accRule = "nonzero";
|
|
1685
|
+
foldCache.set(keys[i], acc);
|
|
1686
|
+
}
|
|
1687
|
+
return acc;
|
|
1688
|
+
}
|
|
1689
|
+
var compareOrder = (a, b) => {
|
|
1690
|
+
const n = Math.min(a.length, b.length);
|
|
1691
|
+
for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
1692
|
+
return a.length - b.length;
|
|
1693
|
+
};
|
|
1694
|
+
var add2 = (a, b) => ({ x: a.x + (b?.x ?? 0), y: a.y + (b?.y ?? 0) });
|
|
1695
|
+
var mapPaths = (shapes, fn) => {
|
|
1696
|
+
const out = fn(shapes.map((s) => s.path));
|
|
1697
|
+
return shapes.map((s, i) => ({ ...s, path: out[i] ?? [] }));
|
|
1698
|
+
};
|
|
1699
|
+
function applyVectorEffects(shapes, effects) {
|
|
1700
|
+
let list = shapes.slice();
|
|
1701
|
+
for (const e of effects ?? []) {
|
|
1702
|
+
if (!isEnabled(e) || !list.length) continue;
|
|
1703
|
+
switch (e.type) {
|
|
1704
|
+
case "trim": {
|
|
1705
|
+
const { start, end, offset, mode } = e.params;
|
|
1706
|
+
list = mapPaths(list, (p) => mode === "individual" ? trimPathsIndividually(p, start, end, offset) : trimPaths(p, start, end, offset));
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
case "roundCorners":
|
|
1710
|
+
list = mapPaths(list, (p) => roundCornerPaths(p, e.params.radius));
|
|
1711
|
+
break;
|
|
1712
|
+
case "puckerBloat":
|
|
1713
|
+
list = mapPaths(list, (p) => puckerBloatPaths(p, e.params.amount));
|
|
1714
|
+
break;
|
|
1715
|
+
case "twist":
|
|
1716
|
+
list = mapPaths(list, (p) => twistPaths(p, e.params.angle, add2(pathsCenter(p), e.params.center)));
|
|
1717
|
+
break;
|
|
1718
|
+
case "zigzag":
|
|
1719
|
+
list = mapPaths(list, (p) => zigzagPaths(p, e.params.size, e.params.ridges, e.params.smooth));
|
|
1720
|
+
break;
|
|
1721
|
+
case "wiggle":
|
|
1722
|
+
list = mapPaths(list, (p) => wigglePaths(p, e.params));
|
|
1723
|
+
break;
|
|
1724
|
+
case "offset": {
|
|
1725
|
+
const o = e.params;
|
|
1726
|
+
list = mapPaths(list, (p) => offsetPaths(p, o.amount, o.join, o.miterLimit, o.copies, o.copyOffset));
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
case "merge": {
|
|
1730
|
+
const first = list[0];
|
|
1731
|
+
list = [{ ...first, path: mergePaths(list.map((s) => s.path), e.params.mode) }];
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
case "repeater": {
|
|
1735
|
+
const r = e.params;
|
|
1736
|
+
const copies = repeatPaths(
|
|
1737
|
+
list.map((s) => s.path),
|
|
1738
|
+
r,
|
|
1739
|
+
add2(pathsCenter(list.map((s) => s.path)), r.anchor)
|
|
1740
|
+
);
|
|
1741
|
+
const n = copies.length;
|
|
1742
|
+
const next = [];
|
|
1743
|
+
for (const { copy, paths } of copies) {
|
|
1744
|
+
const rank = r.composite === "above" ? copy.index : n - 1 - copy.index;
|
|
1745
|
+
list.forEach((s, i) => next.push({ source: s.source, path: paths[i], opacity: s.opacity * copy.opacity, order: [rank, ...s.order] }));
|
|
1746
|
+
}
|
|
1747
|
+
list = next;
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return list;
|
|
1753
|
+
}
|
|
1754
|
+
function childShapes(child, index) {
|
|
1755
|
+
if (child.visible === false) return [];
|
|
1756
|
+
const m = child.transform;
|
|
1757
|
+
const k = child.opacity ?? 1;
|
|
1758
|
+
return vectorAppearanceShapes(child).map((s) => ({
|
|
1759
|
+
source: s.source,
|
|
1760
|
+
path: m && !isIdentityMatrix(m) ? transformBezierPath(s.path, m) : s.path,
|
|
1761
|
+
opacity: s.opacity * k,
|
|
1762
|
+
order: [index, ...s.order]
|
|
1763
|
+
}));
|
|
1764
|
+
}
|
|
1765
|
+
function liveBooleanShapes(group) {
|
|
1766
|
+
const operands = [];
|
|
1767
|
+
let source = null;
|
|
1768
|
+
group.children.forEach((child, i) => {
|
|
1769
|
+
const shapes = childShapes(child, i);
|
|
1770
|
+
if (!shapes.length) return;
|
|
1771
|
+
source ??= shapes[0].source;
|
|
1772
|
+
const path = shapes.length === 1 ? shapes[0].path : pathBoolean("union", shapes.map((s) => ({ path: s.path, fillRule: s.source.fillRule ?? "nonzero" }))).path;
|
|
1773
|
+
const fillRule = shapes.length === 1 ? shapes[0].source.fillRule ?? "nonzero" : "nonzero";
|
|
1774
|
+
operands.push({ path, fillRule, operation: vectorOperandOperation(group, child) });
|
|
1775
|
+
});
|
|
1776
|
+
if (!source) return [];
|
|
1777
|
+
return [{ source, path: foldVectorBooleanOperands(operands), opacity: 1, order: [0] }];
|
|
1778
|
+
}
|
|
1779
|
+
var shapeCache = /* @__PURE__ */ new WeakMap();
|
|
1780
|
+
function vectorAppearanceShapes(node) {
|
|
1781
|
+
const hit = shapeCache.get(node);
|
|
1782
|
+
if (hit) return hit;
|
|
1783
|
+
let base;
|
|
1784
|
+
if (node.type === "path") base = [{ source: node, path: subpathsToBezier(node.subpaths), opacity: 1, order: [] }];
|
|
1785
|
+
else if (node.liveBoolean) base = liveBooleanShapes(node);
|
|
1786
|
+
else base = node.children.flatMap((c, i) => childShapes(c, i));
|
|
1787
|
+
const out = applyVectorEffects(base, node.effects).sort((a, b) => compareOrder(a.order, b.order));
|
|
1788
|
+
shapeCache.set(node, out);
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
var outlineCache = /* @__PURE__ */ new WeakMap();
|
|
1792
|
+
function vectorAppearanceOutline(node) {
|
|
1793
|
+
let hit = outlineCache.get(node);
|
|
1794
|
+
if (!hit) {
|
|
1795
|
+
hit = node.type === "path" && !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceShapes(node).flatMap((s) => s.path);
|
|
1796
|
+
outlineCache.set(node, hit);
|
|
1797
|
+
}
|
|
1798
|
+
return hit;
|
|
1799
|
+
}
|
|
1800
|
+
function shapeNode(shape, id) {
|
|
1801
|
+
const {
|
|
1802
|
+
id: _id,
|
|
1803
|
+
type: _type,
|
|
1804
|
+
subpaths: _s,
|
|
1805
|
+
transform: _t,
|
|
1806
|
+
effects: _e,
|
|
1807
|
+
booleanOperation: _b,
|
|
1808
|
+
blendStep: _bs,
|
|
1809
|
+
locked: _l,
|
|
1810
|
+
visible: _v,
|
|
1811
|
+
opacity: _o,
|
|
1812
|
+
...style
|
|
1813
|
+
} = shape.source;
|
|
1814
|
+
return { ...style, id, type: "path", subpaths: bezierToSubpaths(shape.path), ...shape.opacity < 1 ? { opacity: shape.opacity } : {} };
|
|
1815
|
+
}
|
|
1816
|
+
function placement(node) {
|
|
1817
|
+
return {
|
|
1818
|
+
...node.name !== void 0 ? { name: node.name } : {},
|
|
1819
|
+
...node.transform ? { transform: node.transform } : {},
|
|
1820
|
+
...node.visible === false ? { visible: false } : {},
|
|
1821
|
+
...node.locked ? { locked: true } : {},
|
|
1822
|
+
...node.blendStep ? { blendStep: true } : {},
|
|
1823
|
+
...node.booleanOperation ? { booleanOperation: node.booleanOperation } : {}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
function vectorAppearanceNodes(node) {
|
|
1827
|
+
return vectorAppearanceShapes(node).map((s, k) => shapeNode(s, `${node.id}~fx${k}`));
|
|
1828
|
+
}
|
|
1829
|
+
var expandCache = /* @__PURE__ */ new WeakMap();
|
|
1830
|
+
function expandVectorAppearance(node) {
|
|
1831
|
+
if (!hasVectorAppearance(node)) return node;
|
|
1832
|
+
const hit = expandCache.get(node);
|
|
1833
|
+
if (hit) return hit;
|
|
1834
|
+
let out;
|
|
1835
|
+
if (!hasOwnVectorAppearance(node)) {
|
|
1836
|
+
const group = node;
|
|
1837
|
+
out = { ...group, children: group.children.map(expandVectorAppearance) };
|
|
1838
|
+
} else {
|
|
1839
|
+
const shapes = vectorAppearanceShapes(node);
|
|
1840
|
+
if (node.type === "path" && shapes.length === 1) {
|
|
1841
|
+
const one = shapes[0];
|
|
1842
|
+
const { effects: _e, opacity: _o, ...rest } = node;
|
|
1843
|
+
const opacity = (node.opacity ?? 1) * one.opacity;
|
|
1844
|
+
out = { ...rest, subpaths: bezierToSubpaths(one.path), ...opacity < 1 ? { opacity } : {} };
|
|
1845
|
+
} else {
|
|
1846
|
+
out = {
|
|
1847
|
+
id: node.id,
|
|
1848
|
+
type: "group",
|
|
1849
|
+
...placement(node),
|
|
1850
|
+
...node.opacity !== void 0 ? { opacity: node.opacity } : {},
|
|
1851
|
+
children: vectorAppearanceNodes(node)
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
expandCache.set(node, out);
|
|
1856
|
+
return out;
|
|
1857
|
+
}
|
|
1858
|
+
function expandVectorAppearances(nodes) {
|
|
1859
|
+
let changed = false;
|
|
1860
|
+
const out = nodes.map((n) => {
|
|
1861
|
+
const e = expandVectorAppearance(n);
|
|
1862
|
+
if (e !== n) changed = true;
|
|
1863
|
+
return e;
|
|
1864
|
+
});
|
|
1865
|
+
return changed ? out : nodes;
|
|
1866
|
+
}
|
|
1867
|
+
function sanitizeVectorEffects(value) {
|
|
1868
|
+
if (!Array.isArray(value)) return [];
|
|
1869
|
+
const out = [];
|
|
1870
|
+
const used = /* @__PURE__ */ new Set();
|
|
1871
|
+
for (const raw of value) {
|
|
1872
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1873
|
+
const r = raw;
|
|
1874
|
+
const type = r.type;
|
|
1875
|
+
if (typeof type !== "string" || !VECTOR_EFFECT_DEFAULTS[type]) continue;
|
|
1876
|
+
let id = typeof r.id === "string" && r.id && !used.has(r.id) ? r.id : "";
|
|
1877
|
+
for (let k = out.length + 1; !id; k++) if (!used.has(`fx${k}`)) id = `fx${k}`;
|
|
1878
|
+
used.add(id);
|
|
1879
|
+
const effect = createVectorEffect(type, id, r.params ?? {});
|
|
1880
|
+
out.push(r.enabled === false ? { ...effect, enabled: false } : effect);
|
|
1881
|
+
}
|
|
1882
|
+
return out;
|
|
1883
|
+
}
|
|
1884
|
+
function transformVectorEffects(effects, m) {
|
|
1885
|
+
const lin = (v2) => ({ x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y });
|
|
1886
|
+
const mirror = m[0] * m[3] - m[1] * m[2] < 0 ? -1 : 1;
|
|
1887
|
+
return effects.map((e) => {
|
|
1888
|
+
if (e.type === "twist") return { ...e, params: { ...e.params, center: lin(e.params.center), angle: e.params.angle * mirror } };
|
|
1889
|
+
if (e.type === "repeater")
|
|
1890
|
+
return { ...e, params: { ...e.params, position: lin(e.params.position), anchor: lin(e.params.anchor), rotation: e.params.rotation * mirror } };
|
|
1891
|
+
return e;
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function vectorEnteredGroups(nodes, ids) {
|
|
1895
|
+
const out = /* @__PURE__ */ new Set();
|
|
1896
|
+
if (!ids.length) return out;
|
|
1897
|
+
const want = new Set(ids);
|
|
1898
|
+
const visit = (list, live) => {
|
|
1899
|
+
for (const n of list) {
|
|
1900
|
+
const own = n.type === "group" && hasOwnVectorAppearance(n);
|
|
1901
|
+
if (want.has(n.id)) {
|
|
1902
|
+
for (const id of live) out.add(id);
|
|
1903
|
+
if (own) out.add(n.id);
|
|
1904
|
+
}
|
|
1905
|
+
if (n.type === "group") visit(n.children, own ? [...live, n.id] : live);
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
visit(nodes, []);
|
|
1909
|
+
return out;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/vector/geometry.ts
|
|
1913
|
+
var EPS3 = 1e-9;
|
|
1036
1914
|
var cloneAnchor = (a) => ({
|
|
1037
1915
|
x: a.x,
|
|
1038
1916
|
y: a.y,
|
|
@@ -1057,7 +1935,7 @@ function hasPaint(paint) {
|
|
|
1057
1935
|
return true;
|
|
1058
1936
|
}
|
|
1059
1937
|
var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
|
|
1060
|
-
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) <
|
|
1938
|
+
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
|
|
1061
1939
|
function subpathSegmentCount(sp) {
|
|
1062
1940
|
const n = sp.anchors.length;
|
|
1063
1941
|
if (n < 2) return 0;
|
|
@@ -1089,7 +1967,7 @@ function subpathsToBezier(subpaths, options = {}) {
|
|
|
1089
1967
|
return out;
|
|
1090
1968
|
}
|
|
1091
1969
|
function vectorNodeToBezier(node) {
|
|
1092
|
-
return node.type === "path" ?
|
|
1970
|
+
return node.type === "path" ? vectorAppearanceOutline(node) : [];
|
|
1093
1971
|
}
|
|
1094
1972
|
function vectorPathData(input, precision = 3) {
|
|
1095
1973
|
const subpaths = Array.isArray(input) ? input : input.subpaths;
|
|
@@ -1203,12 +2081,13 @@ function collectPathNodes(node) {
|
|
|
1203
2081
|
function nodeMatrix(node) {
|
|
1204
2082
|
return node.transform ?? IDENTITY_MATRIX;
|
|
1205
2083
|
}
|
|
1206
|
-
function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
|
|
2084
|
+
function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX, options = {}) {
|
|
1207
2085
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
1208
|
-
const path = subpathsToBezier(node.subpaths);
|
|
2086
|
+
const path = options.effects === false || !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceOutline(node);
|
|
1209
2087
|
return isIdentityMatrix(m) ? path : transformBezierPath(path, m);
|
|
1210
2088
|
}
|
|
1211
2089
|
function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
|
|
2090
|
+
node = expandVectorAppearance(node);
|
|
1212
2091
|
if (node.type === "group") {
|
|
1213
2092
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
1214
2093
|
return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
|
|
@@ -1238,11 +2117,12 @@ function boundsIntersect(a, b) {
|
|
|
1238
2117
|
function boundsFromPoints(a, b) {
|
|
1239
2118
|
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
|
|
1240
2119
|
}
|
|
1241
|
-
function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
2120
|
+
function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX, options = {}) {
|
|
1242
2121
|
if (node.visible === false) return false;
|
|
2122
|
+
if (!(node.type === "group" && (!hasOwnVectorAppearance(node) || options.entered?.has(node.id)))) node = expandVectorAppearance(node);
|
|
1243
2123
|
if (node.type === "group") {
|
|
1244
2124
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
1245
|
-
return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m));
|
|
2125
|
+
return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m, options));
|
|
1246
2126
|
}
|
|
1247
2127
|
const path = vectorPathWorldBezier(node, parent);
|
|
1248
2128
|
if (path.length === 0) return false;
|
|
@@ -1255,11 +2135,11 @@ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
|
|
|
1255
2135
|
if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
|
|
1256
2136
|
return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
|
|
1257
2137
|
}
|
|
1258
|
-
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
2138
|
+
function hitTestVectorNodes(nodes, p, tolerance = 0, options = {}) {
|
|
1259
2139
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
1260
2140
|
const node = nodes[i];
|
|
1261
2141
|
if (node.visible === false || node.locked) continue;
|
|
1262
|
-
if (hitTestVectorNode(node, p, tolerance)) return node;
|
|
2142
|
+
if (hitTestVectorNode(node, p, tolerance, IDENTITY_MATRIX, options)) return node;
|
|
1263
2143
|
}
|
|
1264
2144
|
return null;
|
|
1265
2145
|
}
|
|
@@ -1327,9 +2207,26 @@ function decomposeRigid(m) {
|
|
|
1327
2207
|
residual: [cos * a + sin * b, -sin * a + cos * b, cos * c + sin * d, -sin * c + cos * d, 0, 0]
|
|
1328
2208
|
};
|
|
1329
2209
|
}
|
|
2210
|
+
function mapAnchor(a, m) {
|
|
2211
|
+
const p = applyMatrix(m, a);
|
|
2212
|
+
const next = { x: p.x, y: p.y };
|
|
2213
|
+
if (a.in) next.in = applyMatrix(m, a.in);
|
|
2214
|
+
if (a.out) next.out = applyMatrix(m, a.out);
|
|
2215
|
+
if (a.type) next.type = a.type;
|
|
2216
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
2217
|
+
return next;
|
|
2218
|
+
}
|
|
1330
2219
|
function bakeIntoGeometry(node, m) {
|
|
1331
2220
|
if (isIdentityMatrix(m)) return node;
|
|
1332
|
-
if (node.
|
|
2221
|
+
if (node.effects?.length) node = { ...node, effects: transformVectorEffects(node.effects, m) };
|
|
2222
|
+
if (node.type === "group") {
|
|
2223
|
+
const group = node;
|
|
2224
|
+
const children = group.children.map((c) => transformVectorNode(c, m));
|
|
2225
|
+
const spine = group.blend?.spine;
|
|
2226
|
+
if (!spine) return { ...group, children };
|
|
2227
|
+
const moved = { closed: spine.closed, anchors: spine.anchors.map((a) => mapAnchor(a, m)) };
|
|
2228
|
+
return { ...group, children, blend: { ...group.blend, spine: moved } };
|
|
2229
|
+
}
|
|
1333
2230
|
const path = node;
|
|
1334
2231
|
const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
|
|
1335
2232
|
return {
|
|
@@ -1337,15 +2234,7 @@ function bakeIntoGeometry(node, m) {
|
|
|
1337
2234
|
...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
|
|
1338
2235
|
subpaths: path.subpaths.map((sp) => ({
|
|
1339
2236
|
closed: sp.closed,
|
|
1340
|
-
anchors: sp.anchors.map((a) =>
|
|
1341
|
-
const p = applyMatrix(m, a);
|
|
1342
|
-
const next = { x: p.x, y: p.y };
|
|
1343
|
-
if (a.in) next.in = applyMatrix(m, a.in);
|
|
1344
|
-
if (a.out) next.out = applyMatrix(m, a.out);
|
|
1345
|
-
if (a.type) next.type = a.type;
|
|
1346
|
-
if (a.corner) next.corner = { ...a.corner };
|
|
1347
|
-
return next;
|
|
1348
|
-
})
|
|
2237
|
+
anchors: sp.anchors.map((a) => mapAnchor(a, m))
|
|
1349
2238
|
}))
|
|
1350
2239
|
};
|
|
1351
2240
|
}
|
|
@@ -1397,7 +2286,7 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
|
|
|
1397
2286
|
const dy = pos.y - a.y;
|
|
1398
2287
|
const len2 = Math.hypot(dx, dy);
|
|
1399
2288
|
if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
|
|
1400
|
-
else if (type === "smooth" && len2 >
|
|
2289
|
+
else if (type === "smooth" && len2 > EPS3) {
|
|
1401
2290
|
const o = a[other];
|
|
1402
2291
|
const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
|
|
1403
2292
|
next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
|
|
@@ -1427,15 +2316,15 @@ function convertAnchor(sp, index, type) {
|
|
|
1427
2316
|
else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
|
|
1428
2317
|
else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
|
|
1429
2318
|
else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
|
|
1430
|
-
if (!dir || Math.hypot(dir.x, dir.y) <
|
|
2319
|
+
if (!dir || Math.hypot(dir.x, dir.y) < EPS3) return { anchors, closed: sp.closed };
|
|
1431
2320
|
const dl = Math.hypot(dir.x, dir.y);
|
|
1432
2321
|
const ux = dir.x / dl;
|
|
1433
2322
|
const uy = dir.y / dl;
|
|
1434
2323
|
lenIn = a.in && !isOnAnchor(a, a.in) ? Math.hypot(a.in.x - a.x, a.in.y - a.y) : prev ? Math.hypot(prev.x - a.x, prev.y - a.y) / 3 : 0;
|
|
1435
2324
|
lenOut = a.out && !isOnAnchor(a, a.out) ? Math.hypot(a.out.x - a.x, a.out.y - a.y) : next ? Math.hypot(next.x - a.x, next.y - a.y) / 3 : 0;
|
|
1436
2325
|
if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
|
|
1437
|
-
if (lenIn >
|
|
1438
|
-
if (lenOut >
|
|
2326
|
+
if (lenIn > EPS3) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
|
|
2327
|
+
if (lenOut > EPS3) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
|
|
1439
2328
|
a.type = type;
|
|
1440
2329
|
return { anchors, closed: sp.closed };
|
|
1441
2330
|
}
|
|
@@ -1613,6 +2502,33 @@ function gradientMarkup(def) {
|
|
|
1613
2502
|
const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
|
|
1614
2503
|
return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
|
|
1615
2504
|
}
|
|
2505
|
+
function blendFromAttribute(value, m) {
|
|
2506
|
+
if (!value) return null;
|
|
2507
|
+
let raw;
|
|
2508
|
+
try {
|
|
2509
|
+
raw = JSON.parse(value);
|
|
2510
|
+
} catch {
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2514
|
+
const r = raw;
|
|
2515
|
+
const out = {};
|
|
2516
|
+
if (r.spacing === "steps" || r.spacing === "distance" || r.spacing === "smoothColor") out.spacing = r.spacing;
|
|
2517
|
+
if (typeof r.steps === "number" && Number.isFinite(r.steps)) out.steps = Math.min(1e3, Math.max(1, Math.round(r.steps)));
|
|
2518
|
+
if (typeof r.distance === "number" && r.distance > 0) out.distance = r.distance;
|
|
2519
|
+
if (typeof r.orientToSpine === "boolean") out.orientToSpine = r.orientToSpine;
|
|
2520
|
+
if (typeof r.linear === "boolean") out.linear = r.linear;
|
|
2521
|
+
if (r.starts && typeof r.starts === "object") {
|
|
2522
|
+
const starts = Object.fromEntries(Object.entries(r.starts).filter((e) => Number.isInteger(e[1]) && e[1] >= 0));
|
|
2523
|
+
if (Object.keys(starts).length) out.starts = starts;
|
|
2524
|
+
}
|
|
2525
|
+
const spine = r.spine;
|
|
2526
|
+
if (spine && Array.isArray(spine.anchors) && spine.anchors.every((a) => a && Number.isFinite(a.x) && Number.isFinite(a.y))) {
|
|
2527
|
+
const sp = { closed: !!spine.closed, anchors: spine.anchors.map((a) => ({ x: a.x, y: a.y, ...a.in ? { in: { x: a.in.x, y: a.in.y } } : {}, ...a.out ? { out: { x: a.out.x, y: a.out.y } } : {} })) };
|
|
2528
|
+
out.spine = m === IDENTITY_MATRIX ? sp : transformVectorNode({ id: "", type: "path", subpaths: [sp] }, m).subpaths[0];
|
|
2529
|
+
}
|
|
2530
|
+
return out;
|
|
2531
|
+
}
|
|
1616
2532
|
function vectorDocumentToSvg(doc, options = {}) {
|
|
1617
2533
|
const precision = options.precision ?? 3;
|
|
1618
2534
|
const prefix = options.idPrefix ?? "vg-";
|
|
@@ -1626,13 +2542,35 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1626
2542
|
if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => formatNumber(n, 6)).join(" ")})"`;
|
|
1627
2543
|
if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
|
|
1628
2544
|
if (node.visible === false) out2 += ` display="none"`;
|
|
2545
|
+
if (node.blendStep) out2 += ` data-vector-blend-step=""`;
|
|
2546
|
+
if (node.type === "group" && node.blend) out2 += ` data-vector-blend="${escapeAttr(JSON.stringify(node.blend))}"`;
|
|
2547
|
+
if (node.booleanOperation) out2 += ` data-vector-boolean-operation="${node.booleanOperation}"`;
|
|
1629
2548
|
return out2;
|
|
1630
2549
|
};
|
|
1631
|
-
const
|
|
2550
|
+
const emit2 = (node, depth) => {
|
|
1632
2551
|
if (node.visible === false && !options.includeHidden) return;
|
|
2552
|
+
if (hasOwnVectorAppearance(node)) {
|
|
2553
|
+
const live2 = {
|
|
2554
|
+
kind: node.type,
|
|
2555
|
+
...node.effects?.length ? { effects: node.effects } : {},
|
|
2556
|
+
...node.type === "group" && node.liveBoolean ? { liveBoolean: node.liveBoolean } : {}
|
|
2557
|
+
};
|
|
2558
|
+
lines.push(`${indent(depth)}<g${common(node)} data-vector-live="${escapeAttr(JSON.stringify(live2))}">`);
|
|
2559
|
+
lines.push(`${indent(depth + 1)}<g data-vector-part="appearance">`);
|
|
2560
|
+
for (const shape of vectorAppearanceNodes(node)) emit2(shape, depth + 2);
|
|
2561
|
+
lines.push(`${indent(depth + 1)}</g>`);
|
|
2562
|
+
lines.push(`${indent(depth + 1)}<g data-vector-part="source" display="none">`);
|
|
2563
|
+
if (node.type === "path") {
|
|
2564
|
+
const { effects: _e, transform: _t, opacity: _o, visible: _v, booleanOperation: _b, blendStep: _s, ...source } = node;
|
|
2565
|
+
emit2({ ...source, id: `${node.id}~src` }, depth + 2);
|
|
2566
|
+
} else for (const child of node.children) emit2(child, depth + 2);
|
|
2567
|
+
lines.push(`${indent(depth + 1)}</g>`);
|
|
2568
|
+
lines.push(`${indent(depth)}</g>`);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
1633
2571
|
if (node.type === "group") {
|
|
1634
2572
|
lines.push(`${indent(depth)}<g${common(node)}>`);
|
|
1635
|
-
for (const child of node.children)
|
|
2573
|
+
for (const child of node.children) emit2(child, depth + 1);
|
|
1636
2574
|
lines.push(`${indent(depth)}</g>`);
|
|
1637
2575
|
return;
|
|
1638
2576
|
}
|
|
@@ -1696,7 +2634,7 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1696
2634
|
}
|
|
1697
2635
|
lines.push(`${indent(depth)}</g>`);
|
|
1698
2636
|
};
|
|
1699
|
-
for (const node of doc.nodes)
|
|
2637
|
+
for (const node of doc.nodes) emit2(node, 1);
|
|
1700
2638
|
const w = num(doc.width);
|
|
1701
2639
|
const h = num(doc.height);
|
|
1702
2640
|
const nl = pretty ? "\n" : "";
|
|
@@ -1708,6 +2646,7 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1708
2646
|
out.push(...lines, "</svg>");
|
|
1709
2647
|
return out.join(nl);
|
|
1710
2648
|
}
|
|
2649
|
+
var BOOLEAN_OPERATIONS = ["union", "subtract", "intersect", "exclude"];
|
|
1711
2650
|
function parseXml(source) {
|
|
1712
2651
|
const text = source.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<!DOCTYPE[^>[]*(\[[\s\S]*?\])?\s*>/gi, "");
|
|
1713
2652
|
const root = { name: "#root", attrs: {}, children: [] };
|
|
@@ -1982,6 +2921,34 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1982
2921
|
};
|
|
1983
2922
|
return placed(node, m);
|
|
1984
2923
|
};
|
|
2924
|
+
const liveNode = (g, style, m, meta) => {
|
|
2925
|
+
let raw = {};
|
|
2926
|
+
try {
|
|
2927
|
+
raw = JSON.parse(g.attrs["data-vector-live"]) ?? {};
|
|
2928
|
+
} catch {
|
|
2929
|
+
raw = {};
|
|
2930
|
+
}
|
|
2931
|
+
const sourceEl = g.children.find((c) => c.name === "g" && c.attrs["data-vector-part"] === "source");
|
|
2932
|
+
if (!sourceEl) return null;
|
|
2933
|
+
const inner = visit(sourceEl, style, m);
|
|
2934
|
+
const effects = sanitizeVectorEffects(raw.effects);
|
|
2935
|
+
const liveBoolean = BOOLEAN_OPERATIONS.includes(raw.liveBoolean) ? raw.liveBoolean : null;
|
|
2936
|
+
const id = idFor(g);
|
|
2937
|
+
if (raw.kind === "path") {
|
|
2938
|
+
const path = inner.find((n) => n.type === "path");
|
|
2939
|
+
if (!path) return null;
|
|
2940
|
+
const { residual } = decomposeRigid(m);
|
|
2941
|
+
const { visible: _v, ...rest } = path;
|
|
2942
|
+
return { ...rest, id, ...meta, ...effects.length ? { effects: transformVectorEffects(effects, residual) } : {} };
|
|
2943
|
+
}
|
|
2944
|
+
if (!inner.length) return null;
|
|
2945
|
+
const group = { id, type: "group", ...meta, children: inner };
|
|
2946
|
+
if (effects.length) group.effects = transformVectorEffects(effects, m);
|
|
2947
|
+
if (liveBoolean) group.liveBoolean = liveBoolean;
|
|
2948
|
+
const blend = blendFromAttribute(g.attrs["data-vector-blend"], m);
|
|
2949
|
+
if (blend) group.blend = blend;
|
|
2950
|
+
return group;
|
|
2951
|
+
};
|
|
1985
2952
|
const visit = (el, style, matrix) => {
|
|
1986
2953
|
const out = [];
|
|
1987
2954
|
for (const child of el.children) {
|
|
@@ -1995,6 +2962,14 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1995
2962
|
...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
|
|
1996
2963
|
...hidden ? { visible: false } : {}
|
|
1997
2964
|
};
|
|
2965
|
+
if (child.attrs["data-vector-blend-step"] !== void 0) continue;
|
|
2966
|
+
const operation = child.attrs["data-vector-boolean-operation"];
|
|
2967
|
+
if (BOOLEAN_OPERATIONS.includes(operation)) meta.booleanOperation = operation;
|
|
2968
|
+
if (child.name === "g" && child.attrs["data-vector-live"] !== void 0) {
|
|
2969
|
+
const node2 = liveNode(child, s, m, meta);
|
|
2970
|
+
if (node2) out.push(node2);
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
1998
2973
|
if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
|
|
1999
2974
|
const node2 = mergedPath(child, s, m, meta);
|
|
2000
2975
|
if (node2) out.push(node2);
|
|
@@ -2004,6 +2979,8 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
2004
2979
|
const children = visit(child, s, m);
|
|
2005
2980
|
if (!children.length) continue;
|
|
2006
2981
|
const group = { id: idFor(child), type: "group", ...meta, children };
|
|
2982
|
+
const blend = child.name === "g" ? blendFromAttribute(child.attrs["data-vector-blend"], m) : null;
|
|
2983
|
+
if (blend) group.blend = blend;
|
|
2007
2984
|
out.push(group);
|
|
2008
2985
|
continue;
|
|
2009
2986
|
}
|
|
@@ -2029,6 +3006,910 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
2029
3006
|
return { width, height, nodes };
|
|
2030
3007
|
}
|
|
2031
3008
|
|
|
3009
|
+
// src/vector/blend.ts
|
|
3010
|
+
var lerpN = (a, b, t) => a + (b - a) * t;
|
|
3011
|
+
var lerpP = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
3012
|
+
var toP = (v2) => [v2.x, v2.y];
|
|
3013
|
+
var mapP = (m, p) => [m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]];
|
|
3014
|
+
var curvesOf = (c) => Math.max(0, (c.pts.length - 1) / 3) | 0;
|
|
3015
|
+
function subpathToMorphContour(sp, m = IDENTITY_MATRIX) {
|
|
3016
|
+
const n = sp.anchors.length;
|
|
3017
|
+
if (!n) return null;
|
|
3018
|
+
const first = mapP(m, toP(sp.anchors[0]));
|
|
3019
|
+
const pts = [first];
|
|
3020
|
+
const count = n < 2 ? 0 : sp.closed ? n : n - 1;
|
|
3021
|
+
for (let i = 0; i < count; i++) {
|
|
3022
|
+
const a = sp.anchors[i];
|
|
3023
|
+
const b = sp.anchors[(i + 1) % n];
|
|
3024
|
+
const pa = toP(a);
|
|
3025
|
+
const pb = toP(b);
|
|
3026
|
+
const straight = (!a.out || a.out.x === a.x && a.out.y === a.y) && (!b.in || b.in.x === b.x && b.in.y === b.y);
|
|
3027
|
+
const c1 = straight ? lerpP(pa, pb, 1 / 3) : toP(a.out ?? a);
|
|
3028
|
+
const c2 = straight ? lerpP(pa, pb, 2 / 3) : toP(b.in ?? b);
|
|
3029
|
+
pts.push(mapP(m, c1), mapP(m, c2), i === count - 1 && sp.closed ? first : mapP(m, pb));
|
|
3030
|
+
}
|
|
3031
|
+
return { closed: sp.closed && count > 0, pts };
|
|
3032
|
+
}
|
|
3033
|
+
function isStraight(p0, p1, p2, p3) {
|
|
3034
|
+
const dx = p3[0] - p0[0];
|
|
3035
|
+
const dy = p3[1] - p0[1];
|
|
3036
|
+
const len2 = Math.hypot(dx, dy);
|
|
3037
|
+
const tol = Math.max(1e-7, len2 * 1e-7);
|
|
3038
|
+
if (len2 < 1e-12) return Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) < tol && Math.hypot(p2[0] - p0[0], p2[1] - p0[1]) < tol;
|
|
3039
|
+
const off = (p) => Math.abs((p[0] - p0[0]) * dy - (p[1] - p0[1]) * dx) / len2;
|
|
3040
|
+
const along2 = (p) => ((p[0] - p0[0]) * dx + (p[1] - p0[1]) * dy) / (len2 * len2);
|
|
3041
|
+
const u1 = along2(p1);
|
|
3042
|
+
const u2 = along2(p2);
|
|
3043
|
+
return off(p1) < tol && off(p2) < tol && u1 >= -1e-9 && u1 <= 1 + 1e-9 && u2 >= -1e-9 && u2 <= 1 + 1e-9;
|
|
3044
|
+
}
|
|
3045
|
+
function morphContourToSubpath(c, m = IDENTITY_MATRIX, closed = c.closed) {
|
|
3046
|
+
const pts = isIdentityMatrix(m) ? c.pts : c.pts.map((p) => mapP(m, p));
|
|
3047
|
+
const curves = curvesOf(c);
|
|
3048
|
+
const dropLast = closed && c.closed && curves > 0;
|
|
3049
|
+
const count = dropLast ? curves : curves + 1;
|
|
3050
|
+
const anchors = [];
|
|
3051
|
+
for (let k = 0; k < count; k++) anchors.push({ x: pts[3 * k][0], y: pts[3 * k][1] });
|
|
3052
|
+
for (let k = 0; k < curves; k++) {
|
|
3053
|
+
const p0 = pts[3 * k];
|
|
3054
|
+
const p1 = pts[3 * k + 1];
|
|
3055
|
+
const p2 = pts[3 * k + 2];
|
|
3056
|
+
const p3 = pts[3 * k + 3];
|
|
3057
|
+
if (isStraight(p0, p1, p2, p3)) continue;
|
|
3058
|
+
anchors[k].out = { x: p1[0], y: p1[1] };
|
|
3059
|
+
anchors[(k + 1) % count].in = { x: p2[0], y: p2[1] };
|
|
3060
|
+
}
|
|
3061
|
+
for (const a of anchors) {
|
|
3062
|
+
const type = inferAnchorType(a);
|
|
3063
|
+
if (type !== "corner") a.type = type;
|
|
3064
|
+
}
|
|
3065
|
+
const isClosed = closed && curves > 0;
|
|
3066
|
+
return { anchors: dropCollinear(anchors, isClosed), closed: isClosed };
|
|
3067
|
+
}
|
|
3068
|
+
function dropCollinear(anchors, closed) {
|
|
3069
|
+
const out = anchors.slice();
|
|
3070
|
+
const min = closed ? 3 : 2;
|
|
3071
|
+
let i = closed ? 0 : 1;
|
|
3072
|
+
while (out.length > min && i < out.length - (closed ? 0 : 1)) {
|
|
3073
|
+
const n = out.length;
|
|
3074
|
+
const a = out[(i - 1 + n) % n];
|
|
3075
|
+
const b = out[i];
|
|
3076
|
+
const c = out[(i + 1) % n];
|
|
3077
|
+
const plain3 = !b.in && !b.out && !a.out && !c.in;
|
|
3078
|
+
const ux = b.x - a.x;
|
|
3079
|
+
const uy = b.y - a.y;
|
|
3080
|
+
const vx = c.x - b.x;
|
|
3081
|
+
const vy = c.y - b.y;
|
|
3082
|
+
const len2 = Math.hypot(ux, uy) + Math.hypot(vx, vy);
|
|
3083
|
+
if (plain3 && Math.abs(ux * vy - uy * vx) <= 1e-9 * Math.max(1, len2 * len2) && ux * vx + uy * vy >= 0) out.splice(i, 1);
|
|
3084
|
+
else i++;
|
|
3085
|
+
}
|
|
3086
|
+
return out;
|
|
3087
|
+
}
|
|
3088
|
+
var segOf = (c, k) => c.pts.slice(3 * k, 3 * k + 4);
|
|
3089
|
+
function refineMorphContour(c, segments) {
|
|
3090
|
+
const have = curvesOf(c);
|
|
3091
|
+
if (have === 0) return { contour: { closed: c.closed, pts: Array.from({ length: 1 + 3 * segments }, () => [c.pts[0][0], c.pts[0][1]]) }, anchors: [0] };
|
|
3092
|
+
const identity = Array.from({ length: c.closed ? have : have + 1 }, (_, i) => i);
|
|
3093
|
+
if (have >= segments) return { contour: c, anchors: identity };
|
|
3094
|
+
const segs = Array.from({ length: have }, (_, k) => segOf(c, k));
|
|
3095
|
+
const lens = segs.map((s) => segmentLength(s));
|
|
3096
|
+
const sum = lens.reduce((a, b) => a + b, 0);
|
|
3097
|
+
const extra = segments - have;
|
|
3098
|
+
const want = lens.map((l) => sum > 1e-12 ? l / sum * extra : extra / have);
|
|
3099
|
+
const parts = want.map((w) => 1 + Math.floor(w));
|
|
3100
|
+
let left = segments - parts.reduce((a, b) => a + b, 0);
|
|
3101
|
+
const frac = want.map((w) => w - Math.floor(w));
|
|
3102
|
+
const order = parts.map((_, i) => i).sort((a, b) => Math.abs(frac[b] - frac[a]) > 1e-9 ? frac[b] - frac[a] : a - b);
|
|
3103
|
+
for (let i = 0; left > 0; i = (i + 1) % order.length, left--) parts[order[i]]++;
|
|
3104
|
+
const pts = [c.pts[0]];
|
|
3105
|
+
const anchors = [0];
|
|
3106
|
+
let at2 = 0;
|
|
3107
|
+
segs.forEach((seg, k) => {
|
|
3108
|
+
const pieces = parts[k];
|
|
3109
|
+
let rest = seg.slice();
|
|
3110
|
+
let t0 = 0;
|
|
3111
|
+
for (let j = 1; j < pieces; j++) {
|
|
3112
|
+
const t = segmentParamAtLength(seg, lens[k] * j / pieces, lens[k]);
|
|
3113
|
+
const u = Math.min(1, Math.max(0, (t - t0) / (1 - t0 || 1)));
|
|
3114
|
+
const [l, r] = splitSegment(rest, u);
|
|
3115
|
+
pts.push(l[1], l[2], l[3]);
|
|
3116
|
+
rest = r;
|
|
3117
|
+
t0 = t;
|
|
3118
|
+
}
|
|
3119
|
+
pts.push(rest[1], rest[2], k === have - 1 && c.closed ? c.pts[0] : rest[3]);
|
|
3120
|
+
at2 += pieces;
|
|
3121
|
+
if (k < have - 1 || !c.closed) anchors.push(at2);
|
|
3122
|
+
});
|
|
3123
|
+
return { contour: { closed: c.closed, pts }, anchors };
|
|
3124
|
+
}
|
|
3125
|
+
function reverseMorphContour(c) {
|
|
3126
|
+
return { closed: c.closed, pts: c.pts.slice().reverse() };
|
|
3127
|
+
}
|
|
3128
|
+
function turnMorphContour(c, s) {
|
|
3129
|
+
const n = curvesOf(c);
|
|
3130
|
+
if (!c.closed || n === 0) return c;
|
|
3131
|
+
const k = (s % n + n) % n;
|
|
3132
|
+
if (k === 0) return c;
|
|
3133
|
+
const body = c.pts.slice(1);
|
|
3134
|
+
return { closed: true, pts: [c.pts[3 * k], ...body.slice(3 * k), ...body.slice(0, 3 * k)] };
|
|
3135
|
+
}
|
|
3136
|
+
function anchorPoints(c) {
|
|
3137
|
+
const n = curvesOf(c);
|
|
3138
|
+
const count = c.closed ? n : n + 1;
|
|
3139
|
+
return Array.from({ length: Math.max(1, count) }, (_, k) => c.pts[3 * k]);
|
|
3140
|
+
}
|
|
3141
|
+
function polygonArea(pts) {
|
|
3142
|
+
let s = 0;
|
|
3143
|
+
for (let i = 0; i < pts.length; i++) {
|
|
3144
|
+
const a = pts[i];
|
|
3145
|
+
const b = pts[(i + 1) % pts.length];
|
|
3146
|
+
s += a[0] * b[1] - b[0] * a[1];
|
|
3147
|
+
}
|
|
3148
|
+
return s / 2;
|
|
3149
|
+
}
|
|
3150
|
+
function frame(v2) {
|
|
3151
|
+
const n = Math.max(1, v2.length);
|
|
3152
|
+
let cx = 0;
|
|
3153
|
+
let cy = 0;
|
|
3154
|
+
for (const p of v2) {
|
|
3155
|
+
cx += p[0];
|
|
3156
|
+
cy += p[1];
|
|
3157
|
+
}
|
|
3158
|
+
cx /= n;
|
|
3159
|
+
cy /= n;
|
|
3160
|
+
let s = 0;
|
|
3161
|
+
for (const p of v2) s += (p[0] - cx) ** 2 + (p[1] - cy) ** 2;
|
|
3162
|
+
return { c: [cx, cy], r: Math.max(1e-9, Math.sqrt(s / n)) };
|
|
3163
|
+
}
|
|
3164
|
+
var RBINS = 5;
|
|
3165
|
+
var ABINS = 12;
|
|
3166
|
+
var TURN_WEIGHT = 0.1;
|
|
3167
|
+
function shapeContexts(v2, closed) {
|
|
3168
|
+
const { r } = frame(v2);
|
|
3169
|
+
const n = v2.length;
|
|
3170
|
+
return v2.map((p, i) => {
|
|
3171
|
+
const prev = v2[closed ? (i - 1 + n) % n : Math.max(0, i - 1)];
|
|
3172
|
+
const next = v2[closed ? (i + 1) % n : Math.min(n - 1, i + 1)];
|
|
3173
|
+
const dir = Math.atan2(next[1] - prev[1], next[0] - prev[0]);
|
|
3174
|
+
const h = new Float64Array(RBINS * ABINS);
|
|
3175
|
+
let total = 0;
|
|
3176
|
+
for (let j = 0; j < n; j++) {
|
|
3177
|
+
if (i === j) continue;
|
|
3178
|
+
const dx = v2[j][0] - p[0];
|
|
3179
|
+
const dy = v2[j][1] - p[1];
|
|
3180
|
+
const d = Math.hypot(dx, dy) / r;
|
|
3181
|
+
const lr = (Math.log2(Math.max(d, 1e-6)) + 3) / 4 * RBINS;
|
|
3182
|
+
if (!(lr >= 0 && lr < RBINS)) continue;
|
|
3183
|
+
let ang = Math.atan2(dy, dx) - dir;
|
|
3184
|
+
ang = (ang % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
|
|
3185
|
+
h[Math.floor(lr) * ABINS + Math.min(ABINS - 1, Math.floor(ang / (2 * Math.PI) * ABINS))] += 1;
|
|
3186
|
+
total++;
|
|
3187
|
+
}
|
|
3188
|
+
if (total > 0) for (let k = 0; k < h.length; k++) h[k] /= total;
|
|
3189
|
+
return h;
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
function chi2(a, b) {
|
|
3193
|
+
let s = 0;
|
|
3194
|
+
for (let k = 0; k < a.length; k++) {
|
|
3195
|
+
const t = a[k] + b[k];
|
|
3196
|
+
if (t > 1e-12) s += (a[k] - b[k]) * (a[k] - b[k]) / t;
|
|
3197
|
+
}
|
|
3198
|
+
return s / 2;
|
|
3199
|
+
}
|
|
3200
|
+
function bestStart(fixed, free, useShapeContext) {
|
|
3201
|
+
const fa = anchorPoints(fixed);
|
|
3202
|
+
const ga = anchorPoints(free);
|
|
3203
|
+
const n = ga.length;
|
|
3204
|
+
const ff = frame(fa);
|
|
3205
|
+
const gf = frame(ga);
|
|
3206
|
+
const fn = fa.map((p) => [(p[0] - ff.c[0]) / ff.r, (p[1] - ff.c[1]) / ff.r]);
|
|
3207
|
+
const gn = ga.map((p) => [(p[0] - gf.c[0]) / gf.r, (p[1] - gf.c[1]) / gf.r]);
|
|
3208
|
+
const fsc = useShapeContext ? shapeContexts(fa, fixed.closed) : null;
|
|
3209
|
+
const gsc = useShapeContext ? shapeContexts(ga, free.closed) : null;
|
|
3210
|
+
const m = Math.max(1, fn.length);
|
|
3211
|
+
const cost = (s) => {
|
|
3212
|
+
let dot2 = 0;
|
|
3213
|
+
let cross2 = 0;
|
|
3214
|
+
let sc = 0;
|
|
3215
|
+
for (let i = 0; i < fn.length; i++) {
|
|
3216
|
+
const j = free.closed ? (i + s) % n : i;
|
|
3217
|
+
const p = fn[i];
|
|
3218
|
+
const q = gn[j];
|
|
3219
|
+
dot2 += p[0] * q[0] + p[1] * q[1];
|
|
3220
|
+
cross2 += p[0] * q[1] - p[1] * q[0];
|
|
3221
|
+
if (fsc && gsc) sc += chi2(fsc[i], gsc[j]);
|
|
3222
|
+
}
|
|
3223
|
+
const residual = Math.max(0, 1 - (dot2 * dot2 + cross2 * cross2) / (m * m));
|
|
3224
|
+
const turn = 1 - Math.cos(Math.atan2(cross2, dot2));
|
|
3225
|
+
return residual + TURN_WEIGHT * turn + sc / m;
|
|
3226
|
+
};
|
|
3227
|
+
if (!free.closed) return { start: 0, cost: cost(0) };
|
|
3228
|
+
const stride = n > 96 ? Math.ceil(n / 48) : 1;
|
|
3229
|
+
let best = { start: 0, cost: Infinity };
|
|
3230
|
+
for (let s = 0; s < n; s += stride) {
|
|
3231
|
+
const c = cost(s);
|
|
3232
|
+
if (c < best.cost - 1e-12) best = { start: s, cost: c };
|
|
3233
|
+
}
|
|
3234
|
+
if (stride > 1) {
|
|
3235
|
+
const s0 = best.start;
|
|
3236
|
+
for (let d = 1; d < stride; d++)
|
|
3237
|
+
for (const s of [(s0 + d) % n, (s0 - d + n) % n]) {
|
|
3238
|
+
const c = cost(s);
|
|
3239
|
+
if (c < best.cost - 1e-12) best = { start: s, cost: c };
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
return best;
|
|
3243
|
+
}
|
|
3244
|
+
function placeIn(c, from, to) {
|
|
3245
|
+
const b = unionBounds([boundsOfPoints(c.pts)]);
|
|
3246
|
+
const p = b ? [b.x + b.width / 2, b.y + b.height / 2] : c.pts[0];
|
|
3247
|
+
if (!from || !to) return p;
|
|
3248
|
+
const fx = from.width > 0 ? (p[0] - from.x) / from.width : 0.5;
|
|
3249
|
+
const fy = from.height > 0 ? (p[1] - from.y) / from.height : 0.5;
|
|
3250
|
+
return [to.x + to.width * fx, to.y + to.height * fy];
|
|
3251
|
+
}
|
|
3252
|
+
function boundsOfPoints(pts) {
|
|
3253
|
+
if (!pts.length) return null;
|
|
3254
|
+
let x0 = Infinity;
|
|
3255
|
+
let y0 = Infinity;
|
|
3256
|
+
let x1 = -Infinity;
|
|
3257
|
+
let y1 = -Infinity;
|
|
3258
|
+
for (const p of pts) {
|
|
3259
|
+
x0 = Math.min(x0, p[0]);
|
|
3260
|
+
y0 = Math.min(y0, p[1]);
|
|
3261
|
+
x1 = Math.max(x1, p[0]);
|
|
3262
|
+
y1 = Math.max(y1, p[1]);
|
|
3263
|
+
}
|
|
3264
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
3265
|
+
}
|
|
3266
|
+
function correspondMorphContours(a, b, options = {}) {
|
|
3267
|
+
const minSeg = Math.max(1, Math.round(options.minSegments ?? 16));
|
|
3268
|
+
const maxSeg = Math.max(minSeg, Math.round(options.maxSegments ?? 400));
|
|
3269
|
+
const spacing = options.spacing && options.spacing > 0 ? options.spacing : null;
|
|
3270
|
+
const useSC = options.shapeContext !== false;
|
|
3271
|
+
const match = options.matchStart !== false;
|
|
3272
|
+
const ba = boundsOfPoints(a.flatMap((c) => c.pts));
|
|
3273
|
+
const bb = boundsOfPoints(b.flatMap((c) => c.pts));
|
|
3274
|
+
const contourLength = (c) => {
|
|
3275
|
+
let l = 0;
|
|
3276
|
+
for (let k = 0; k < curvesOf(c); k++) l += segmentLength(segOf(c, k));
|
|
3277
|
+
return l;
|
|
3278
|
+
};
|
|
3279
|
+
const pairs = [];
|
|
3280
|
+
const [sa, sb] = options.starts ?? [];
|
|
3281
|
+
for (let k = 0; k < Math.max(a.length, b.length); k++) {
|
|
3282
|
+
const ca = a[k];
|
|
3283
|
+
const cb = b[k];
|
|
3284
|
+
const explicitA = k === 0 && sa !== null && sa !== void 0 ? sa : null;
|
|
3285
|
+
const explicitB = k === 0 && sb !== null && sb !== void 0 ? sb : null;
|
|
3286
|
+
const counts = [ca ? curvesOf(ca) : 0, cb ? curvesOf(cb) : 0];
|
|
3287
|
+
const longest = Math.max(ca ? contourLength(ca) : 0, cb ? contourLength(cb) : 0);
|
|
3288
|
+
let n = Math.max(counts[0], counts[1], minSeg, spacing ? Math.ceil(longest / spacing) : 0);
|
|
3289
|
+
n = Math.max(counts[0], counts[1], Math.min(n, maxSeg));
|
|
3290
|
+
const point = (c, from, to) => {
|
|
3291
|
+
const p = placeIn(c, from, to);
|
|
3292
|
+
return { closed: c.closed, pts: Array.from({ length: 1 + 3 * n }, () => [p[0], p[1]]) };
|
|
3293
|
+
};
|
|
3294
|
+
if (ca && cb) {
|
|
3295
|
+
const ra = refineMorphContour(ca, n);
|
|
3296
|
+
const rb = refineMorphContour(cb, n);
|
|
3297
|
+
pairs.push({
|
|
3298
|
+
x: ra.contour,
|
|
3299
|
+
y: rb.contour,
|
|
3300
|
+
point: false,
|
|
3301
|
+
startX: explicitA === null ? null : ra.anchors[explicitA] ?? null,
|
|
3302
|
+
startY: explicitB === null ? null : explicitB === rb.anchors.length - 1 && !cb.closed ? -1 : rb.anchors[explicitB] ?? null
|
|
3303
|
+
});
|
|
3304
|
+
if (explicitA !== null && !ca.closed && explicitA === ra.anchors.length - 1 && explicitA > 0) pairs[pairs.length - 1].startX = -1;
|
|
3305
|
+
} else if (ca) pairs.push({ x: refineMorphContour(ca, n).contour, y: point(ca, ba, bb), point: true, startX: null, startY: null });
|
|
3306
|
+
else if (cb) pairs.push({ x: point(cb, bb, ba), y: refineMorphContour(cb, n).contour, point: true, startX: null, startY: null });
|
|
3307
|
+
}
|
|
3308
|
+
const startAt = (c, s) => {
|
|
3309
|
+
if (s === null) return c;
|
|
3310
|
+
if (s === -1) return reverseMorphContour(c);
|
|
3311
|
+
return c.closed ? turnMorphContour(c, s) : c;
|
|
3312
|
+
};
|
|
3313
|
+
const explicit = pairs.length > 0 && (pairs[0].startX !== null || pairs[0].startY !== null);
|
|
3314
|
+
const ref = pairs.find((p) => !p.point && p.x.closed && p.y.closed);
|
|
3315
|
+
const fixedBack = ref ? Math.sign(polygonArea(anchorPoints(ref.x))) !== Math.sign(polygonArea(anchorPoints(ref.y))) && Math.abs(polygonArea(anchorPoints(ref.y))) > 1e-12 : explicit ? false : null;
|
|
3316
|
+
let best = null;
|
|
3317
|
+
for (const back of fixedBack === null ? [false, true] : [fixedBack]) {
|
|
3318
|
+
const xs = [];
|
|
3319
|
+
const ys = [];
|
|
3320
|
+
let cost = 0;
|
|
3321
|
+
pairs.forEach((p, k) => {
|
|
3322
|
+
let x = k === 0 ? startAt(p.x, p.startX) : p.x;
|
|
3323
|
+
let y = k === 0 ? startAt(p.y, p.startY) : p.y;
|
|
3324
|
+
if (back) y = reverseMorphContour(y);
|
|
3325
|
+
if (p.point) {
|
|
3326
|
+
xs.push(x);
|
|
3327
|
+
ys.push(y);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
const pinned = k === 0 && explicit;
|
|
3331
|
+
const turnX = !y.closed && x.closed;
|
|
3332
|
+
if (match && !pinned && (y.closed || turnX)) {
|
|
3333
|
+
const r = turnX ? bestStart(y, x, useSC) : bestStart(x, y, useSC);
|
|
3334
|
+
if (turnX) x = turnMorphContour(x, r.start);
|
|
3335
|
+
else y = turnMorphContour(y, r.start);
|
|
3336
|
+
cost += r.cost;
|
|
3337
|
+
} else cost += bestStart(x, { ...y, closed: false }, useSC).cost;
|
|
3338
|
+
xs.push(x);
|
|
3339
|
+
ys.push(y);
|
|
3340
|
+
});
|
|
3341
|
+
if (!best || cost < best.cost - 1e-9) best = { xs, ys, cost, back };
|
|
3342
|
+
}
|
|
3343
|
+
return best ? { a: best.xs, b: best.ys, reversed: best.back } : { a: [], b: [], reversed: false };
|
|
3344
|
+
}
|
|
3345
|
+
function morphPathsCross(a, b) {
|
|
3346
|
+
let count = 0;
|
|
3347
|
+
const d = (p, q, r) => (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
|
|
3348
|
+
for (let k = 0; k < Math.min(a.length, b.length); k++) {
|
|
3349
|
+
const pa = anchorPoints(a[k]);
|
|
3350
|
+
const pb = anchorPoints(b[k]);
|
|
3351
|
+
const n = Math.min(pa.length, pb.length);
|
|
3352
|
+
for (let i = 0; i < n; i++)
|
|
3353
|
+
for (let j = i + 1; j < n; j++) {
|
|
3354
|
+
const d1 = d(pa[j], pb[j], pa[i]);
|
|
3355
|
+
const d2 = d(pa[j], pb[j], pb[i]);
|
|
3356
|
+
const d3 = d(pa[i], pb[i], pa[j]);
|
|
3357
|
+
const d4 = d(pa[i], pb[i], pb[j]);
|
|
3358
|
+
if (d1 * d2 < 0 && d3 * d4 < 0) count++;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
return count;
|
|
3362
|
+
}
|
|
3363
|
+
function procrustes(a, b) {
|
|
3364
|
+
const pa = a.flatMap(anchorPoints);
|
|
3365
|
+
const pb = b.flatMap(anchorPoints);
|
|
3366
|
+
const ca = frame(pa).c;
|
|
3367
|
+
const cb = frame(pb).c;
|
|
3368
|
+
let ss = 0;
|
|
3369
|
+
let dot2 = 0;
|
|
3370
|
+
let cross2 = 0;
|
|
3371
|
+
for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
|
|
3372
|
+
const p = [pa[i][0] - ca[0], pa[i][1] - ca[1]];
|
|
3373
|
+
const q = [pb[i][0] - cb[0], pb[i][1] - cb[1]];
|
|
3374
|
+
ss += p[0] * p[0] + p[1] * p[1];
|
|
3375
|
+
dot2 += p[0] * q[0] + p[1] * q[1];
|
|
3376
|
+
cross2 += p[0] * q[1] - p[1] * q[0];
|
|
3377
|
+
}
|
|
3378
|
+
return { ca, cb, theta: Math.atan2(cross2, dot2), s: ss > 1e-12 ? Math.hypot(dot2, cross2) / ss : 1 };
|
|
3379
|
+
}
|
|
3380
|
+
function interpolateMorphContours(a, b, u, options = {}) {
|
|
3381
|
+
const w = options.linear ? 0 : Math.min(1, Math.max(0, options.rigidity ?? 1));
|
|
3382
|
+
const sim = w > 0 ? options.similarity ?? procrustes(a, b) : null;
|
|
3383
|
+
const rot = (p, ang, k) => {
|
|
3384
|
+
const c = Math.cos(ang);
|
|
3385
|
+
const s = Math.sin(ang);
|
|
3386
|
+
return [k * (c * p[0] - s * p[1]), k * (s * p[0] + c * p[1])];
|
|
3387
|
+
};
|
|
3388
|
+
const thetaU = sim ? sim.theta * u : 0;
|
|
3389
|
+
const sU = sim ? Math.max(1e-9, sim.s) ** u : 1;
|
|
3390
|
+
const cU = sim ? lerpP(sim.ca, sim.cb, u) : [0, 0];
|
|
3391
|
+
return a.map((ca, k) => {
|
|
3392
|
+
const cb = b[k];
|
|
3393
|
+
const pts = ca.pts.map((p, i) => {
|
|
3394
|
+
const q = cb.pts[i] ?? p;
|
|
3395
|
+
const linear = lerpP(p, q, u);
|
|
3396
|
+
if (!sim) return linear;
|
|
3397
|
+
const d = [p[0] - sim.ca[0], p[1] - sim.ca[1]];
|
|
3398
|
+
const full = rot(d, sim.theta, sim.s);
|
|
3399
|
+
const resid = [q[0] - (full[0] + sim.cb[0]), q[1] - (full[1] + sim.cb[1])];
|
|
3400
|
+
const part = rot(d, thetaU, sU);
|
|
3401
|
+
const rigid = [part[0] + cU[0] + resid[0] * u, part[1] + cU[1] + resid[1] * u];
|
|
3402
|
+
return lerpP(linear, rigid, w);
|
|
3403
|
+
});
|
|
3404
|
+
return { closed: ca.closed && cb.closed, pts };
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
function interpolateRigidMatrix(a, b, t) {
|
|
3408
|
+
const decompose = (m) => {
|
|
3409
|
+
const sx2 = Math.hypot(m[0], m[1]);
|
|
3410
|
+
const ang2 = sx2 > 1e-12 ? Math.atan2(m[1], m[0]) : 0;
|
|
3411
|
+
const c2 = Math.cos(ang2);
|
|
3412
|
+
const s2 = Math.sin(ang2);
|
|
3413
|
+
return { e: m[4], f: m[5], ang: ang2, sx: sx2, sh: c2 * m[2] + s2 * m[3], sy: -s2 * m[2] + c2 * m[3] };
|
|
3414
|
+
};
|
|
3415
|
+
const p = decompose(a);
|
|
3416
|
+
const q = decompose(b);
|
|
3417
|
+
let turn = q.ang - p.ang;
|
|
3418
|
+
turn = ((turn + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
|
|
3419
|
+
const ang = p.ang + turn * t;
|
|
3420
|
+
const sx = lerpN(p.sx, q.sx, t);
|
|
3421
|
+
const sh = lerpN(p.sh, q.sh, t);
|
|
3422
|
+
const sy = lerpN(p.sy, q.sy, t);
|
|
3423
|
+
const c = Math.cos(ang);
|
|
3424
|
+
const s = Math.sin(ang);
|
|
3425
|
+
return [c * sx, s * sx, c * sh - s * sy, s * sh + c * sy, lerpN(p.e, q.e, t), lerpN(p.f, q.f, t)];
|
|
3426
|
+
}
|
|
3427
|
+
function interpolateVectorColor(a, b, t) {
|
|
3428
|
+
if (t <= 0) return a;
|
|
3429
|
+
if (t >= 1) return b;
|
|
3430
|
+
return mixColorsOklab(a, b, t) ?? (t < 0.5 ? a : b);
|
|
3431
|
+
}
|
|
3432
|
+
function sortedStops(g) {
|
|
3433
|
+
return g.stops.slice().sort((x, y) => x.offset - y.offset);
|
|
3434
|
+
}
|
|
3435
|
+
function sampleGradient(stops, offset) {
|
|
3436
|
+
if (!stops.length) return { color: "transparent", opacity: 1 };
|
|
3437
|
+
const first = stops[0];
|
|
3438
|
+
const last = stops[stops.length - 1];
|
|
3439
|
+
if (offset <= first.offset) return { color: first.color, opacity: first.opacity ?? 1 };
|
|
3440
|
+
if (offset >= last.offset) return { color: last.color, opacity: last.opacity ?? 1 };
|
|
3441
|
+
let i = 0;
|
|
3442
|
+
while (i + 1 < stops.length && stops[i + 1].offset < offset) i++;
|
|
3443
|
+
const x = stops[i];
|
|
3444
|
+
const y = stops[i + 1];
|
|
3445
|
+
const u = y.offset - x.offset > 1e-12 ? (offset - x.offset) / (y.offset - x.offset) : 1;
|
|
3446
|
+
return { color: interpolateVectorColor(x.color, y.color, u), opacity: lerpN(x.opacity ?? 1, y.opacity ?? 1, u) };
|
|
3447
|
+
}
|
|
3448
|
+
function resampleGradientStops(g, offsets) {
|
|
3449
|
+
const stops = sortedStops(g);
|
|
3450
|
+
return offsets.map((offset) => {
|
|
3451
|
+
const s = sampleGradient(stops, offset);
|
|
3452
|
+
return s.opacity < 1 ? { offset, color: s.color, opacity: s.opacity } : { offset, color: s.color };
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
function lerpAngle(a, b, t) {
|
|
3456
|
+
const d = ((b - a + 180) % 360 + 360) % 360 - 180;
|
|
3457
|
+
return a + d * t;
|
|
3458
|
+
}
|
|
3459
|
+
function lerpGradient(a, b, t) {
|
|
3460
|
+
if (a.type !== b.type) return null;
|
|
3461
|
+
let stopsA = sortedStops(a);
|
|
3462
|
+
let stopsB = sortedStops(b);
|
|
3463
|
+
if (stopsA.length !== stopsB.length) {
|
|
3464
|
+
const offs = [...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
|
|
3465
|
+
const common = offs.filter((o, i) => i === 0 || o - offs[i - 1] > 1e-4);
|
|
3466
|
+
stopsA = resampleGradientStops(a, common);
|
|
3467
|
+
stopsB = resampleGradientStops(b, common);
|
|
3468
|
+
}
|
|
3469
|
+
const stops = stopsA.map((x, i) => {
|
|
3470
|
+
const y = stopsB[i];
|
|
3471
|
+
const opacity = lerpN(x.opacity ?? 1, y.opacity ?? 1, t);
|
|
3472
|
+
const stop = { offset: lerpN(x.offset, y.offset, t), color: interpolateVectorColor(x.color, y.color, t) };
|
|
3473
|
+
if (opacity < 1) stop.opacity = opacity;
|
|
3474
|
+
return stop;
|
|
3475
|
+
});
|
|
3476
|
+
if (a.type === "linear" && b.type === "linear") return { type: "linear", angle: lerpAngle(a.angle ?? 90, b.angle ?? 90, t), stops };
|
|
3477
|
+
const ra = a;
|
|
3478
|
+
const rb = b;
|
|
3479
|
+
return { type: "radial", cx: lerpN(ra.cx ?? 0.5, rb.cx ?? 0.5, t), cy: lerpN(ra.cy ?? 0.5, rb.cy ?? 0.5, t), r: lerpN(ra.r ?? 0.5, rb.r ?? 0.5, t), stops };
|
|
3480
|
+
}
|
|
3481
|
+
function solidLike(color, like) {
|
|
3482
|
+
return { ...like, stops: like.stops.map((s) => color === null ? { offset: s.offset, color: s.color, opacity: 0 } : { offset: s.offset, color }) };
|
|
3483
|
+
}
|
|
3484
|
+
function interpolateVectorPaint(a, b, t) {
|
|
3485
|
+
if (t <= 0) return a;
|
|
3486
|
+
if (t >= 1) return b;
|
|
3487
|
+
const pa = hasPaint(a);
|
|
3488
|
+
const pb = hasPaint(b);
|
|
3489
|
+
if (!pa && !pb) return t < 0.5 ? a : b;
|
|
3490
|
+
if (typeof a === "string" && pa && typeof b === "string" && pb) return interpolateVectorColor(a, b, t);
|
|
3491
|
+
if (!pa && typeof b === "string") return interpolateVectorColor("transparent", b, t);
|
|
3492
|
+
if (!pb && typeof a === "string") return interpolateVectorColor(a, "transparent", t);
|
|
3493
|
+
const ga = pa && typeof a === "object" && a ? a : null;
|
|
3494
|
+
const gb = pb && typeof b === "object" && b ? b : null;
|
|
3495
|
+
if (ga && gb) return lerpGradient(ga, gb, t) ?? (t < 0.5 ? a : b);
|
|
3496
|
+
if (gb) return lerpGradient(solidLike(pa ? a : null, gb), gb, t) ?? b;
|
|
3497
|
+
if (ga) return lerpGradient(ga, solidLike(pb ? b : null, ga), t) ?? a;
|
|
3498
|
+
return t < 0.5 ? a : b;
|
|
3499
|
+
}
|
|
3500
|
+
function interpolateWidthProfiles(a, b, t) {
|
|
3501
|
+
if (!a?.length && !b?.length) return null;
|
|
3502
|
+
const at2 = [...a ?? [], ...b ?? [], { t: 0 }, { t: 1 }].map((p) => p.t).sort((x, y) => x - y);
|
|
3503
|
+
const ts = at2.filter((v2, i) => i === 0 || v2 - at2[i - 1] > 1e-6);
|
|
3504
|
+
return ts.map((pos) => {
|
|
3505
|
+
const x = widthProfileAt(a, pos);
|
|
3506
|
+
const y = widthProfileAt(b, pos);
|
|
3507
|
+
return { t: pos, left: lerpN(x.left, y.left, t), right: lerpN(x.right, y.right, t) };
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
function lerpDashes(a, b, t) {
|
|
3511
|
+
const dashed = (d) => !!d?.length && d.some((v2) => v2 > 0);
|
|
3512
|
+
if (!dashed(a) && !dashed(b)) return t < 0.5 ? a : b;
|
|
3513
|
+
const even = (p) => p.length % 2 ? [...p, ...p] : p;
|
|
3514
|
+
const solid = (p) => {
|
|
3515
|
+
const e = even(p);
|
|
3516
|
+
const out = [];
|
|
3517
|
+
for (let i = 0; i < e.length; i += 2) out.push(e[i] + e[i + 1], 0);
|
|
3518
|
+
return out;
|
|
3519
|
+
};
|
|
3520
|
+
const x = dashed(a) ? even(a) : solid(b);
|
|
3521
|
+
const y = dashed(b) ? even(b) : solid(a);
|
|
3522
|
+
if (x.length !== y.length) return t < 0.5 ? a : b;
|
|
3523
|
+
return x.map((v2, i) => lerpN(v2, y[i], t));
|
|
3524
|
+
}
|
|
3525
|
+
var SWITCHED_KEYS = ["strokeLinecap", "strokeLinejoin", "fillRule", "strokeAlign", "startArrow", "endArrow", "arrowAlign"];
|
|
3526
|
+
function interpolateVectorStyle(a, b, t) {
|
|
3527
|
+
const near2 = t < 0.5 ? a : b;
|
|
3528
|
+
const out = {};
|
|
3529
|
+
const fill = interpolateVectorPaint(a.fill, b.fill, t);
|
|
3530
|
+
if (fill !== void 0) out.fill = fill;
|
|
3531
|
+
const stroke = interpolateVectorPaint(a.stroke, b.stroke, t);
|
|
3532
|
+
if (stroke !== void 0) out.stroke = stroke;
|
|
3533
|
+
if (a.strokeWidth !== void 0 || b.strokeWidth !== void 0) out.strokeWidth = lerpN(a.strokeWidth ?? 1, b.strokeWidth ?? 1, t);
|
|
3534
|
+
for (const key of SWITCHED_KEYS) if (near2[key] !== void 0 && near2[key] !== null) out[key] = near2[key];
|
|
3535
|
+
for (const key of ["startArrowScale", "endArrowScale"])
|
|
3536
|
+
if (a[key] !== void 0 || b[key] !== void 0) out[key] = lerpN(a[key] ?? 1, b[key] ?? 1, t);
|
|
3537
|
+
const dash = lerpDashes(a.strokeDasharray, b.strokeDasharray, t);
|
|
3538
|
+
if (dash?.length) out.strokeDasharray = dash;
|
|
3539
|
+
const profile = interpolateWidthProfiles(a.strokeWidthProfile, b.strokeWidthProfile, t);
|
|
3540
|
+
if (profile) out.strokeWidthProfile = profile;
|
|
3541
|
+
return out;
|
|
3542
|
+
}
|
|
3543
|
+
function reversedStyle(s) {
|
|
3544
|
+
const out = { ...s };
|
|
3545
|
+
out.startArrow = s.endArrow;
|
|
3546
|
+
out.endArrow = s.startArrow;
|
|
3547
|
+
out.startArrowScale = s.endArrowScale;
|
|
3548
|
+
out.endArrowScale = s.startArrowScale;
|
|
3549
|
+
if (s.strokeWidthProfile?.length) out.strokeWidthProfile = reverseWidthProfile(s.strokeWidthProfile);
|
|
3550
|
+
for (const k of ["startArrow", "endArrow", "startArrowScale", "endArrowScale"]) if (out[k] === void 0) delete out[k];
|
|
3551
|
+
return out;
|
|
3552
|
+
}
|
|
3553
|
+
var STYLE_KEYS2 = [
|
|
3554
|
+
"fill",
|
|
3555
|
+
"stroke",
|
|
3556
|
+
"strokeWidth",
|
|
3557
|
+
"strokeLinecap",
|
|
3558
|
+
"strokeLinejoin",
|
|
3559
|
+
"strokeDasharray",
|
|
3560
|
+
"fillRule",
|
|
3561
|
+
"strokeAlign",
|
|
3562
|
+
"startArrow",
|
|
3563
|
+
"endArrow",
|
|
3564
|
+
"arrowAlign",
|
|
3565
|
+
"startArrowScale",
|
|
3566
|
+
"endArrowScale",
|
|
3567
|
+
"strokeWidthProfile"
|
|
3568
|
+
];
|
|
3569
|
+
function styleOf(n) {
|
|
3570
|
+
const out = {};
|
|
3571
|
+
for (const k of STYLE_KEYS2) if (n[k] !== void 0) out[k] = n[k];
|
|
3572
|
+
return out;
|
|
3573
|
+
}
|
|
3574
|
+
function contoursOf(node, m) {
|
|
3575
|
+
return expandVectorCorners(node.subpaths).map((sp) => subpathToMorphContour(sp, m)).filter((c) => !!c);
|
|
3576
|
+
}
|
|
3577
|
+
function preparePathMorph(a, b, options, ma = nodeMatrix(a), mb = nodeMatrix(b)) {
|
|
3578
|
+
const corr = correspondMorphContours(contoursOf(a, ma), contoursOf(b, mb), options);
|
|
3579
|
+
const sim = procrustes(corr.a, corr.b);
|
|
3580
|
+
const styleA = styleOf(a);
|
|
3581
|
+
const styleB = corr.reversed ? reversedStyle(styleOf(b)) : styleOf(b);
|
|
3582
|
+
const rigid = !isIdentityMatrix(ma) || !isIdentityMatrix(mb);
|
|
3583
|
+
const id = options.id ?? a.id;
|
|
3584
|
+
return (t) => {
|
|
3585
|
+
const contours = interpolateMorphContours(corr.a, corr.b, t, { linear: options.linear, rigidity: options.rigidity, similarity: sim });
|
|
3586
|
+
const m = rigid ? interpolateRigidMatrix(ma, mb, t) : IDENTITY_MATRIX;
|
|
3587
|
+
const back = rigid ? invertMatrix(m) : IDENTITY_MATRIX;
|
|
3588
|
+
const subpaths = contours.map((c, k) => {
|
|
3589
|
+
const closed = t <= 0 ? corr.a[k].closed : t >= 1 ? corr.b[k].closed : corr.a[k].closed && corr.b[k].closed;
|
|
3590
|
+
return morphContourToSubpath({ closed: corr.a[k].closed && corr.b[k].closed, pts: c.pts }, back, closed);
|
|
3591
|
+
});
|
|
3592
|
+
const node = { id, type: "path", subpaths, ...interpolateVectorStyle(styleA, styleB, t) };
|
|
3593
|
+
const opacity = lerpN(a.opacity ?? 1, b.opacity ?? 1, t);
|
|
3594
|
+
if (opacity < 1) node.opacity = opacity;
|
|
3595
|
+
if (rigid && !isIdentityMatrix(m)) node.transform = m;
|
|
3596
|
+
return node;
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
function flattenPaths(node, parent = IDENTITY_MATRIX) {
|
|
3600
|
+
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
3601
|
+
if (node.type === "path") return [{ path: node, m, opacity: 1 }];
|
|
3602
|
+
const o = node.opacity ?? 1;
|
|
3603
|
+
return node.children.filter((c) => c.visible !== false).flatMap((c) => flattenPaths(c, m)).map((e) => ({ ...e, opacity: e.opacity * o }));
|
|
3604
|
+
}
|
|
3605
|
+
var placedBounds = (path, m) => boundsOfPoints(contoursOf(path, m).flatMap((c) => c.pts));
|
|
3606
|
+
function collapsedPath(path, m, from, to) {
|
|
3607
|
+
const own = placedBounds(path, m);
|
|
3608
|
+
const c = own ? [own.x + own.width / 2, own.y + own.height / 2] : [0, 0];
|
|
3609
|
+
const fx = from && from.width > 0 ? (c[0] - from.x) / from.width : 0.5;
|
|
3610
|
+
const fy = from && from.height > 0 ? (c[1] - from.y) / from.height : 0.5;
|
|
3611
|
+
const p = to ? { x: to.x + to.width * fx, y: to.y + to.height * fy } : { x: c[0], y: c[1] };
|
|
3612
|
+
const subpaths = path.subpaths.map((sp) => ({ closed: sp.closed, anchors: sp.anchors.map(() => ({ x: p.x, y: p.y })) }));
|
|
3613
|
+
const { transform: _t, ...rest } = path;
|
|
3614
|
+
return { ...rest, subpaths };
|
|
3615
|
+
}
|
|
3616
|
+
function createVectorMorph(a, b, options = {}) {
|
|
3617
|
+
if (a.type === "path" && b.type === "path") {
|
|
3618
|
+
const at2 = preparePathMorph(a, b, options);
|
|
3619
|
+
return { at: at2 };
|
|
3620
|
+
}
|
|
3621
|
+
const pa = flattenPaths(a);
|
|
3622
|
+
const pb = flattenPaths(b);
|
|
3623
|
+
const ba = unionBounds(pa.map((e) => placedBounds(e.path, e.m)));
|
|
3624
|
+
const bb = unionBounds(pb.map((e) => placedBounds(e.path, e.m)));
|
|
3625
|
+
const id = options.id ?? a.id;
|
|
3626
|
+
const parts = [];
|
|
3627
|
+
const opacities = [];
|
|
3628
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
3629
|
+
const x = pa[i];
|
|
3630
|
+
const y = pb[i];
|
|
3631
|
+
const sub3 = { ...options, id: `${id}-${i}`, starts: void 0 };
|
|
3632
|
+
if (x && y) {
|
|
3633
|
+
parts.push(preparePathMorph(x.path, y.path, sub3, x.m, y.m));
|
|
3634
|
+
opacities.push([x.opacity, y.opacity]);
|
|
3635
|
+
} else if (x) {
|
|
3636
|
+
parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub3, x.m, IDENTITY_MATRIX));
|
|
3637
|
+
opacities.push([x.opacity, x.opacity]);
|
|
3638
|
+
} else if (y) {
|
|
3639
|
+
parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub3, IDENTITY_MATRIX, y.m));
|
|
3640
|
+
opacities.push([y.opacity, y.opacity]);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
return {
|
|
3644
|
+
at: (t) => {
|
|
3645
|
+
const children = parts.map((p, i) => {
|
|
3646
|
+
const n = p(t);
|
|
3647
|
+
const o = lerpN(opacities[i][0], opacities[i][1], t) * (n.opacity ?? 1);
|
|
3648
|
+
if (o < 1) n.opacity = o;
|
|
3649
|
+
else delete n.opacity;
|
|
3650
|
+
return n;
|
|
3651
|
+
});
|
|
3652
|
+
const group = { id, type: "group", children };
|
|
3653
|
+
return group;
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
function interpolateVectorPaths(a, b, t, options = {}) {
|
|
3658
|
+
return createVectorMorph(a, b, options).at(t);
|
|
3659
|
+
}
|
|
3660
|
+
var keyframeMorphs = /* @__PURE__ */ new WeakMap();
|
|
3661
|
+
function evaluateVectorPathKeyframes(keyframes, time, options = {}) {
|
|
3662
|
+
const keys = keyframes.filter((k) => Number.isFinite(k.time)).sort((x, y) => x.time - y.time);
|
|
3663
|
+
if (!keys.length) return null;
|
|
3664
|
+
if (!(time > keys[0].time)) return keys[0].value;
|
|
3665
|
+
const last = keys[keys.length - 1];
|
|
3666
|
+
if (time >= last.time) return last.value;
|
|
3667
|
+
let i = 0;
|
|
3668
|
+
while (i + 1 < keys.length && keys[i + 1].time <= time) i++;
|
|
3669
|
+
const a = keys[i];
|
|
3670
|
+
const b = keys[i + 1];
|
|
3671
|
+
if (time === a.time) return a.value;
|
|
3672
|
+
if (a.easing === "hold") return a.value;
|
|
3673
|
+
const span = b.time - a.time;
|
|
3674
|
+
const u = Math.min(1, Math.max(0, evaluateEasing(a.easing, (time - a.time) / span, span)));
|
|
3675
|
+
let inner = keyframeMorphs.get(a.value);
|
|
3676
|
+
if (!inner) keyframeMorphs.set(a.value, inner = /* @__PURE__ */ new WeakMap());
|
|
3677
|
+
let morph = inner.get(b.value);
|
|
3678
|
+
if (!morph) inner.set(b.value, morph = createVectorMorph(a.value, b.value, options));
|
|
3679
|
+
return morph.at(u);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// src/vector/blendGroup.ts
|
|
3683
|
+
var DEFAULT_VECTOR_BLEND_STEPS = 5;
|
|
3684
|
+
var DEFAULT_VECTOR_BLEND_DISTANCE = 20;
|
|
3685
|
+
var VECTOR_BLEND_MAX_STEPS = 1e3;
|
|
3686
|
+
var VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;
|
|
3687
|
+
function isVectorBlendGroup(node) {
|
|
3688
|
+
return !!node && node.type === "group" && !!node.blend;
|
|
3689
|
+
}
|
|
3690
|
+
function vectorBlendKeys(group) {
|
|
3691
|
+
return group.children.filter((c) => !c.blendStep);
|
|
3692
|
+
}
|
|
3693
|
+
function resolveVectorBlendOptions(options) {
|
|
3694
|
+
return {
|
|
3695
|
+
spacing: options?.spacing ?? "steps",
|
|
3696
|
+
steps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,
|
|
3697
|
+
distance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,
|
|
3698
|
+
orientToSpine: options?.orientToSpine ?? false,
|
|
3699
|
+
linear: options?.linear ?? false
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function paintColors(p) {
|
|
3703
|
+
if (typeof p === "string") return [p];
|
|
3704
|
+
if (p && typeof p === "object") return p.stops.map((s) => s.color);
|
|
3705
|
+
return [];
|
|
3706
|
+
}
|
|
3707
|
+
function vectorBlendColorDistance(a, b) {
|
|
3708
|
+
const pa = collectPathNodes(a)[0];
|
|
3709
|
+
const pb = collectPathNodes(b)[0];
|
|
3710
|
+
if (!pa || !pb) return 0;
|
|
3711
|
+
let d = 0;
|
|
3712
|
+
for (const key of ["fill", "stroke"]) {
|
|
3713
|
+
const ca = paintColors(pa[key]);
|
|
3714
|
+
const cb = paintColors(pb[key]);
|
|
3715
|
+
ca.forEach((c, i) => {
|
|
3716
|
+
const other = cb[i] ?? cb[0];
|
|
3717
|
+
if (other === void 0) return;
|
|
3718
|
+
const x = hexToRgba(c);
|
|
3719
|
+
const y = hexToRgba(other);
|
|
3720
|
+
if (!x || !y) return;
|
|
3721
|
+
d = Math.max(d, Math.abs(x.r - y.r), Math.abs(x.g - y.g), Math.abs(x.b - y.b), Math.abs(x.a - y.a));
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
return d;
|
|
3725
|
+
}
|
|
3726
|
+
var centreOf = (n) => {
|
|
3727
|
+
const b = vectorNodeBounds(n);
|
|
3728
|
+
return b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];
|
|
3729
|
+
};
|
|
3730
|
+
function vectorBlendStepCount(a, b, options, length2) {
|
|
3731
|
+
const o = resolveVectorBlendOptions(options);
|
|
3732
|
+
if (o.spacing === "distance") {
|
|
3733
|
+
const d = Math.max(0.01, o.distance);
|
|
3734
|
+
return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length2 / d) - 1));
|
|
3735
|
+
}
|
|
3736
|
+
if (o.spacing === "smoothColor") {
|
|
3737
|
+
const cd = vectorBlendColorDistance(a, b);
|
|
3738
|
+
if (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(cd * 255 / 2)));
|
|
3739
|
+
const ba = vectorNodeBounds(a);
|
|
3740
|
+
const bb = vectorNodeBounds(b);
|
|
3741
|
+
const dist4 = ba && bb ? Math.max(Math.abs(ba.x - bb.x), Math.abs(ba.y - bb.y), Math.abs(ba.x + ba.width - bb.x - bb.width), Math.abs(ba.y + ba.height - bb.y - bb.height)) : length2;
|
|
3742
|
+
return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist4 / 2)));
|
|
3743
|
+
}
|
|
3744
|
+
return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));
|
|
3745
|
+
}
|
|
3746
|
+
function vectorBlendRail(sp) {
|
|
3747
|
+
const pts = [];
|
|
3748
|
+
const angles = [];
|
|
3749
|
+
const push = (p, tangent2) => {
|
|
3750
|
+
const last = pts[pts.length - 1];
|
|
3751
|
+
if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;
|
|
3752
|
+
pts.push(p);
|
|
3753
|
+
angles.push(Math.atan2(tangent2[1], tangent2[0]));
|
|
3754
|
+
};
|
|
3755
|
+
const count = subpathSegmentCount(sp);
|
|
3756
|
+
for (let i = 0; i < count; i++) {
|
|
3757
|
+
const seg = subpathSegment(sp, i);
|
|
3758
|
+
const samples = seg.length === 2 ? 1 : 32;
|
|
3759
|
+
if (i === 0) push(seg[0], segmentTangent(seg, 0));
|
|
3760
|
+
for (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));
|
|
3761
|
+
}
|
|
3762
|
+
if (pts.length < 2) return null;
|
|
3763
|
+
const cum = [0];
|
|
3764
|
+
for (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));
|
|
3765
|
+
return { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };
|
|
3766
|
+
}
|
|
3767
|
+
function vectorBlendRailAt(rail, s) {
|
|
3768
|
+
let len2 = Number.isFinite(s) ? s : 0;
|
|
3769
|
+
if (rail.closed && rail.total > 0) len2 = (len2 % rail.total + rail.total) % rail.total;
|
|
3770
|
+
len2 = Math.min(rail.total, Math.max(0, len2));
|
|
3771
|
+
let lo = 0;
|
|
3772
|
+
let hi = rail.cum.length - 1;
|
|
3773
|
+
while (hi - lo > 1) {
|
|
3774
|
+
const mid = lo + hi >> 1;
|
|
3775
|
+
if (rail.cum[mid] <= len2) lo = mid;
|
|
3776
|
+
else hi = mid;
|
|
3777
|
+
}
|
|
3778
|
+
const span = rail.cum[hi] - rail.cum[lo];
|
|
3779
|
+
const u = span > 1e-12 ? (len2 - rail.cum[lo]) / span : 0;
|
|
3780
|
+
const a = rail.pts[lo];
|
|
3781
|
+
const b = rail.pts[hi];
|
|
3782
|
+
return { point: [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u], angle: Math.atan2(b[1] - a[1], b[0] - a[0]) };
|
|
3783
|
+
}
|
|
3784
|
+
function vectorBlendSpine(group) {
|
|
3785
|
+
const keys = vectorBlendKeys(group);
|
|
3786
|
+
if (keys.length < 2) return null;
|
|
3787
|
+
const stored = group.blend?.spine;
|
|
3788
|
+
if (stored && stored.anchors.length >= 2) {
|
|
3789
|
+
const rail2 = vectorBlendRail(stored);
|
|
3790
|
+
if (rail2) {
|
|
3791
|
+
const end = rail2.closed ? rail2.total * (1 - 1 / keys.length) : rail2.total;
|
|
3792
|
+
return { spine: stored, rail: rail2, stops: keys.map((_, i) => end * i / (keys.length - 1)), replaced: true };
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
const centres = keys.map(centreOf);
|
|
3796
|
+
const spine = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };
|
|
3797
|
+
const stops = [0];
|
|
3798
|
+
for (let i = 1; i < centres.length; i++) stops.push(stops[i - 1] + Math.hypot(centres[i][0] - centres[i - 1][0], centres[i][1] - centres[i - 1][1]));
|
|
3799
|
+
const rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };
|
|
3800
|
+
return { spine, rail, stops, replaced: false };
|
|
3801
|
+
}
|
|
3802
|
+
function vectorBlendKeyTargets(group) {
|
|
3803
|
+
const s = vectorBlendSpine(group);
|
|
3804
|
+
return s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];
|
|
3805
|
+
}
|
|
3806
|
+
var morphCache = /* @__PURE__ */ new WeakMap();
|
|
3807
|
+
function morphFor(a, b, blend) {
|
|
3808
|
+
const starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null];
|
|
3809
|
+
const key = JSON.stringify([!!blend.linear, starts]);
|
|
3810
|
+
let inner = morphCache.get(a);
|
|
3811
|
+
if (!inner) morphCache.set(a, inner = /* @__PURE__ */ new WeakMap());
|
|
3812
|
+
let byKey = inner.get(b);
|
|
3813
|
+
if (!byKey) inner.set(b, byKey = /* @__PURE__ */ new Map());
|
|
3814
|
+
let morph = byKey.get(key);
|
|
3815
|
+
if (!morph) byKey.set(key, morph = createVectorMorph(a, b, { linear: blend.linear, starts }));
|
|
3816
|
+
return morph;
|
|
3817
|
+
}
|
|
3818
|
+
var wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
|
|
3819
|
+
function asStep(node, id) {
|
|
3820
|
+
const rename = (n, nid) => n.type === "group" ? { ...n, id: nid, children: n.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n, id: nid };
|
|
3821
|
+
return { ...rename(node, id), blendStep: true, locked: true };
|
|
3822
|
+
}
|
|
3823
|
+
function generateVectorBlendChildren(group) {
|
|
3824
|
+
const keys = vectorBlendKeys(group);
|
|
3825
|
+
const blend = group.blend;
|
|
3826
|
+
if (!blend || keys.length < 2) return keys;
|
|
3827
|
+
const spine = vectorBlendSpine(group);
|
|
3828
|
+
if (!spine) return keys;
|
|
3829
|
+
const out = [];
|
|
3830
|
+
let counter = 0;
|
|
3831
|
+
keys.forEach((a, i) => {
|
|
3832
|
+
out.push(a);
|
|
3833
|
+
const b = keys[i + 1];
|
|
3834
|
+
if (!b) return;
|
|
3835
|
+
const s0 = spine.stops[i];
|
|
3836
|
+
const s1 = spine.stops[i + 1];
|
|
3837
|
+
const n = vectorBlendStepCount(a, b, blend, Math.abs(s1 - s0));
|
|
3838
|
+
const morph = morphFor(a, b, blend);
|
|
3839
|
+
const p0 = vectorBlendRailAt(spine.rail, s0);
|
|
3840
|
+
const p1 = vectorBlendRailAt(spine.rail, s1);
|
|
3841
|
+
for (let j = 1; j <= n; j++) {
|
|
3842
|
+
const t = j / (n + 1);
|
|
3843
|
+
let step = morph.at(t);
|
|
3844
|
+
if (spine.replaced) {
|
|
3845
|
+
const p = vectorBlendRailAt(spine.rail, s0 + (s1 - s0) * t);
|
|
3846
|
+
const dx = p.point[0] - (p0.point[0] + (p1.point[0] - p0.point[0]) * t);
|
|
3847
|
+
const dy = p.point[1] - (p0.point[1] + (p1.point[1] - p0.point[1]) * t);
|
|
3848
|
+
let m = Math.hypot(dx, dy) > 1e-9 ? translateMatrix(dx, dy) : IDENTITY_MATRIX;
|
|
3849
|
+
if (blend.orientToSpine) {
|
|
3850
|
+
const turn = wrapAngle(p.angle - (p0.angle + wrapAngle(p1.angle - p0.angle) * t));
|
|
3851
|
+
if (Math.abs(turn) > 1e-9) {
|
|
3852
|
+
const c = centreOf(step);
|
|
3853
|
+
const origin = { x: c[0] + m[4], y: c[1] + m[5] };
|
|
3854
|
+
m = multiplyMatrices(m, rotateMatrix(turn * 180 / Math.PI, origin));
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
if (m !== IDENTITY_MATRIX) step = transformVectorNode(step, m);
|
|
3858
|
+
}
|
|
3859
|
+
out.push(asStep(step, `${group.id}~${++counter}`));
|
|
3860
|
+
}
|
|
3861
|
+
});
|
|
3862
|
+
return out;
|
|
3863
|
+
}
|
|
3864
|
+
var childrenCache = /* @__PURE__ */ new WeakMap();
|
|
3865
|
+
var sameList = (a, b) => a.length === b.length && a.every((n, i) => n === b[i]);
|
|
3866
|
+
function blendChildren(group) {
|
|
3867
|
+
const keys = vectorBlendKeys(group);
|
|
3868
|
+
const sig = `${group.id}|${JSON.stringify(group.blend)}`;
|
|
3869
|
+
const cached = keys[0] ? childrenCache.get(keys[0]) : void 0;
|
|
3870
|
+
let children;
|
|
3871
|
+
if (cached && cached.sig === sig && sameList(cached.keys, keys)) children = cached.children;
|
|
3872
|
+
else {
|
|
3873
|
+
children = generateVectorBlendChildren(group);
|
|
3874
|
+
if (keys[0]) childrenCache.set(keys[0], { sig, keys, children });
|
|
3875
|
+
}
|
|
3876
|
+
return sameList(children, group.children) ? group.children : children;
|
|
3877
|
+
}
|
|
3878
|
+
function refreshVectorBlends(nodes) {
|
|
3879
|
+
let changed = false;
|
|
3880
|
+
const out = nodes.map((n) => {
|
|
3881
|
+
if (n.type !== "group") return n;
|
|
3882
|
+
const inner = refreshVectorBlends(n.children);
|
|
3883
|
+
let next = inner === n.children ? n : { ...n, children: inner };
|
|
3884
|
+
if (next.blend) {
|
|
3885
|
+
const children = blendChildren(next);
|
|
3886
|
+
if (children !== next.children) next = { ...next, children };
|
|
3887
|
+
}
|
|
3888
|
+
if (next !== n) changed = true;
|
|
3889
|
+
return next;
|
|
3890
|
+
});
|
|
3891
|
+
return changed ? out : nodes;
|
|
3892
|
+
}
|
|
3893
|
+
function plain2(node, newId) {
|
|
3894
|
+
const { blendStep: _s, locked: _l, ...rest } = node;
|
|
3895
|
+
const renamed = { ...rest, id: newId() };
|
|
3896
|
+
if (renamed.type === "group") renamed.children = renamed.children.map((c) => plain2(c, newId));
|
|
3897
|
+
return renamed;
|
|
3898
|
+
}
|
|
3899
|
+
function expandVectorBlend(group, newId) {
|
|
3900
|
+
const { blend: _b, ...rest } = group;
|
|
3901
|
+
return { ...rest, children: group.children.map((c) => c.blendStep ? plain2(c, newId) : c) };
|
|
3902
|
+
}
|
|
3903
|
+
function releaseVectorBlend(group, newId) {
|
|
3904
|
+
const m = nodeMatrix(group);
|
|
3905
|
+
const opacity = group.opacity;
|
|
3906
|
+
const keys = vectorBlendKeys(group).map((c) => group.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c);
|
|
3907
|
+
const spine = group.blend?.spine;
|
|
3908
|
+
if (!spine || spine.anchors.length < 2) return keys;
|
|
3909
|
+
const path = { id: newId(), type: "path", name: "Spine", fill: null, stroke: null, subpaths: [{ closed: spine.closed, anchors: spine.anchors.map((a) => ({ ...a })) }] };
|
|
3910
|
+
return [...keys, group.transform ? transformVectorNode(path, m) : path];
|
|
3911
|
+
}
|
|
3912
|
+
|
|
2032
3913
|
// src/vector/snap.ts
|
|
2033
3914
|
var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
|
|
2034
3915
|
function constrainToLeave(leave, p) {
|
|
@@ -2242,8 +4123,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
2242
4123
|
let anchors = BUDGET;
|
|
2243
4124
|
let segments = BUDGET;
|
|
2244
4125
|
const visit = (list, parent) => {
|
|
2245
|
-
for (const
|
|
2246
|
-
if (exclude.has(
|
|
4126
|
+
for (const stored of list) {
|
|
4127
|
+
if (exclude.has(stored.id) || stored.visible === false) continue;
|
|
4128
|
+
const n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);
|
|
2247
4129
|
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
2248
4130
|
if (n.type === "group") {
|
|
2249
4131
|
visit(n.children, m);
|
|
@@ -2558,14 +4440,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
2558
4440
|
* corner) slides along it until one of them is on a target. An axis that snaps to nothing
|
|
2559
4441
|
* may match another object's width or height instead.
|
|
2560
4442
|
*/
|
|
2561
|
-
snapScale(
|
|
2562
|
-
const toDoc =
|
|
2563
|
-
const toLocal =
|
|
4443
|
+
snapScale(frame2, handle, sx, sy, mods, tol) {
|
|
4444
|
+
const toDoc = frame2.angle ? rotateMatrix(frame2.angle) : IDENTITY_MATRIX;
|
|
4445
|
+
const toLocal = frame2.angle ? rotateMatrix(-frame2.angle) : IDENTITY_MATRIX;
|
|
2564
4446
|
const corner = handle.length === 2;
|
|
2565
4447
|
const proportional = !!mods.shift;
|
|
2566
|
-
const centre = { x:
|
|
2567
|
-
const origin = mods.alt ? centre : handlePos(opposite(handle),
|
|
2568
|
-
const hp = handlePos(handle,
|
|
4448
|
+
const centre = { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 };
|
|
4449
|
+
const origin = mods.alt ? centre : handlePos(opposite(handle), frame2);
|
|
4450
|
+
const hp = handlePos(handle, frame2);
|
|
2569
4451
|
const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
|
|
2570
4452
|
const valid = (n, s) => Number.isFinite(n) && n * s > 0;
|
|
2571
4453
|
const nearest = (v2, lines) => {
|
|
@@ -2591,7 +4473,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
2591
4473
|
const q = applyMatrix(toLocal, { x: tx ?? p.x, y: ty ?? p.y });
|
|
2592
4474
|
let cx = (q.x - origin.x) / d0.x;
|
|
2593
4475
|
let cy = (q.y - origin.y) / d0.y;
|
|
2594
|
-
if (!
|
|
4476
|
+
if (!frame2.angle) {
|
|
2595
4477
|
if (tx === null) cx = sx;
|
|
2596
4478
|
if (ty === null) cy = sy;
|
|
2597
4479
|
}
|
|
@@ -2620,7 +4502,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
2620
4502
|
if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
|
|
2621
4503
|
};
|
|
2622
4504
|
for (const h of [handle, ...ends]) {
|
|
2623
|
-
const hv = handlePos(h,
|
|
4505
|
+
const hv = handlePos(h, frame2);
|
|
2624
4506
|
const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
|
|
2625
4507
|
const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
|
|
2626
4508
|
const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
|
|
@@ -2655,25 +4537,25 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
2655
4537
|
const movesY = handle !== "e" && handle !== "w";
|
|
2656
4538
|
const sized = [];
|
|
2657
4539
|
if (!proportional) {
|
|
2658
|
-
if (movesX && !snap.hx && !(
|
|
2659
|
-
const len2 = this.matchSize(Math.abs(out.sx) *
|
|
4540
|
+
if (movesX && !snap.hx && !(frame2.angle && snap.hy) && frame2.width > 1e-9) {
|
|
4541
|
+
const len2 = this.matchSize(Math.abs(out.sx) * frame2.width, tol);
|
|
2660
4542
|
if (len2 !== null && len2 > 0) {
|
|
2661
|
-
out.sx = Math.sign(out.sx || 1) * (len2 /
|
|
4543
|
+
out.sx = Math.sign(out.sx || 1) * (len2 / frame2.width);
|
|
2662
4544
|
sized.push("x");
|
|
2663
4545
|
}
|
|
2664
4546
|
}
|
|
2665
|
-
if (movesY && !snap.hy && !(
|
|
2666
|
-
const len2 = this.matchSize(Math.abs(out.sy) *
|
|
4547
|
+
if (movesY && !snap.hy && !(frame2.angle && snap.hx) && frame2.height > 1e-9) {
|
|
4548
|
+
const len2 = this.matchSize(Math.abs(out.sy) * frame2.height, tol);
|
|
2667
4549
|
if (len2 !== null && len2 > 0) {
|
|
2668
|
-
out.sy = Math.sign(out.sy || 1) * (len2 /
|
|
4550
|
+
out.sy = Math.sign(out.sy || 1) * (len2 / frame2.height);
|
|
2669
4551
|
sized.push("y");
|
|
2670
4552
|
}
|
|
2671
4553
|
}
|
|
2672
4554
|
}
|
|
2673
|
-
const x0 = origin.x + (
|
|
2674
|
-
const x1 = origin.x + (
|
|
2675
|
-
const y0 = origin.y + (
|
|
2676
|
-
const y1 = origin.y + (
|
|
4555
|
+
const x0 = origin.x + (frame2.x - origin.x) * out.sx;
|
|
4556
|
+
const x1 = origin.x + (frame2.x + frame2.width - origin.x) * out.sx;
|
|
4557
|
+
const y0 = origin.y + (frame2.y - origin.y) * out.sy;
|
|
4558
|
+
const y1 = origin.y + (frame2.y + frame2.height - origin.y) * out.sy;
|
|
2677
4559
|
const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
|
|
2678
4560
|
const page = [
|
|
2679
4561
|
{ x: box.x0, y: box.y0 },
|
|
@@ -2781,8 +4663,8 @@ function solveOnSegment(seg, base, cond) {
|
|
|
2781
4663
|
}
|
|
2782
4664
|
|
|
2783
4665
|
// src/vector/pathEdit.ts
|
|
2784
|
-
var
|
|
2785
|
-
var
|
|
4666
|
+
var EPS4 = 1e-9;
|
|
4667
|
+
var unit3 = (x, y) => {
|
|
2786
4668
|
const l = Math.hypot(x, y);
|
|
2787
4669
|
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
2788
4670
|
};
|
|
@@ -2793,16 +4675,16 @@ function cubicOf(sp, i) {
|
|
|
2793
4675
|
return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
|
|
2794
4676
|
}
|
|
2795
4677
|
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
2796
|
-
function
|
|
4678
|
+
function startTangent2(c) {
|
|
2797
4679
|
for (let k = 1; k < 4; k++) {
|
|
2798
|
-
const u =
|
|
4680
|
+
const u = unit3(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
2799
4681
|
if (u) return u;
|
|
2800
4682
|
}
|
|
2801
4683
|
return [1, 0];
|
|
2802
4684
|
}
|
|
2803
|
-
function
|
|
4685
|
+
function endTangent2(c) {
|
|
2804
4686
|
for (let k = 2; k >= 0; k--) {
|
|
2805
|
-
const u =
|
|
4687
|
+
const u = unit3(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
2806
4688
|
if (u) return u;
|
|
2807
4689
|
}
|
|
2808
4690
|
return [1, 0];
|
|
@@ -2810,7 +4692,7 @@ function endTangent(c) {
|
|
|
2810
4692
|
function sample(c, n, out, withStart) {
|
|
2811
4693
|
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
|
|
2812
4694
|
}
|
|
2813
|
-
var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) <
|
|
4695
|
+
var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS4 && Math.abs(h.y - a.y) < EPS4;
|
|
2814
4696
|
function tidy(a, keepType = true) {
|
|
2815
4697
|
const next = cloneAnchor(a);
|
|
2816
4698
|
if (onAnchor2(next, next.in)) delete next.in;
|
|
@@ -2872,8 +4754,8 @@ function removeAnchorKeepingShape(sp, index) {
|
|
|
2872
4754
|
const pts = [];
|
|
2873
4755
|
sample(left, N, pts, true);
|
|
2874
4756
|
sample(right, N, pts, false);
|
|
2875
|
-
const t0 =
|
|
2876
|
-
const t1 =
|
|
4757
|
+
const t0 = startTangent2(left);
|
|
4758
|
+
const t1 = endTangent2(right);
|
|
2877
4759
|
const { curve } = fitAcross(pts, N, t0, t1);
|
|
2878
4760
|
if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
|
|
2879
4761
|
anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
|
|
@@ -3074,7 +4956,7 @@ function smoothSubpath(sp, amount = 1, only) {
|
|
|
3074
4956
|
const prev = orig[(i + n - 1) % n];
|
|
3075
4957
|
const next = orig[(i + 1) % n];
|
|
3076
4958
|
const p = orig[i];
|
|
3077
|
-
const dir =
|
|
4959
|
+
const dir = unit3(next.x - prev.x, next.y - prev.y);
|
|
3078
4960
|
if (!dir) continue;
|
|
3079
4961
|
const li = dist2(p, prev) / 3;
|
|
3080
4962
|
const lo = dist2(p, next) / 3;
|
|
@@ -3140,13 +5022,13 @@ function mergePair(a, b, tol) {
|
|
|
3140
5022
|
}
|
|
3141
5023
|
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
3142
5024
|
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
3143
|
-
const ta =
|
|
3144
|
-
const tb =
|
|
5025
|
+
const ta = endTangent2(ca);
|
|
5026
|
+
const tb = startTangent2(cb);
|
|
3145
5027
|
if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
|
|
3146
5028
|
const pts = [];
|
|
3147
5029
|
sample(ca, 16, pts, true);
|
|
3148
5030
|
sample(cb, 16, pts, false);
|
|
3149
|
-
const { curve, error } = fitAcross(pts, 16,
|
|
5031
|
+
const { curve, error } = fitAcross(pts, 16, startTangent2(ca), endTangent2(cb));
|
|
3150
5032
|
return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
|
|
3151
5033
|
}
|
|
3152
5034
|
function removeRedundantPoints(sp, tolerance) {
|
|
@@ -3278,14 +5160,14 @@ function lineHits(p, q, seg) {
|
|
|
3278
5160
|
return out;
|
|
3279
5161
|
}
|
|
3280
5162
|
var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
|
|
3281
|
-
var
|
|
5163
|
+
var degenerate2 = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
|
|
3282
5164
|
function arc(l, a, b, eps) {
|
|
3283
5165
|
const n = l.segs.length;
|
|
3284
5166
|
const segs = [];
|
|
3285
5167
|
const chord = [];
|
|
3286
5168
|
const push = (k, t0, t1) => {
|
|
3287
5169
|
const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
|
|
3288
|
-
if (!
|
|
5170
|
+
if (!degenerate2(piece, eps)) {
|
|
3289
5171
|
segs.push(piece);
|
|
3290
5172
|
chord.push(l.chord[k]);
|
|
3291
5173
|
}
|
|
@@ -3743,8 +5625,9 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3743
5625
|
// src/vector/builder.ts
|
|
3744
5626
|
function vectorPathEntries(doc, roots, options = {}) {
|
|
3745
5627
|
const out = [];
|
|
3746
|
-
const visit = (
|
|
3747
|
-
if (
|
|
5628
|
+
const visit = (stored, parent, inside) => {
|
|
5629
|
+
if (stored.visible === false || stored.locked && !options.locked) return;
|
|
5630
|
+
const node = options.expand === false ? stored : expandVectorAppearance(stored);
|
|
3748
5631
|
const picked = inside || roots === null || roots.includes(node.id);
|
|
3749
5632
|
if (node.type === "group") {
|
|
3750
5633
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
@@ -3798,7 +5681,7 @@ var STROKE_KEYS = [
|
|
|
3798
5681
|
"strokeWidthProfile"
|
|
3799
5682
|
];
|
|
3800
5683
|
function vectorResultStyle(node, paint, edge = false) {
|
|
3801
|
-
const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
|
|
5684
|
+
const { id: _id, subpaths: _s, transform: _t, effects: _e, booleanOperation: _b, ...rest } = node;
|
|
3802
5685
|
const base = { ...rest, fillRule: "nonzero" };
|
|
3803
5686
|
if (edge) {
|
|
3804
5687
|
for (const k of STROKE_KEYS) delete base[k];
|
|
@@ -3962,6 +5845,15 @@ var DEFAULT_VECTOR_PATH_EDIT = {
|
|
|
3962
5845
|
fillGap: 0
|
|
3963
5846
|
};
|
|
3964
5847
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
5848
|
+
function blendOptionsOf(input) {
|
|
5849
|
+
const out = {};
|
|
5850
|
+
if (input.spacing === "steps" || input.spacing === "distance" || input.spacing === "smoothColor") out.spacing = input.spacing;
|
|
5851
|
+
if (typeof input.steps === "number" && Number.isFinite(input.steps)) out.steps = Math.min(1e3, Math.max(1, Math.round(input.steps)));
|
|
5852
|
+
if (typeof input.distance === "number" && Number.isFinite(input.distance) && input.distance > 0) out.distance = input.distance;
|
|
5853
|
+
if (typeof input.orientToSpine === "boolean") out.orientToSpine = input.orientToSpine;
|
|
5854
|
+
if (typeof input.linear === "boolean") out.linear = input.linear;
|
|
5855
|
+
return out;
|
|
5856
|
+
}
|
|
3965
5857
|
var VECTOR_STYLE_KEYS = [
|
|
3966
5858
|
"fill",
|
|
3967
5859
|
"stroke",
|
|
@@ -4168,11 +6060,11 @@ function getTransformHandlePositions(box) {
|
|
|
4168
6060
|
{ handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
|
|
4169
6061
|
];
|
|
4170
6062
|
}
|
|
4171
|
-
function getVectorSelectionBoxGeometry(
|
|
6063
|
+
function getVectorSelectionBoxGeometry(frame2, viewport) {
|
|
4172
6064
|
const z = viewport.zoom;
|
|
4173
|
-
const box = { x:
|
|
4174
|
-
const rot = rotateMatrix(
|
|
4175
|
-
const toScreen2 = (p) =>
|
|
6065
|
+
const box = { x: frame2.x * z + viewport.x, y: frame2.y * z + viewport.y, width: frame2.width * z, height: frame2.height * z };
|
|
6066
|
+
const rot = rotateMatrix(frame2.angle, { x: viewport.x, y: viewport.y });
|
|
6067
|
+
const toScreen2 = (p) => frame2.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
|
|
4176
6068
|
const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
|
|
4177
6069
|
const corners2 = [
|
|
4178
6070
|
{ x: box.x, y: box.y },
|
|
@@ -4181,7 +6073,7 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
|
|
|
4181
6073
|
{ x: box.x, y: box.y + box.height }
|
|
4182
6074
|
].map(toScreen2);
|
|
4183
6075
|
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
4184
|
-
return { box, angle:
|
|
6076
|
+
return { box, angle: frame2.angle, transform: frame2.angle ? `rotate(${r(frame2.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
|
|
4185
6077
|
}
|
|
4186
6078
|
var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
|
|
4187
6079
|
var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
|
|
@@ -4205,7 +6097,8 @@ var TOOL_LABELS = {
|
|
|
4205
6097
|
eraser: "Eraser",
|
|
4206
6098
|
width: "Width",
|
|
4207
6099
|
shapeBuilder: "Shape Builder",
|
|
4208
|
-
paintBucket: "Paint Bucket"
|
|
6100
|
+
paintBucket: "Paint Bucket",
|
|
6101
|
+
blend: "Blend"
|
|
4209
6102
|
};
|
|
4210
6103
|
var TOOL_KEYS = {
|
|
4211
6104
|
v: "select",
|
|
@@ -4222,7 +6115,8 @@ var TOOL_KEYS = {
|
|
|
4222
6115
|
e: "eraser",
|
|
4223
6116
|
w: "width",
|
|
4224
6117
|
m: "shapeBuilder",
|
|
4225
|
-
b: "paintBucket"
|
|
6118
|
+
b: "paintBucket",
|
|
6119
|
+
j: "blend"
|
|
4226
6120
|
};
|
|
4227
6121
|
function vectorToolLabel(tool) {
|
|
4228
6122
|
return TOOL_LABELS[tool];
|
|
@@ -4242,7 +6136,8 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
4242
6136
|
eraser: "E",
|
|
4243
6137
|
width: "W",
|
|
4244
6138
|
shapeBuilder: "M",
|
|
4245
|
-
paintBucket: "B"
|
|
6139
|
+
paintBucket: "B",
|
|
6140
|
+
blend: "J"
|
|
4246
6141
|
};
|
|
4247
6142
|
var VECTOR_TOOL_GLYPHS = {
|
|
4248
6143
|
select: "\u2196",
|
|
@@ -4259,7 +6154,8 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
4259
6154
|
eraser: "\u232B",
|
|
4260
6155
|
width: "\u21F9",
|
|
4261
6156
|
shapeBuilder: "\u2295",
|
|
4262
|
-
paintBucket: "\u25CD"
|
|
6157
|
+
paintBucket: "\u25CD",
|
|
6158
|
+
blend: "\u29D3"
|
|
4263
6159
|
};
|
|
4264
6160
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
4265
6161
|
var BOOLEAN_OPS = {
|
|
@@ -4313,6 +6209,12 @@ function spliceTree(nodes, id, replacements) {
|
|
|
4313
6209
|
return changed ? out : nodes;
|
|
4314
6210
|
}
|
|
4315
6211
|
var toPoint2 = (p) => [p.x, p.y];
|
|
6212
|
+
var placementOf = (n) => ({
|
|
6213
|
+
...n.transform ? { transform: n.transform } : {},
|
|
6214
|
+
...n.visible === false ? { visible: false } : {},
|
|
6215
|
+
...n.locked ? { locked: true } : {},
|
|
6216
|
+
...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
|
|
6217
|
+
});
|
|
4316
6218
|
var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
4317
6219
|
var idCounter = 0;
|
|
4318
6220
|
var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
|
|
@@ -4347,6 +6249,9 @@ var VectorEditorController = class {
|
|
|
4347
6249
|
/** Paint bucket map of the visible outlines. */
|
|
4348
6250
|
fillCache = null;
|
|
4349
6251
|
builderPreview = null;
|
|
6252
|
+
blendStart = null;
|
|
6253
|
+
/** Options new blends get (the toolbars edit them along with the selected blend's). */
|
|
6254
|
+
blendDefaults = {};
|
|
4350
6255
|
/** What the highlight shows, to skip notifying when the pointer stays over the same face. */
|
|
4351
6256
|
builderHoverKey = null;
|
|
4352
6257
|
createId;
|
|
@@ -4361,6 +6266,7 @@ var VectorEditorController = class {
|
|
|
4361
6266
|
constructor(options = {}) {
|
|
4362
6267
|
this.history = options.history ?? new HistoryManager();
|
|
4363
6268
|
this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
|
|
6269
|
+
this.doc = this.refreshed(this.doc);
|
|
4364
6270
|
this.tool = options.tool ?? "select";
|
|
4365
6271
|
this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
|
|
4366
6272
|
this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
|
|
@@ -4425,6 +6331,8 @@ var VectorEditorController = class {
|
|
|
4425
6331
|
pen: this.pen,
|
|
4426
6332
|
pointer: this.pointer,
|
|
4427
6333
|
builder: this.builderPreview,
|
|
6334
|
+
blendStart: this.blendStart,
|
|
6335
|
+
blendDefaults: this.blendDefaults,
|
|
4428
6336
|
canUndo: h.canUndo,
|
|
4429
6337
|
canRedo: h.canRedo,
|
|
4430
6338
|
undoLabel: h.undoName,
|
|
@@ -4432,7 +6340,15 @@ var VectorEditorController = class {
|
|
|
4432
6340
|
revision: this.revision
|
|
4433
6341
|
};
|
|
4434
6342
|
}
|
|
6343
|
+
/** `doc` with its blends' steps brought up to date (the same object when they already are). */
|
|
6344
|
+
refreshed(doc) {
|
|
6345
|
+
const nodes = refreshVectorBlends(doc.nodes);
|
|
6346
|
+
return nodes === doc.nodes ? doc : { ...doc, nodes };
|
|
6347
|
+
}
|
|
4435
6348
|
notify(docChanged = false) {
|
|
6349
|
+
const fresh = this.refreshed(this.doc);
|
|
6350
|
+
if (fresh !== this.doc) this.doc = fresh;
|
|
6351
|
+
if (this.blendStart && !this.getNode(this.blendStart.nodeId)) this.blendStart = null;
|
|
4436
6352
|
this.revision++;
|
|
4437
6353
|
this.state = this.computeState();
|
|
4438
6354
|
if (docChanged) this.onChange?.(this.doc);
|
|
@@ -4488,6 +6404,8 @@ var VectorEditorController = class {
|
|
|
4488
6404
|
return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
|
|
4489
6405
|
case "paintBucket":
|
|
4490
6406
|
return this.builderPreview ? "cell" : "crosshair";
|
|
6407
|
+
case "blend":
|
|
6408
|
+
return h?.nodeId ? this.blendStart && this.blendStart.nodeId !== h.nodeId ? "copy" : "pointer" : "crosshair";
|
|
4491
6409
|
default:
|
|
4492
6410
|
return "crosshair";
|
|
4493
6411
|
}
|
|
@@ -4521,8 +6439,12 @@ var VectorEditorController = class {
|
|
|
4521
6439
|
}
|
|
4522
6440
|
this.notify(docChanged);
|
|
4523
6441
|
}
|
|
4524
|
-
/**
|
|
4525
|
-
|
|
6442
|
+
/**
|
|
6443
|
+
* Records the change from `before` to the current state as one undo step. Consecutive
|
|
6444
|
+
* changes with the same `merge` key (a slider drag) fold into one step.
|
|
6445
|
+
*/
|
|
6446
|
+
commit(name, before, merge) {
|
|
6447
|
+
this.doc = this.refreshed(this.doc);
|
|
4526
6448
|
const after = this.snapshot();
|
|
4527
6449
|
if (before.doc === after.doc) {
|
|
4528
6450
|
this.notify();
|
|
@@ -4532,17 +6454,23 @@ var VectorEditorController = class {
|
|
|
4532
6454
|
name,
|
|
4533
6455
|
execute: () => this.restore(after),
|
|
4534
6456
|
undo: () => this.restore(before),
|
|
4535
|
-
redo: () => this.restore(after)
|
|
6457
|
+
redo: () => this.restore(after),
|
|
6458
|
+
...merge ? { tag: merge } : {}
|
|
4536
6459
|
};
|
|
4537
6460
|
this.committing = true;
|
|
4538
|
-
this.history.
|
|
6461
|
+
if (merge) this.history.executeMerged(command, { key: merge });
|
|
6462
|
+
else this.history.execute(command);
|
|
4539
6463
|
this.committing = false;
|
|
4540
6464
|
}
|
|
4541
|
-
/** Runs `fn` (which updates fields) and records it as one undo step. */
|
|
4542
|
-
change(name, fn) {
|
|
6465
|
+
/** Runs `fn` (which updates fields) and records it as one undo step (merged by `merge` key). */
|
|
6466
|
+
change(name, fn, merge) {
|
|
4543
6467
|
const before = this.snapshot();
|
|
4544
6468
|
fn();
|
|
4545
|
-
this.commit(name, before);
|
|
6469
|
+
this.commit(name, before, merge);
|
|
6470
|
+
}
|
|
6471
|
+
/** Ends a merged edit (a slider released): the next change starts a new undo step. */
|
|
6472
|
+
endMergedEdit() {
|
|
6473
|
+
this.history.endGesture();
|
|
4546
6474
|
}
|
|
4547
6475
|
setDoc(nodes) {
|
|
4548
6476
|
if (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };
|
|
@@ -4625,6 +6553,7 @@ var VectorEditorController = class {
|
|
|
4625
6553
|
this.widthEdit = null;
|
|
4626
6554
|
this.builderPreview = null;
|
|
4627
6555
|
this.builderHoverKey = null;
|
|
6556
|
+
this.blendStart = null;
|
|
4628
6557
|
if (tool !== "direct") {
|
|
4629
6558
|
this.anchors = [];
|
|
4630
6559
|
this.editingId = null;
|
|
@@ -5091,7 +7020,7 @@ var VectorEditorController = class {
|
|
|
5091
7020
|
const created = [];
|
|
5092
7021
|
const replace = /* @__PURE__ */ new Map();
|
|
5093
7022
|
for (const p of paths) {
|
|
5094
|
-
const outline = bezierToSubpaths(vectorStrokeOutlinePath(
|
|
7023
|
+
const outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), p));
|
|
5095
7024
|
if (!outline.length) continue;
|
|
5096
7025
|
const {
|
|
5097
7026
|
stroke,
|
|
@@ -5106,11 +7035,12 @@ var VectorEditorController = class {
|
|
|
5106
7035
|
startArrowScale: _ss,
|
|
5107
7036
|
endArrowScale: _es,
|
|
5108
7037
|
strokeWidthProfile: _wp,
|
|
7038
|
+
effects,
|
|
5109
7039
|
...rest
|
|
5110
7040
|
} = cloneVectorNode(p);
|
|
5111
7041
|
const id = this.newId();
|
|
5112
7042
|
const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
|
|
5113
|
-
const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
|
|
7043
|
+
const list = hasPaint(p.fill) ? [{ ...rest, ...effects ? { effects } : {}, stroke: null }, strokeNode] : [strokeNode];
|
|
5114
7044
|
created.push(id);
|
|
5115
7045
|
replace.set(p.id, list);
|
|
5116
7046
|
}
|
|
@@ -5125,6 +7055,121 @@ var VectorEditorController = class {
|
|
|
5125
7055
|
});
|
|
5126
7056
|
return created;
|
|
5127
7057
|
}
|
|
7058
|
+
// -------------------------------------------------------------------------
|
|
7059
|
+
// Path effects
|
|
7060
|
+
// -------------------------------------------------------------------------
|
|
7061
|
+
/** Selected (or given) nodes that can carry effects: the roots of the selection. */
|
|
7062
|
+
effectTargets(ids) {
|
|
7063
|
+
return (ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && !n.blendStep);
|
|
7064
|
+
}
|
|
7065
|
+
/**
|
|
7066
|
+
* Adds an effect of `type` to the selected (or given) nodes, at the bottom of each stack (or
|
|
7067
|
+
* at `index`), with default parameters merged with `params`. A repeater's copies start one
|
|
7068
|
+
* shape width apart. One undo step; returns the new effect ids (one per node, in order).
|
|
7069
|
+
*/
|
|
7070
|
+
addEffect(type, options = {}) {
|
|
7071
|
+
const targets = this.effectTargets(options.ids);
|
|
7072
|
+
if (!targets.length || !createVectorEffect(type, "")) return [];
|
|
7073
|
+
const created = [];
|
|
7074
|
+
const next = /* @__PURE__ */ new Map();
|
|
7075
|
+
for (const node of targets) {
|
|
7076
|
+
const list = node.effects ?? [];
|
|
7077
|
+
let k = list.length + 1;
|
|
7078
|
+
while (list.some((e) => e.id === `fx${k}`)) k++;
|
|
7079
|
+
const params = { ...options.params ?? {} };
|
|
7080
|
+
if (type === "repeater" && params.position === void 0) {
|
|
7081
|
+
const b = bezierPathBounds(vectorAppearanceOutline(node));
|
|
7082
|
+
if (b && b.maxX - b.minX > 0) params.position = { x: Math.round((b.maxX - b.minX) * 1.25 * 100) / 100, y: 0 };
|
|
7083
|
+
}
|
|
7084
|
+
const effect = createVectorEffect(type, `fx${k}`, params);
|
|
7085
|
+
const at2 = options.index === void 0 ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));
|
|
7086
|
+
next.set(node.id, { ...node, effects: [...list.slice(0, at2), effect, ...list.slice(at2)] });
|
|
7087
|
+
created.push(effect.id);
|
|
7088
|
+
}
|
|
7089
|
+
this.change(`Add ${VECTOR_EFFECT_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));
|
|
7090
|
+
return created;
|
|
7091
|
+
}
|
|
7092
|
+
editEffects(nodeId, label, fn, merge) {
|
|
7093
|
+
const node = this.getNode(nodeId);
|
|
7094
|
+
if (!node) return { ok: false, message: `There is no node "${nodeId}".` };
|
|
7095
|
+
const result = fn(node.effects ?? []);
|
|
7096
|
+
if (typeof result === "string") return { ok: false, message: result };
|
|
7097
|
+
this.change(label, () => this.replaceNode(nodeId, result.length ? { ...node, effects: result } : (({ effects: _e, ...rest }) => rest)(node)), merge);
|
|
7098
|
+
return { ok: true };
|
|
7099
|
+
}
|
|
7100
|
+
/**
|
|
7101
|
+
* Turns an effect on or off and changes its parameters (unknown or invalid values are
|
|
7102
|
+
* ignored). With `merge` (true, or a key of your own), consecutive updates of the same effect
|
|
7103
|
+
* fold into one undo step, as a slider drag should; call `endMergedEdit` when it ends.
|
|
7104
|
+
*/
|
|
7105
|
+
updateEffect(nodeId, effectId, patch, options = {}) {
|
|
7106
|
+
const merge = options.merge === true ? `effect:${nodeId}:${effectId}` : options.merge || void 0;
|
|
7107
|
+
const node = this.getNode(nodeId);
|
|
7108
|
+
const current = node?.effects?.find((e) => e.id === effectId);
|
|
7109
|
+
const label = current ? VECTOR_EFFECT_LABELS[current.type] ?? "Effect" : "Effect";
|
|
7110
|
+
return this.editEffects(
|
|
7111
|
+
nodeId,
|
|
7112
|
+
`Edit ${label}`,
|
|
7113
|
+
(list) => {
|
|
7114
|
+
const i = list.findIndex((e2) => e2.id === effectId);
|
|
7115
|
+
if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
|
|
7116
|
+
const e = list[i];
|
|
7117
|
+
const params = patch.params ? normalizeVectorEffectParams(e.type, patch.params, e.params) : e.params;
|
|
7118
|
+
const next = { ...e, params, ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {} };
|
|
7119
|
+
if (next.enabled !== false) delete next.enabled;
|
|
7120
|
+
return list.map((x, j) => j === i ? next : x);
|
|
7121
|
+
},
|
|
7122
|
+
merge
|
|
7123
|
+
);
|
|
7124
|
+
}
|
|
7125
|
+
/** Removes an effect from a node's stack. */
|
|
7126
|
+
removeEffect(nodeId, effectId) {
|
|
7127
|
+
return this.editEffects(nodeId, "Remove Effect", (list) => list.some((e) => e.id === effectId) ? list.filter((e) => e.id !== effectId) : `Node "${nodeId}" has no effect "${effectId}".`);
|
|
7128
|
+
}
|
|
7129
|
+
/** Moves an effect to position `index` of its stack (0 is applied first). */
|
|
7130
|
+
moveEffect(nodeId, effectId, index) {
|
|
7131
|
+
return this.editEffects(nodeId, "Reorder Effects", (list) => {
|
|
7132
|
+
const i = list.findIndex((e2) => e2.id === effectId);
|
|
7133
|
+
if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
|
|
7134
|
+
const to = Math.min(list.length - 1, Math.max(0, Math.round(index)));
|
|
7135
|
+
const out = list.slice();
|
|
7136
|
+
const [e] = out.splice(i, 1);
|
|
7137
|
+
out.splice(to, 0, e);
|
|
7138
|
+
return out;
|
|
7139
|
+
});
|
|
7140
|
+
}
|
|
7141
|
+
/**
|
|
7142
|
+
* Expand Appearance: bakes the path effects and live booleans of the selected (or given)
|
|
7143
|
+
* nodes, and of everything inside them, into plain paths. A node keeps its id (a group when
|
|
7144
|
+
* it draws as several shapes); the shapes it made get new ids. Returns the ids of the expanded
|
|
7145
|
+
* nodes (empty when none was live).
|
|
7146
|
+
*/
|
|
7147
|
+
expandAppearance(options = {}) {
|
|
7148
|
+
const roots = (options.ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && hasVectorAppearance(n));
|
|
7149
|
+
if (!roots.length) return [];
|
|
7150
|
+
const fresh = (n) => {
|
|
7151
|
+
const id = n.id.includes("~fx") ? this.newId() : n.id;
|
|
7152
|
+
return n.type === "group" ? { ...n, id, children: n.children.map(fresh) } : { ...n, id };
|
|
7153
|
+
};
|
|
7154
|
+
const bake = (n) => {
|
|
7155
|
+
if (!hasVectorAppearance(n)) return n;
|
|
7156
|
+
if (!hasOwnVectorAppearance(n)) return { ...n, children: n.children.map(bake) };
|
|
7157
|
+
let e = expandVectorAppearance(n);
|
|
7158
|
+
if (e.type === "group" && e.children.length === 1 && e.children[0].type === "path") {
|
|
7159
|
+
const { id: _id, opacity, name, ...child } = e.children[0];
|
|
7160
|
+
const k = (e.opacity ?? 1) * (opacity ?? 1);
|
|
7161
|
+
e = { ...child, id: e.id, name: e.name ?? name, ...placementOf(e), ...k < 1 ? { opacity: k } : {} };
|
|
7162
|
+
}
|
|
7163
|
+
return fresh(e);
|
|
7164
|
+
};
|
|
7165
|
+
const next = new Map(roots.map((n) => [n.id, bake(n)]));
|
|
7166
|
+
this.change("Expand Appearance", () => {
|
|
7167
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));
|
|
7168
|
+
this.anchors = [];
|
|
7169
|
+
this.editingId = null;
|
|
7170
|
+
});
|
|
7171
|
+
return roots.map((n) => n.id);
|
|
7172
|
+
}
|
|
5128
7173
|
rename(id, name) {
|
|
5129
7174
|
return this.updateNode(id, { name }, { label: "Rename" });
|
|
5130
7175
|
}
|
|
@@ -5249,24 +7294,24 @@ var VectorEditorController = class {
|
|
|
5249
7294
|
return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
|
|
5250
7295
|
}
|
|
5251
7296
|
/** The selection frame's centre in document space. */
|
|
5252
|
-
frameCenter(
|
|
5253
|
-
return applyMatrix(rotateMatrix(
|
|
7297
|
+
frameCenter(frame2) {
|
|
7298
|
+
return applyMatrix(rotateMatrix(frame2.angle), { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 });
|
|
5254
7299
|
}
|
|
5255
7300
|
/**
|
|
5256
7301
|
* Scales along the selection box's own axes (rotated with the selection when its nodes
|
|
5257
7302
|
* share a rotation) around `origin` (default: the box centre, in document space).
|
|
5258
7303
|
*/
|
|
5259
7304
|
scale(sx, sy = sx, options = {}) {
|
|
5260
|
-
const
|
|
5261
|
-
if (!
|
|
5262
|
-
const origin = applyMatrix(rotateMatrix(-
|
|
5263
|
-
return this.transform(this.inFrame(
|
|
7305
|
+
const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
|
|
7306
|
+
if (!frame2) return { ok: false, message: "Nothing is selected." };
|
|
7307
|
+
const origin = applyMatrix(rotateMatrix(-frame2.angle), options.origin ?? this.frameCenter(frame2));
|
|
7308
|
+
return this.transform(this.inFrame(frame2.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: "Scale" });
|
|
5264
7309
|
}
|
|
5265
7310
|
/** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
|
|
5266
7311
|
rotate(degrees, options = {}) {
|
|
5267
|
-
const
|
|
5268
|
-
if (!
|
|
5269
|
-
const origin = options.origin ?? this.frameCenter(
|
|
7312
|
+
const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
|
|
7313
|
+
if (!frame2) return { ok: false, message: "Nothing is selected." };
|
|
7314
|
+
const origin = options.origin ?? this.frameCenter(frame2);
|
|
5270
7315
|
return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
|
|
5271
7316
|
}
|
|
5272
7317
|
/** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */
|
|
@@ -5275,21 +7320,21 @@ var VectorEditorController = class {
|
|
|
5275
7320
|
if (!ids.length) return { ok: false, message: "Nothing is selected." };
|
|
5276
7321
|
this.change("Set Rotation", () => {
|
|
5277
7322
|
for (const id of ids) {
|
|
5278
|
-
const
|
|
5279
|
-
if (!
|
|
5280
|
-
const delta = normalizeAngle(degrees -
|
|
5281
|
-
if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(
|
|
7323
|
+
const frame2 = this.frameOf([id]);
|
|
7324
|
+
if (!frame2) continue;
|
|
7325
|
+
const delta = normalizeAngle(degrees - frame2.angle);
|
|
7326
|
+
if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(frame2)));
|
|
5282
7327
|
}
|
|
5283
7328
|
});
|
|
5284
7329
|
return { ok: true };
|
|
5285
7330
|
}
|
|
5286
7331
|
/** Mirrors along the selection box's own axes. */
|
|
5287
7332
|
flip(axis, options = {}) {
|
|
5288
|
-
const
|
|
5289
|
-
if (!
|
|
5290
|
-
const c = { x:
|
|
7333
|
+
const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
|
|
7334
|
+
if (!frame2) return { ok: false, message: "Nothing is selected." };
|
|
7335
|
+
const c = { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 };
|
|
5291
7336
|
const m = axis === "horizontal" ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);
|
|
5292
|
-
return this.transform(this.inFrame(
|
|
7337
|
+
return this.transform(this.inFrame(frame2.angle, m), { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
|
|
5293
7338
|
}
|
|
5294
7339
|
/**
|
|
5295
7340
|
* Aligns the selected nodes to their combined bounds, or to the artboard when one node is
|
|
@@ -5416,7 +7461,7 @@ var VectorEditorController = class {
|
|
|
5416
7461
|
const opacity = n.opacity;
|
|
5417
7462
|
const m = nodeMatrix(n);
|
|
5418
7463
|
out.push(
|
|
5419
|
-
...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
|
|
7464
|
+
...n.children.map((c) => c.blendStep ? (({ blendStep: _s, locked: _l, ...rest }) => rest)(c) : c).map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c).map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
|
|
5420
7465
|
);
|
|
5421
7466
|
} else if (n.type === "group") {
|
|
5422
7467
|
const children = flatten2(n.children);
|
|
@@ -5453,34 +7498,33 @@ var VectorEditorController = class {
|
|
|
5453
7498
|
/**
|
|
5454
7499
|
* Combines the selected paths (two or more) into one with a boolean operation. `subtract`
|
|
5455
7500
|
* removes the upper shapes from the lowest one. The result takes the lowest shape's style
|
|
5456
|
-
* and place.
|
|
7501
|
+
* and place. Shapes take part as drawn (path effects and live booleans applied).
|
|
7502
|
+
*
|
|
7503
|
+
* With `live`, the operands stay editable instead: a lone live boolean group gets the
|
|
7504
|
+
* operation (its operands follow it again), operands of live boolean groups get it as their
|
|
7505
|
+
* own, and anything else (two or more nodes with one parent) is wrapped in a new live boolean
|
|
7506
|
+
* group. Returns the result's (or the live group's) id, or null.
|
|
5457
7507
|
*/
|
|
5458
7508
|
booleanOp(op, options = {}) {
|
|
5459
7509
|
const roots = options.ids ?? this.selectedRoots();
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
});
|
|
5464
|
-
const paths = [...new Set(order)].map((id2) => this.getNode(id2));
|
|
5465
|
-
if (paths.length < 2) return null;
|
|
7510
|
+
if (options.live) return this.liveBooleanOp(op, roots);
|
|
7511
|
+
const entries = vectorPathEntries(this.doc, roots, { locked: true });
|
|
7512
|
+
if (entries.length < 2) return null;
|
|
5466
7513
|
const result = pathBoolean(
|
|
5467
7514
|
BOOLEAN_OPS[op],
|
|
5468
|
-
|
|
5469
|
-
path: vectorPathWorldBezier(
|
|
5470
|
-
|
|
5471
|
-
vectorParentMatrix(this.doc.nodes, p.id) ?? IDENTITY_MATRIX
|
|
5472
|
-
),
|
|
5473
|
-
fillRule: p.fillRule ?? "nonzero"
|
|
7515
|
+
entries.map((e) => ({
|
|
7516
|
+
path: vectorPathWorldBezier({ ...e.node, subpaths: e.node.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) }, e.parent),
|
|
7517
|
+
fillRule: e.node.fillRule ?? "nonzero"
|
|
5474
7518
|
}))
|
|
5475
7519
|
);
|
|
5476
|
-
const base =
|
|
5477
|
-
const anchorRoot = roots.find((r) =>
|
|
7520
|
+
const base = entries[0].node;
|
|
7521
|
+
const anchorRoot = roots.find((r) => this.holds(r, base.id)) ?? base.id;
|
|
5478
7522
|
const subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));
|
|
5479
7523
|
if (result.error || !subpaths.length) return null;
|
|
5480
7524
|
const id = this.newId();
|
|
5481
|
-
const { transform: _t, ...baseRest } = cloneVectorNode(base);
|
|
7525
|
+
const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(base);
|
|
5482
7526
|
const node = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };
|
|
5483
|
-
const drop = /* @__PURE__ */ new Set([...
|
|
7527
|
+
const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
|
|
5484
7528
|
this.change(BOOLEAN_LABELS[op], () => {
|
|
5485
7529
|
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
|
|
5486
7530
|
this.selection = [id];
|
|
@@ -5489,18 +7533,75 @@ var VectorEditorController = class {
|
|
|
5489
7533
|
});
|
|
5490
7534
|
return id;
|
|
5491
7535
|
}
|
|
7536
|
+
/** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */
|
|
7537
|
+
holds(rootId, id) {
|
|
7538
|
+
const n = this.getNode(rootId);
|
|
7539
|
+
return !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id));
|
|
7540
|
+
}
|
|
7541
|
+
liveBooleanOp(op, roots) {
|
|
7542
|
+
const nodes = roots.map((id2) => this.getNode(id2));
|
|
7543
|
+
if (!nodes.length || nodes.some((n) => !n)) return null;
|
|
7544
|
+
const label = `Live ${BOOLEAN_LABELS[op]}`;
|
|
7545
|
+
const lone = nodes.length === 1 ? nodes[0] : null;
|
|
7546
|
+
if (isVectorLiveBoolean(lone)) {
|
|
7547
|
+
const children = lone.children.map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
|
|
7548
|
+
this.change(label, () => this.replaceNode(lone.id, { ...lone, liveBoolean: op, children }));
|
|
7549
|
+
return lone.id;
|
|
7550
|
+
}
|
|
7551
|
+
const parents = roots.map((id2) => locateVectorNode(this.doc.nodes, id2)?.parent ?? null);
|
|
7552
|
+
if (parents.every((g) => isVectorLiveBoolean(g))) {
|
|
7553
|
+
const set = new Set(roots);
|
|
7554
|
+
this.change(
|
|
7555
|
+
label,
|
|
7556
|
+
() => this.setDoc(
|
|
7557
|
+
mapTree(this.doc.nodes, (n) => {
|
|
7558
|
+
if (n.type !== "group" || !n.liveBoolean || !n.children.some((c) => set.has(c.id))) return n;
|
|
7559
|
+
const group = n;
|
|
7560
|
+
const children = group.children.map((c) => {
|
|
7561
|
+
if (!set.has(c.id)) return c;
|
|
7562
|
+
const { booleanOperation: _b, ...rest } = c;
|
|
7563
|
+
return op === group.liveBoolean ? rest : { ...rest, booleanOperation: op };
|
|
7564
|
+
});
|
|
7565
|
+
return { ...group, children };
|
|
7566
|
+
})
|
|
7567
|
+
)
|
|
7568
|
+
);
|
|
7569
|
+
return parents[0].id;
|
|
7570
|
+
}
|
|
7571
|
+
if (nodes.length < 2) return null;
|
|
7572
|
+
const parent = parents[0]?.id ?? null;
|
|
7573
|
+
if (parents.some((g) => (g?.id ?? null) !== parent)) return null;
|
|
7574
|
+
const id = this.newId();
|
|
7575
|
+
this.change(label, () => {
|
|
7576
|
+
const set = new Set(roots);
|
|
7577
|
+
const wrap = (list) => {
|
|
7578
|
+
const members = list.filter((n) => set.has(n.id)).map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
|
|
7579
|
+
const top = Math.max(...list.map((n, i) => set.has(n.id) ? i : -1));
|
|
7580
|
+
const out = [];
|
|
7581
|
+
list.forEach((n, i) => {
|
|
7582
|
+
if (i === top) out.push({ id, type: "group", name: BOOLEAN_LABELS[op], liveBoolean: op, children: members });
|
|
7583
|
+
if (!set.has(n.id)) out.push(n);
|
|
7584
|
+
});
|
|
7585
|
+
return out;
|
|
7586
|
+
};
|
|
7587
|
+
if (parent === null) this.setDoc(wrap(this.doc.nodes));
|
|
7588
|
+
else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: wrap(n.children) } : n));
|
|
7589
|
+
this.selection = [id];
|
|
7590
|
+
this.anchors = [];
|
|
7591
|
+
this.editingId = null;
|
|
7592
|
+
});
|
|
7593
|
+
return id;
|
|
7594
|
+
}
|
|
5492
7595
|
/** Joins the selected paths into one compound path (subpaths kept, lowest style). */
|
|
5493
7596
|
combine(options = {}) {
|
|
5494
7597
|
const roots = options.ids ?? this.selectedRoots();
|
|
5495
|
-
const
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
});
|
|
5499
|
-
if (paths.length < 2) return null;
|
|
7598
|
+
const entries = vectorPathEntries(this.doc, roots, { locked: true });
|
|
7599
|
+
if (entries.length < 2) return null;
|
|
7600
|
+
const paths = entries.map((e) => e.node);
|
|
5500
7601
|
const id = this.newId();
|
|
5501
|
-
const first = roots.find((r) =>
|
|
5502
|
-
const subpaths =
|
|
5503
|
-
const { transform: _t, ...baseRest } = cloneVectorNode(paths[0]);
|
|
7602
|
+
const first = roots.find((r) => this.holds(r, paths[0].id)) ?? paths[0].id;
|
|
7603
|
+
const subpaths = entries.flatMap((e) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(e.node, e.parent))));
|
|
7604
|
+
const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(paths[0]);
|
|
5504
7605
|
const node = { ...baseRest, id, name: "Compound Path", subpaths };
|
|
5505
7606
|
const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
|
|
5506
7607
|
this.change("Combine", () => {
|
|
@@ -5555,10 +7656,7 @@ var VectorEditorController = class {
|
|
|
5555
7656
|
const result = pathfinder(op, shapes);
|
|
5556
7657
|
if (result.error || !result.pieces.length) return [];
|
|
5557
7658
|
const last = entries[entries.length - 1].node.id;
|
|
5558
|
-
const topRoot = [...roots].reverse().find((r) =>
|
|
5559
|
-
const n = this.getNode(r);
|
|
5560
|
-
return !!n && !!findVectorNode([n], last);
|
|
5561
|
-
}) ?? last;
|
|
7659
|
+
const topRoot = [...roots].reverse().find((r) => this.holds(r, last)) ?? last;
|
|
5562
7660
|
const nodes = [];
|
|
5563
7661
|
for (const piece of result.pieces) {
|
|
5564
7662
|
const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
|
|
@@ -5580,11 +7678,178 @@ var VectorEditorController = class {
|
|
|
5580
7678
|
return nodes.map((n) => n.id);
|
|
5581
7679
|
}
|
|
5582
7680
|
/** The Shape Builder map of the selected (or given) paths, cached per document and selection. */
|
|
7681
|
+
// -------------------------------------------------------------------------
|
|
7682
|
+
// Blends
|
|
7683
|
+
// -------------------------------------------------------------------------
|
|
7684
|
+
/** Selected (or given) blend groups; a selected key or step counts as its blend. */
|
|
7685
|
+
blendTargets(ids) {
|
|
7686
|
+
const out = /* @__PURE__ */ new Map();
|
|
7687
|
+
for (const id of ids ?? this.selection) {
|
|
7688
|
+
const node = this.getNode(id);
|
|
7689
|
+
const group = isVectorBlendGroup(node) ? node : locateVectorNode(this.doc.nodes, id)?.parent;
|
|
7690
|
+
if (isVectorBlendGroup(group)) out.set(group.id, group);
|
|
7691
|
+
}
|
|
7692
|
+
return [...out.values()];
|
|
7693
|
+
}
|
|
7694
|
+
/**
|
|
7695
|
+
* Blends two or more selected (or given) paths or groups: they become the keys of a live
|
|
7696
|
+
* blend group (bottom to top, or in the order given with `keepOrder`) whose in-betweens are
|
|
7697
|
+
* regenerated whenever a key or the options change. The keys must share a parent. Returns
|
|
7698
|
+
* the blend group's id, or null.
|
|
7699
|
+
*/
|
|
7700
|
+
blend(input = {}) {
|
|
7701
|
+
const ids = [...new Set(input.ids ?? this.selectedRoots())].filter((id2) => {
|
|
7702
|
+
const n = this.getNode(id2);
|
|
7703
|
+
return !!n && !n.blendStep && !n.locked;
|
|
7704
|
+
});
|
|
7705
|
+
if (ids.length < 2) return null;
|
|
7706
|
+
const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
|
|
7707
|
+
const parent = locs[0].parent?.id ?? null;
|
|
7708
|
+
if (locs.some((l) => (l.parent?.id ?? null) !== parent)) return null;
|
|
7709
|
+
const order = input.keepOrder ? ids : ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);
|
|
7710
|
+
const blend = blendOptionsOf({ ...this.blendDefaults, ...input });
|
|
7711
|
+
if (input.starts) {
|
|
7712
|
+
const starts = Object.fromEntries(Object.entries(input.starts).filter(([k, v2]) => order.includes(k) && Number.isInteger(v2) && v2 >= 0));
|
|
7713
|
+
if (Object.keys(starts).length) blend.starts = starts;
|
|
7714
|
+
}
|
|
7715
|
+
const id = this.newId();
|
|
7716
|
+
this.change("Blend", () => {
|
|
7717
|
+
const set = new Set(order);
|
|
7718
|
+
const regroup = (list) => {
|
|
7719
|
+
const members = order.map((m) => list.find((n) => n.id === m));
|
|
7720
|
+
const top = Math.max(...members.map((m) => list.indexOf(m)));
|
|
7721
|
+
const out = [];
|
|
7722
|
+
list.forEach((n, i) => {
|
|
7723
|
+
if (i === top) out.push({ id, type: "group", name: input.name ?? "Blend", blend, children: members });
|
|
7724
|
+
if (!set.has(n.id)) out.push(n);
|
|
7725
|
+
});
|
|
7726
|
+
return out;
|
|
7727
|
+
};
|
|
7728
|
+
if (parent === null) this.setDoc(regroup(this.doc.nodes));
|
|
7729
|
+
else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: regroup(n.children) } : n));
|
|
7730
|
+
this.selection = [id];
|
|
7731
|
+
this.anchors = [];
|
|
7732
|
+
this.editingId = null;
|
|
7733
|
+
});
|
|
7734
|
+
return id;
|
|
7735
|
+
}
|
|
7736
|
+
/** Sets the options new blends get (spacing, steps, distance, orientation, morph). */
|
|
7737
|
+
setBlendDefaults(patch) {
|
|
7738
|
+
this.blendDefaults = { ...this.blendDefaults, ...blendOptionsOf(patch) };
|
|
7739
|
+
this.notify();
|
|
7740
|
+
}
|
|
7741
|
+
/**
|
|
7742
|
+
* Adds a node as the last key of a blend (it must share the blend's parent); `start` is the
|
|
7743
|
+
* anchor of its first subpath the blend starts from. One undo step.
|
|
7744
|
+
*/
|
|
7745
|
+
addBlendKey(blendId, nodeId, options = {}) {
|
|
7746
|
+
const group = this.getNode(blendId);
|
|
7747
|
+
const node = this.getNode(nodeId);
|
|
7748
|
+
if (!isVectorBlendGroup(group)) return { ok: false, message: `"${blendId}" is not a blend.` };
|
|
7749
|
+
if (!node || node.blendStep || node.locked || node.id === blendId) return { ok: false, message: `"${nodeId}" cannot be a key.` };
|
|
7750
|
+
const parentOf = (id) => locateVectorNode(this.doc.nodes, id)?.parent?.id ?? null;
|
|
7751
|
+
if (parentOf(blendId) !== parentOf(nodeId)) return { ok: false, message: "The new key must sit beside the blend (same parent)." };
|
|
7752
|
+
const start = options.start;
|
|
7753
|
+
this.change("Blend", () => {
|
|
7754
|
+
const blend = start !== null && start !== void 0 ? { ...group.blend, starts: { ...group.blend.starts, [nodeId]: start } } : group.blend;
|
|
7755
|
+
const m = invertMatrix(nodeMatrix(group));
|
|
7756
|
+
const key = group.transform ? transformVectorNode(node, m) : node;
|
|
7757
|
+
const without = mapTree(this.doc.nodes, (n) => n.id === nodeId ? null : n);
|
|
7758
|
+
this.setDoc(spliceTree(without, blendId, [{ ...group, blend, children: [...group.children, key] }]));
|
|
7759
|
+
this.selection = [blendId];
|
|
7760
|
+
this.anchors = [];
|
|
7761
|
+
});
|
|
7762
|
+
return { ok: true };
|
|
7763
|
+
}
|
|
7764
|
+
/** Changes the spacing, steps, distance, orientation or morph of the selected (or given) blends. */
|
|
7765
|
+
setBlendOptions(patch, options = {}) {
|
|
7766
|
+
const groups = this.blendTargets(options.ids);
|
|
7767
|
+
if (!groups.length) return { ok: false, message: "Select a blend." };
|
|
7768
|
+
const clean2 = blendOptionsOf(patch);
|
|
7769
|
+
if (!Object.keys(clean2).length) return { ok: false, message: "No valid blend options were given." };
|
|
7770
|
+
const set = new Set(groups.map((g) => g.id));
|
|
7771
|
+
this.change("Blend Options", () => {
|
|
7772
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => set.has(n.id) && n.type === "group" && n.blend ? { ...n, blend: { ...n.blend, ...clean2 } } : n));
|
|
7773
|
+
});
|
|
7774
|
+
return { ok: true };
|
|
7775
|
+
}
|
|
7776
|
+
/** Expand: turns the selected (or given) blends into plain groups of their keys and steps. Returns their ids. */
|
|
7777
|
+
expandBlend(options = {}) {
|
|
7778
|
+
const groups = this.blendTargets(options.ids);
|
|
7779
|
+
if (!groups.length) return [];
|
|
7780
|
+
const set = new Set(groups.map((g) => g.id));
|
|
7781
|
+
const used = /* @__PURE__ */ new Set();
|
|
7782
|
+
walkVectorNodes(this.doc.nodes, (n) => used.add(n.id));
|
|
7783
|
+
const fresh = () => {
|
|
7784
|
+
let id = this.createId();
|
|
7785
|
+
while (used.has(id)) id = this.createId();
|
|
7786
|
+
used.add(id);
|
|
7787
|
+
return id;
|
|
7788
|
+
};
|
|
7789
|
+
this.change("Expand Blend", () => {
|
|
7790
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => set.has(n.id) && n.type === "group" ? expandVectorBlend(n, fresh) : n));
|
|
7791
|
+
this.selection = [...set];
|
|
7792
|
+
this.anchors = [];
|
|
7793
|
+
});
|
|
7794
|
+
return [...set];
|
|
7795
|
+
}
|
|
7796
|
+
/** Release: replaces the selected (or given) blends with their keys (and a replaced spine as a path). */
|
|
7797
|
+
releaseBlend(options = {}) {
|
|
7798
|
+
const groups = this.blendTargets(options.ids);
|
|
7799
|
+
if (!groups.length) return [];
|
|
7800
|
+
const released = [];
|
|
7801
|
+
this.change("Release Blend", () => {
|
|
7802
|
+
for (const g of groups) {
|
|
7803
|
+
const nodes = releaseVectorBlend(g, () => this.newId());
|
|
7804
|
+
released.push(...nodes.map((n) => n.id));
|
|
7805
|
+
this.setDoc(spliceTree(this.doc.nodes, g.id, nodes));
|
|
7806
|
+
}
|
|
7807
|
+
this.selection = released;
|
|
7808
|
+
this.anchors = [];
|
|
7809
|
+
});
|
|
7810
|
+
return released;
|
|
7811
|
+
}
|
|
7812
|
+
/**
|
|
7813
|
+
* Replace Spine: the selected blend's steps follow the selected path instead of the straight
|
|
7814
|
+
* lines between its keys. The path's first subpath becomes the spine (the path is removed)
|
|
7815
|
+
* and the keys move onto it, spread evenly by arc length.
|
|
7816
|
+
*/
|
|
7817
|
+
replaceBlendSpine(options = {}) {
|
|
7818
|
+
const roots = options.ids ?? this.selectedRoots();
|
|
7819
|
+
const groups = this.blendTargets(roots);
|
|
7820
|
+
const paths = roots.map((id) => this.getNode(id)).filter((n) => n?.type === "path" && !n.blendStep && !isVectorBlendGroup(locateVectorNode(this.doc.nodes, n.id)?.parent));
|
|
7821
|
+
if (groups.length !== 1 || paths.length !== 1) return { ok: false, message: "Select one blend and one path to be its spine." };
|
|
7822
|
+
const group = groups[0];
|
|
7823
|
+
const path = paths[0];
|
|
7824
|
+
const first = path.subpaths.find((sp) => sp.anchors.length >= 2);
|
|
7825
|
+
if (!first) return { ok: false, message: "The spine needs a path with at least two points." };
|
|
7826
|
+
const m = multiplyMatrices(this.worldOf(path.id), invertMatrix(this.worldOf(group.id)));
|
|
7827
|
+
const spine = transformVectorNode({ id: "", type: "path", subpaths: [first] }, m).subpaths[0];
|
|
7828
|
+
this.change("Replace Spine", () => {
|
|
7829
|
+
const withSpine = { ...group, blend: { ...group.blend, spine } };
|
|
7830
|
+
const targets = vectorBlendKeyTargets(withSpine);
|
|
7831
|
+
let k = 0;
|
|
7832
|
+
const children = withSpine.children.map((c) => {
|
|
7833
|
+
if (c.blendStep) return c;
|
|
7834
|
+
const target = targets[k++];
|
|
7835
|
+
const b = vectorNodeBounds(c);
|
|
7836
|
+
if (!target || !b) return c;
|
|
7837
|
+
const dx = target[0] - (b.x + b.width / 2);
|
|
7838
|
+
const dy = target[1] - (b.y + b.height / 2);
|
|
7839
|
+
return Math.hypot(dx, dy) > 1e-9 ? transformVectorNode(c, translateMatrix(dx, dy)) : c;
|
|
7840
|
+
});
|
|
7841
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === path.id ? null : n.id === group.id ? { ...withSpine, children } : n));
|
|
7842
|
+
this.selection = [group.id];
|
|
7843
|
+
this.anchors = [];
|
|
7844
|
+
if (this.editingId === path.id) this.editingId = null;
|
|
7845
|
+
});
|
|
7846
|
+
return { ok: true };
|
|
7847
|
+
}
|
|
5583
7848
|
shapeBuilderMap(ids) {
|
|
5584
7849
|
const roots = ids ?? this.selectedRoots();
|
|
5585
7850
|
const key = roots.join("\0");
|
|
5586
7851
|
if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
|
|
5587
|
-
const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
|
|
7852
|
+
const entries = roots.length ? vectorPathEntries(this.doc, roots, { expand: false }) : [];
|
|
5588
7853
|
const map = entries.length ? vectorShapeBuilderMap(entries) : null;
|
|
5589
7854
|
this.builderCache = { doc: this.doc, key, map };
|
|
5590
7855
|
return map;
|
|
@@ -6318,9 +8583,9 @@ var VectorEditorController = class {
|
|
|
6318
8583
|
}
|
|
6319
8584
|
hitTransformHandle(screen) {
|
|
6320
8585
|
if (this.tool !== "select" || !this.selection.length) return null;
|
|
6321
|
-
const
|
|
6322
|
-
if (!
|
|
6323
|
-
const { box, handles } = getVectorSelectionBoxGeometry(
|
|
8586
|
+
const frame2 = this.frameOf(this.selectedRoots());
|
|
8587
|
+
if (!frame2) return null;
|
|
8588
|
+
const { box, handles } = getVectorSelectionBoxGeometry(frame2, this.viewport);
|
|
6324
8589
|
const r = this.hitRadius + 2;
|
|
6325
8590
|
const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
|
|
6326
8591
|
for (const h of usable2) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
|
|
@@ -6368,16 +8633,22 @@ var VectorEditorController = class {
|
|
|
6368
8633
|
return best;
|
|
6369
8634
|
}
|
|
6370
8635
|
/** Deepest unlocked visible path under `p`. */
|
|
8636
|
+
/** Live groups being edited (selected, or holding the selection or the edited path): their children are hit as stored. */
|
|
8637
|
+
enteredGroups() {
|
|
8638
|
+
return vectorEnteredGroups(this.doc.nodes, this.editingId ? [...this.selection, this.editingId] : this.selection);
|
|
8639
|
+
}
|
|
6371
8640
|
hitPath(p) {
|
|
6372
8641
|
const tol = this.tolerance();
|
|
8642
|
+
const entered = this.enteredGroups();
|
|
6373
8643
|
const search = (list, parent) => {
|
|
6374
8644
|
for (let i = list.length - 1; i >= 0; i--) {
|
|
6375
8645
|
const n = list[i];
|
|
6376
8646
|
if (n.visible === false || n.locked) continue;
|
|
6377
8647
|
if (n.type === "group") {
|
|
8648
|
+
if (hasOwnVectorAppearance(n) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;
|
|
6378
8649
|
const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
|
|
6379
8650
|
if (found) return found;
|
|
6380
|
-
} else if (hitTestVectorNode(n, p, tol, parent)) return n;
|
|
8651
|
+
} else if (hitTestVectorNode(n, p, tol, parent, { entered })) return n;
|
|
6381
8652
|
}
|
|
6382
8653
|
return null;
|
|
6383
8654
|
};
|
|
@@ -6402,7 +8673,7 @@ var VectorEditorController = class {
|
|
|
6402
8673
|
case "select": {
|
|
6403
8674
|
const handle = this.hitTransformHandle(screen);
|
|
6404
8675
|
if (handle) return { nodeId: null, transformHandle: handle };
|
|
6405
|
-
const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
|
|
8676
|
+
const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
|
|
6406
8677
|
return node ? { nodeId: node.id } : null;
|
|
6407
8678
|
}
|
|
6408
8679
|
case "direct": {
|
|
@@ -6432,6 +8703,11 @@ var VectorEditorController = class {
|
|
|
6432
8703
|
const seg = this.nearestOutline(p);
|
|
6433
8704
|
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
6434
8705
|
}
|
|
8706
|
+
case "blend": {
|
|
8707
|
+
const hit = this.blendHit(screen, p);
|
|
8708
|
+
if (!hit) return null;
|
|
8709
|
+
return hit.anchor === null ? { nodeId: hit.nodeId } : { nodeId: hit.nodeId, anchor: { nodeId: hit.nodeId, subpath: 0, index: hit.anchor } };
|
|
8710
|
+
}
|
|
6435
8711
|
case "width": {
|
|
6436
8712
|
const hit = this.hitWidthHandle(screen);
|
|
6437
8713
|
if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
|
|
@@ -6658,6 +8934,8 @@ var VectorEditorController = class {
|
|
|
6658
8934
|
case "shapeBuilder":
|
|
6659
8935
|
case "paintBucket":
|
|
6660
8936
|
return this.downBuilder(p, input);
|
|
8937
|
+
case "blend":
|
|
8938
|
+
return this.downBlend(screen, p);
|
|
6661
8939
|
case "scissors": {
|
|
6662
8940
|
const seg = this.nearestOutline(p);
|
|
6663
8941
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -6673,18 +8951,58 @@ var VectorEditorController = class {
|
|
|
6673
8951
|
return this.downShape(p, input);
|
|
6674
8952
|
}
|
|
6675
8953
|
}
|
|
8954
|
+
/** The shape under the pointer for the Blend tool, and the anchor of its first subpath under it. */
|
|
8955
|
+
blendHit(screen, p) {
|
|
8956
|
+
const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
|
|
8957
|
+
if (!node) return null;
|
|
8958
|
+
let anchor = null;
|
|
8959
|
+
if (node.type === "path" && node.subpaths[0]) {
|
|
8960
|
+
const refs = node.subpaths[0].anchors.map((_, index) => ({ nodeId: node.id, subpath: 0, index }));
|
|
8961
|
+
anchor = this.hitAnchor(screen, refs)?.index ?? null;
|
|
8962
|
+
}
|
|
8963
|
+
return { nodeId: node.id, anchor };
|
|
8964
|
+
}
|
|
8965
|
+
/** Blend tool: the first click picks the shape to blend from, the next one blends it into the shape clicked. */
|
|
8966
|
+
downBlend(screen, p) {
|
|
8967
|
+
const hit = this.blendHit(screen, p);
|
|
8968
|
+
const start = this.blendStart;
|
|
8969
|
+
if (!hit) {
|
|
8970
|
+
if (start) {
|
|
8971
|
+
this.blendStart = null;
|
|
8972
|
+
this.notify();
|
|
8973
|
+
}
|
|
8974
|
+
return;
|
|
8975
|
+
}
|
|
8976
|
+
if (!start || start.nodeId === hit.nodeId) {
|
|
8977
|
+
this.blendStart = hit;
|
|
8978
|
+
this.selection = [hit.nodeId];
|
|
8979
|
+
this.anchors = [];
|
|
8980
|
+
this.notify();
|
|
8981
|
+
return;
|
|
8982
|
+
}
|
|
8983
|
+
this.blendStart = null;
|
|
8984
|
+
const first = this.getNode(start.nodeId);
|
|
8985
|
+
if (isVectorBlendGroup(first)) {
|
|
8986
|
+
if (!this.addBlendKey(first.id, hit.nodeId, { start: hit.anchor }).ok) this.notify();
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8989
|
+
const starts = {};
|
|
8990
|
+
if (start.anchor !== null) starts[start.nodeId] = start.anchor;
|
|
8991
|
+
if (hit.anchor !== null) starts[hit.nodeId] = hit.anchor;
|
|
8992
|
+
if (!this.blend({ ids: [start.nodeId, hit.nodeId], keepOrder: true, starts })) this.notify();
|
|
8993
|
+
}
|
|
6676
8994
|
downSelect(screen, p, input) {
|
|
6677
8995
|
const handle = this.hitTransformHandle(screen);
|
|
6678
8996
|
if (handle) {
|
|
6679
|
-
const
|
|
8997
|
+
const frame2 = this.frameOf(this.selectedRoots());
|
|
6680
8998
|
const before = this.snapshot();
|
|
6681
|
-
const center = applyMatrix(rotateMatrix(
|
|
6682
|
-
this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame, before, base: this.doc };
|
|
8999
|
+
const center = applyMatrix(rotateMatrix(frame2.angle), { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 });
|
|
9000
|
+
this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame: frame2, before, base: this.doc };
|
|
6683
9001
|
this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
|
|
6684
9002
|
this.notify();
|
|
6685
9003
|
return;
|
|
6686
9004
|
}
|
|
6687
|
-
const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
|
|
9005
|
+
const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
|
|
6688
9006
|
if (node) {
|
|
6689
9007
|
const selected = this.selection.includes(node.id);
|
|
6690
9008
|
if (input.shift) {
|
|
@@ -7474,7 +9792,10 @@ var VectorEditorController = class {
|
|
|
7474
9792
|
this.widthPoints = [];
|
|
7475
9793
|
this.notify();
|
|
7476
9794
|
} else if (this.pen) this.finishPath();
|
|
7477
|
-
else if (this.
|
|
9795
|
+
else if (this.blendStart) {
|
|
9796
|
+
this.blendStart = null;
|
|
9797
|
+
this.notify();
|
|
9798
|
+
} else if (this.tool === "direct" && this.anchors.length) {
|
|
7478
9799
|
this.anchors = [];
|
|
7479
9800
|
this.notify();
|
|
7480
9801
|
} else if (this.tool !== "select") this.setTool("select");
|
|
@@ -7606,8 +9927,9 @@ function pencilPressureProfile(points, pressures) {
|
|
|
7606
9927
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
7607
9928
|
var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
|
|
7608
9929
|
var VECTOR_BUILDER_HATCH_GAP = 6;
|
|
7609
|
-
function renderNode(
|
|
7610
|
-
if (
|
|
9930
|
+
function renderNode(stored, defs, idPrefix) {
|
|
9931
|
+
if (stored.visible === false) return null;
|
|
9932
|
+
const node = expandVectorAppearance(stored);
|
|
7611
9933
|
if (node.type === "group") {
|
|
7612
9934
|
const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
|
|
7613
9935
|
return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: node.opacity ?? 1, children };
|
|
@@ -7648,16 +9970,20 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
7648
9970
|
};
|
|
7649
9971
|
}
|
|
7650
9972
|
var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
|
|
7651
|
-
function screenOutline(state, node) {
|
|
9973
|
+
function screenOutline(state, node, entered) {
|
|
7652
9974
|
const v2 = state.viewport;
|
|
7653
9975
|
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
7654
9976
|
const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
|
|
7655
|
-
return
|
|
9977
|
+
return outlinePaths(node, parent, entered).map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 })).join(" ");
|
|
7656
9978
|
}
|
|
7657
|
-
function
|
|
7658
|
-
if (node.type === "path") return [{
|
|
9979
|
+
function outlinePaths(node, parent, entered) {
|
|
9980
|
+
if (node.type === "path") return [vectorPathWorldBezier(node, parent, { effects: false })];
|
|
7659
9981
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
7660
|
-
|
|
9982
|
+
if (hasOwnVectorAppearance(node) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];
|
|
9983
|
+
return node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));
|
|
9984
|
+
}
|
|
9985
|
+
function getVectorEnteredGroups(state) {
|
|
9986
|
+
return vectorEnteredGroups(state.document.nodes, state.editingId ? [...state.selection, state.editingId] : state.selection);
|
|
7661
9987
|
}
|
|
7662
9988
|
function collect(node) {
|
|
7663
9989
|
if (node.type === "path") return [node];
|
|
@@ -7667,13 +9993,14 @@ function getVectorOverlayRenderData(state) {
|
|
|
7667
9993
|
const doc = state.document;
|
|
7668
9994
|
const z = state.viewport.zoom;
|
|
7669
9995
|
const outlines = [];
|
|
9996
|
+
const entered = getVectorEnteredGroups(state);
|
|
7670
9997
|
const seen = /* @__PURE__ */ new Set();
|
|
7671
9998
|
const outline = (id, selected2) => {
|
|
7672
9999
|
if (seen.has(id)) return;
|
|
7673
10000
|
const node = findVectorNode(doc.nodes, id);
|
|
7674
10001
|
if (!node || node.visible === false) return;
|
|
7675
10002
|
seen.add(id);
|
|
7676
|
-
outlines.push({ id, d: screenOutline(state, node), selected: selected2 });
|
|
10003
|
+
outlines.push({ id, d: screenOutline(state, node, entered), selected: selected2 });
|
|
7677
10004
|
};
|
|
7678
10005
|
for (const id of state.selection) outline(id, true);
|
|
7679
10006
|
if (state.editingId) outline(state.editingId, true);
|
|
@@ -7800,8 +10127,79 @@ function getVectorOverlayRenderData(state) {
|
|
|
7800
10127
|
status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
|
|
7801
10128
|
}
|
|
7802
10129
|
}
|
|
7803
|
-
|
|
10130
|
+
const blend = blendOverlay(state);
|
|
10131
|
+
if (state.tool === "blend" && !it) status = state.blendStart ? "Click another shape to blend into" : "Click a shape to blend from";
|
|
10132
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend };
|
|
10133
|
+
}
|
|
10134
|
+
function selectedBlends(state) {
|
|
10135
|
+
const out = /* @__PURE__ */ new Map();
|
|
10136
|
+
for (const id of state.selection) {
|
|
10137
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
10138
|
+
const group = isVectorBlendGroup(node) ? node : locateVectorNode(state.document.nodes, id)?.parent;
|
|
10139
|
+
if (isVectorBlendGroup(group)) out.set(group.id, group);
|
|
10140
|
+
}
|
|
10141
|
+
return [...out.values()];
|
|
10142
|
+
}
|
|
10143
|
+
function blendOverlay(state) {
|
|
10144
|
+
const v2 = state.viewport;
|
|
10145
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
10146
|
+
const doc = state.document;
|
|
10147
|
+
const sc = (p, m) => {
|
|
10148
|
+
const q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
|
|
10149
|
+
return { x: fmt(q.x), y: fmt(q.y) };
|
|
10150
|
+
};
|
|
10151
|
+
const spines = selectedBlends(state).flatMap((g) => {
|
|
10152
|
+
const s = vectorBlendSpine(g);
|
|
10153
|
+
if (!s) return [];
|
|
10154
|
+
const m = multiplyMatrices(vectorWorldMatrix(doc.nodes, g.id) ?? IDENTITY_MATRIX, view);
|
|
10155
|
+
const d = bezierPathToSvg(transformBezierPath(subpathsToBezier([s.spine], { corners: false }), m), { precision: 2 });
|
|
10156
|
+
const keys = s.stops.map((stop) => {
|
|
10157
|
+
const p = vectorBlendRailAt(s.rail, stop).point;
|
|
10158
|
+
return sc({ x: p[0], y: p[1] }, m);
|
|
10159
|
+
});
|
|
10160
|
+
return [{ id: g.id, d, keys, replaced: s.replaced }];
|
|
10161
|
+
});
|
|
10162
|
+
let start = null;
|
|
10163
|
+
const anchors = [];
|
|
10164
|
+
if (state.tool === "blend") {
|
|
10165
|
+
const from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : void 0;
|
|
10166
|
+
if (from && state.blendStart) {
|
|
10167
|
+
const a = state.blendStart.anchor === null ? void 0 : getAnchorWorld(doc, { nodeId: from.id, subpath: 0, index: state.blendStart.anchor });
|
|
10168
|
+
start = { d: screenOutline(state, from, /* @__PURE__ */ new Set()), anchor: a ? sc(a, view) : null };
|
|
10169
|
+
}
|
|
10170
|
+
const hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : void 0;
|
|
10171
|
+
if (hovered?.type === "path" && hovered.subpaths[0]) {
|
|
10172
|
+
hovered.subpaths[0].anchors.forEach((_, index) => {
|
|
10173
|
+
const a = getAnchorWorld(doc, { nodeId: hovered.id, subpath: 0, index });
|
|
10174
|
+
if (a) anchors.push({ ...sc(a, view), hovered: state.hover?.anchor?.index === index });
|
|
10175
|
+
});
|
|
10176
|
+
}
|
|
10177
|
+
}
|
|
10178
|
+
return { spines, start, anchors };
|
|
10179
|
+
}
|
|
10180
|
+
function getVectorBlendControls(state) {
|
|
10181
|
+
const blends = selectedBlends(state);
|
|
10182
|
+
const doc = state.document;
|
|
10183
|
+
const roots = state.selection.filter((id) => {
|
|
10184
|
+
const node = findVectorNode(doc.nodes, id);
|
|
10185
|
+
return !!node && !node.blendStep && !node.locked && !state.selection.includes(locateVectorNode(doc.nodes, id)?.parent?.id ?? "");
|
|
10186
|
+
});
|
|
10187
|
+
const parents = new Set(roots.map((id) => locateVectorNode(doc.nodes, id)?.parent?.id ?? null));
|
|
10188
|
+
const paths = roots.filter((id) => findVectorNode(doc.nodes, id)?.type === "path" && !isVectorBlendGroup(locateVectorNode(doc.nodes, id)?.parent));
|
|
10189
|
+
const first = blends[0];
|
|
10190
|
+
return {
|
|
10191
|
+
ids: blends.map((g) => g.id),
|
|
10192
|
+
options: resolveVectorBlendOptions(first ? first.blend : state.blendDefaults),
|
|
10193
|
+
canBlend: roots.length >= 2 && parents.size === 1,
|
|
10194
|
+
canReplaceSpine: blends.length === 1 && paths.length === 1,
|
|
10195
|
+
hasSpine: !!first?.blend?.spine
|
|
10196
|
+
};
|
|
7804
10197
|
}
|
|
10198
|
+
function applyVectorBlendOptions(controller, patch) {
|
|
10199
|
+
controller.setBlendDefaults(patch);
|
|
10200
|
+
if (getVectorBlendControls(controller.getState()).ids.length) controller.setBlendOptions(patch);
|
|
10201
|
+
}
|
|
10202
|
+
var VECTOR_BLEND_SPACING_LABELS = { steps: "Specified Steps", distance: "Specified Distance", smoothColor: "Smooth Color" };
|
|
7805
10203
|
var VECTOR_OVERLAY_THEME = {
|
|
7806
10204
|
accent: "#0ea5e9",
|
|
7807
10205
|
/** Smart guide lines and labels. */
|
|
@@ -7952,8 +10350,28 @@ var VECTOR_EDIT_TOGGLES = [
|
|
|
7952
10350
|
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
7953
10351
|
{ key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
|
|
7954
10352
|
];
|
|
10353
|
+
var VECTOR_EFFECT_MENU = VECTOR_EFFECT_TYPES.map((type) => ({ type, label: VECTOR_EFFECT_LABELS[type] }));
|
|
10354
|
+
function getVectorEffectsPanel(state) {
|
|
10355
|
+
const nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n) => !!n && !n.blendStep);
|
|
10356
|
+
if (!nodes.length) return null;
|
|
10357
|
+
const one = nodes.length === 1 ? nodes[0] : null;
|
|
10358
|
+
return {
|
|
10359
|
+
nodeId: one?.id ?? null,
|
|
10360
|
+
count: nodes.length,
|
|
10361
|
+
effects: (one?.effects ?? []).map((e) => ({
|
|
10362
|
+
id: e.id,
|
|
10363
|
+
type: e.type,
|
|
10364
|
+
label: VECTOR_EFFECT_LABELS[e.type] ?? e.type,
|
|
10365
|
+
enabled: e.enabled !== false,
|
|
10366
|
+
params: e.params,
|
|
10367
|
+
specs: VECTOR_EFFECT_PARAM_SPECS[e.type] ?? []
|
|
10368
|
+
})),
|
|
10369
|
+
expandable: nodes.some(hasVectorAppearance)
|
|
10370
|
+
};
|
|
10371
|
+
}
|
|
7955
10372
|
|
|
7956
10373
|
// src/vector/commands.ts
|
|
10374
|
+
var EFFECT_PARAMS_DESCRIPTION = "Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (\u2212100\u2026100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above.";
|
|
7957
10375
|
var fail = (message) => {
|
|
7958
10376
|
throw new Error(message);
|
|
7959
10377
|
};
|
|
@@ -7986,6 +10404,18 @@ var WIDTH_POINTS = {
|
|
|
7986
10404
|
},
|
|
7987
10405
|
description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
|
|
7988
10406
|
};
|
|
10407
|
+
var BLEND_PROPS = {
|
|
10408
|
+
spacing: { type: "string", enum: ["steps", "distance", "smoothColor"], description: "How in-betweens are spaced (default steps)." },
|
|
10409
|
+
steps: { type: "integer", minimum: 1, maximum: 1e3, description: "In-betweens between each pair of keys (steps spacing; default 5)." },
|
|
10410
|
+
distance: { type: "number", exclusiveMinimum: 0, description: "Distance between in-betweens along the spine (distance spacing; default 20)." },
|
|
10411
|
+
orientToSpine: { type: "boolean", description: "Turn the in-betweens with a replaced spine." },
|
|
10412
|
+
linear: { type: "boolean", description: "Move anchors in straight lines instead of the rotation-aware morph." }
|
|
10413
|
+
};
|
|
10414
|
+
var checkBlendOptions = (params) => {
|
|
10415
|
+
if (params?.spacing !== void 0 && !VECTOR_BLEND_SPACINGS.includes(params.spacing)) fail(`Unknown blend spacing "${params.spacing}".`);
|
|
10416
|
+
if (params?.steps !== void 0 && !(Number.isFinite(params.steps) && params.steps >= 1)) fail("steps must be at least 1.");
|
|
10417
|
+
if (params?.distance !== void 0 && !(Number.isFinite(params.distance) && params.distance > 0)) fail("distance must be positive.");
|
|
10418
|
+
};
|
|
7989
10419
|
var PAINT = {
|
|
7990
10420
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
7991
10421
|
};
|
|
@@ -8050,7 +10480,12 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8050
10480
|
visible: n.visible !== false,
|
|
8051
10481
|
locked: !!n.locked,
|
|
8052
10482
|
selected: state.selection.includes(n.id),
|
|
8053
|
-
...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length }
|
|
10483
|
+
...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length },
|
|
10484
|
+
...n.type === "group" && n.blend ? { blend: { ...resolveVectorBlendOptions(n.blend), spine: !!n.blend.spine, keys: vectorBlendKeys(n).map((k) => k.id) } } : {},
|
|
10485
|
+
...n.blendStep ? { blendStep: true } : {},
|
|
10486
|
+
...n.effects?.length ? { effects: n.effects } : {},
|
|
10487
|
+
...n.type === "group" && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {},
|
|
10488
|
+
...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
|
|
8054
10489
|
});
|
|
8055
10490
|
if (n.type === "group") visit(n.children, depth + 1);
|
|
8056
10491
|
}
|
|
@@ -8286,15 +10721,181 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8286
10721
|
id: `${prefix}boolean`,
|
|
8287
10722
|
label: "Boolean Operation\u2026",
|
|
8288
10723
|
keywords: ["union", "unite", "subtract", "intersect", "exclude", "combine", "pathfinder"],
|
|
8289
|
-
description: "Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude.",
|
|
8290
|
-
params: {
|
|
10724
|
+
description: "Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude. With `live`, the shapes stay editable operands of a live boolean group instead (a lone live group gets the operation; its operands get their own).",
|
|
10725
|
+
params: {
|
|
10726
|
+
type: "object",
|
|
10727
|
+
properties: {
|
|
10728
|
+
operation: { type: "string", enum: ["union", "subtract", "intersect", "exclude"] },
|
|
10729
|
+
ids: IDS,
|
|
10730
|
+
live: { type: "boolean", description: "Keep the operands editable in a live boolean group (default false: one new path)." }
|
|
10731
|
+
},
|
|
10732
|
+
required: ["operation"]
|
|
10733
|
+
},
|
|
10734
|
+
undoable: true,
|
|
10735
|
+
run: (_ctx, params) => {
|
|
10736
|
+
const id = controller.booleanOp(params.operation, { ids: ids(params), live: params.live });
|
|
10737
|
+
if (!id) fail(params.live ? "Select two or more nodes with one parent, a live boolean group or its operands." : "Select at least two overlapping paths (the result was empty).");
|
|
10738
|
+
return { id };
|
|
10739
|
+
}
|
|
10740
|
+
},
|
|
10741
|
+
{
|
|
10742
|
+
...base,
|
|
10743
|
+
id: `${prefix}addEffect`,
|
|
10744
|
+
label: "Add Path Effect\u2026",
|
|
10745
|
+
keywords: ["effect", "trim", "round corners", "pucker", "bloat", "twist", "zig zag", "wiggle", "offset", "merge", "repeater", "appearance", "live"],
|
|
10746
|
+
description: "Adds a live path effect to the bottom of the effect stack of nodes (paths or groups). Effects apply top to bottom; effects after a repeater act on its copies. Returns the new effect ids.",
|
|
10747
|
+
params: {
|
|
10748
|
+
type: "object",
|
|
10749
|
+
properties: {
|
|
10750
|
+
type: { type: "string", enum: VECTOR_EFFECT_TYPES },
|
|
10751
|
+
params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
|
|
10752
|
+
index: { type: "integer", minimum: 0, description: "Position in the stack (default: last)." },
|
|
10753
|
+
ids: IDS
|
|
10754
|
+
},
|
|
10755
|
+
required: ["type"]
|
|
10756
|
+
},
|
|
10757
|
+
undoable: true,
|
|
10758
|
+
enabled: enabledFor,
|
|
10759
|
+
run: (_ctx, params) => {
|
|
10760
|
+
if (!VECTOR_EFFECT_TYPES.includes(params.type)) fail(`Unknown effect type "${params.type}".`);
|
|
10761
|
+
const targets = ids(params);
|
|
10762
|
+
const created = controller.addEffect(params.type, { ids: targets, params: params.params, index: params.index });
|
|
10763
|
+
if (!created.length) fail("Nothing to add the effect to.");
|
|
10764
|
+
return { effects: targets.map((id, i) => ({ nodeId: id, effectId: created[i] })) };
|
|
10765
|
+
}
|
|
10766
|
+
},
|
|
10767
|
+
{
|
|
10768
|
+
...base,
|
|
10769
|
+
id: `${prefix}updateEffect`,
|
|
10770
|
+
label: "Edit Path Effect\u2026",
|
|
10771
|
+
keywords: ["effect", "parameters", "enable", "disable", "reorder"],
|
|
10772
|
+
description: "Changes a path effect: its parameters, whether it is enabled, and its position in the stack.",
|
|
10773
|
+
params: {
|
|
10774
|
+
type: "object",
|
|
10775
|
+
properties: {
|
|
10776
|
+
nodeId: { type: "string" },
|
|
10777
|
+
effectId: { type: "string" },
|
|
10778
|
+
params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
|
|
10779
|
+
enabled: { type: "boolean" },
|
|
10780
|
+
index: { type: "integer", minimum: 0, description: "Move it to this position in the stack (0 applies first)." }
|
|
10781
|
+
},
|
|
10782
|
+
required: ["nodeId", "effectId"]
|
|
10783
|
+
},
|
|
10784
|
+
undoable: true,
|
|
10785
|
+
run: (_ctx, params) => {
|
|
10786
|
+
if (params.params !== void 0 || params.enabled !== void 0) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));
|
|
10787
|
+
if (params.index !== void 0) check(controller.moveEffect(params.nodeId, params.effectId, params.index));
|
|
10788
|
+
return { effects: controller.getNode(params.nodeId)?.effects ?? [] };
|
|
10789
|
+
}
|
|
10790
|
+
},
|
|
10791
|
+
{
|
|
10792
|
+
...base,
|
|
10793
|
+
id: `${prefix}removeEffect`,
|
|
10794
|
+
label: "Remove Path Effect",
|
|
10795
|
+
keywords: ["effect", "delete"],
|
|
10796
|
+
description: "Removes a path effect from a node.",
|
|
10797
|
+
params: { type: "object", properties: { nodeId: { type: "string" }, effectId: { type: "string" } }, required: ["nodeId", "effectId"] },
|
|
10798
|
+
undoable: true,
|
|
10799
|
+
run: (_ctx, params) => {
|
|
10800
|
+
check(controller.removeEffect(params.nodeId, params.effectId));
|
|
10801
|
+
return { ok: true };
|
|
10802
|
+
}
|
|
10803
|
+
},
|
|
10804
|
+
{
|
|
10805
|
+
...base,
|
|
10806
|
+
id: `${prefix}expandAppearance`,
|
|
10807
|
+
label: "Expand Appearance",
|
|
10808
|
+
keywords: ["effects", "bake", "flatten", "live boolean", "expand"],
|
|
10809
|
+
description: "Bakes the path effects and live booleans of nodes (and everything inside them) into plain paths.",
|
|
10810
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
10811
|
+
undoable: true,
|
|
10812
|
+
enabled: enabledFor,
|
|
10813
|
+
run: (_ctx, params) => {
|
|
10814
|
+
const expanded = controller.expandAppearance({ ids: ids(params) });
|
|
10815
|
+
if (!expanded.length) fail("Nothing selected has path effects or a live boolean.");
|
|
10816
|
+
return { ids: expanded };
|
|
10817
|
+
}
|
|
10818
|
+
},
|
|
10819
|
+
{
|
|
10820
|
+
...base,
|
|
10821
|
+
id: `${prefix}blend`,
|
|
10822
|
+
label: "Make Blend\u2026",
|
|
10823
|
+
keywords: ["blend", "morph", "tween", "interpolate", "in-between", "steps", "transition"],
|
|
10824
|
+
description: "Blends two or more paths or groups (sharing a parent; bottom to top, or in the order given with keepOrder) into a live blend group: the in-betweens morph shape (anchors matched by arc length, the start and winding that twist least, rotation-aware so turning shapes turn) and paint (OKLab colours, gradients, stroke width, opacity, width profiles), and regenerate whenever a key changes. Spacing: `steps` (count between each pair of keys), `distance` (along the spine) or `smoothColor`. `starts` maps key ids to the anchor of their first subpath the blend starts from. Returns the blend id.",
|
|
10825
|
+
params: {
|
|
10826
|
+
type: "object",
|
|
10827
|
+
properties: {
|
|
10828
|
+
ids: IDS,
|
|
10829
|
+
...BLEND_PROPS,
|
|
10830
|
+
keepOrder: { type: "boolean", description: "Blend in the order of `ids` instead of stacking order." },
|
|
10831
|
+
starts: { type: "object", additionalProperties: { type: "integer", minimum: 0 }, description: "Start anchor per key id." }
|
|
10832
|
+
}
|
|
10833
|
+
},
|
|
8291
10834
|
undoable: true,
|
|
8292
10835
|
run: (_ctx, params) => {
|
|
8293
|
-
|
|
8294
|
-
|
|
10836
|
+
checkBlendOptions(params);
|
|
10837
|
+
const id = controller.blend({ ...params, ids: ids(params) });
|
|
10838
|
+
if (!id) fail("Select two or more shapes that share a parent.");
|
|
8295
10839
|
return { id };
|
|
8296
10840
|
}
|
|
8297
10841
|
},
|
|
10842
|
+
{
|
|
10843
|
+
...base,
|
|
10844
|
+
id: `${prefix}blendOptions`,
|
|
10845
|
+
label: "Blend Options\u2026",
|
|
10846
|
+
keywords: ["blend", "steps", "spacing", "distance", "smooth color", "orientation", "align to path"],
|
|
10847
|
+
description: "Changes the spacing (steps, distance or smoothColor), step count, distance, orient-to-spine and linear morph of the given (or selected) blends; their steps regenerate.",
|
|
10848
|
+
params: { type: "object", properties: { ids: IDS, ...BLEND_PROPS } },
|
|
10849
|
+
undoable: true,
|
|
10850
|
+
run: (_ctx, params) => {
|
|
10851
|
+
checkBlendOptions(params);
|
|
10852
|
+
const r = controller.setBlendOptions(params ?? {}, { ids: params?.ids });
|
|
10853
|
+
if (!r.ok) fail(r.message);
|
|
10854
|
+
return { ok: true };
|
|
10855
|
+
}
|
|
10856
|
+
},
|
|
10857
|
+
{
|
|
10858
|
+
...base,
|
|
10859
|
+
id: `${prefix}expandBlend`,
|
|
10860
|
+
label: "Expand Blend",
|
|
10861
|
+
keywords: ["blend", "expand", "flatten", "bake"],
|
|
10862
|
+
description: "Expands the given (or selected) blends into plain groups of their keys and in-betweens (editable paths with fresh ids). Returns the group ids.",
|
|
10863
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
10864
|
+
undoable: true,
|
|
10865
|
+
run: (_ctx, params) => {
|
|
10866
|
+
const groups = controller.expandBlend({ ids: params?.ids });
|
|
10867
|
+
if (!groups.length) fail("Select a blend to expand.");
|
|
10868
|
+
return { ids: groups };
|
|
10869
|
+
}
|
|
10870
|
+
},
|
|
10871
|
+
{
|
|
10872
|
+
...base,
|
|
10873
|
+
id: `${prefix}releaseBlend`,
|
|
10874
|
+
label: "Release Blend",
|
|
10875
|
+
keywords: ["blend", "release", "undo blend"],
|
|
10876
|
+
description: "Releases the given (or selected) blends: the in-betweens go and the keys (plus a replaced spine, as an unpainted path) take the blend's place. Returns their ids.",
|
|
10877
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
10878
|
+
undoable: true,
|
|
10879
|
+
run: (_ctx, params) => {
|
|
10880
|
+
const released = controller.releaseBlend({ ids: params?.ids });
|
|
10881
|
+
if (!released.length) fail("Select a blend to release.");
|
|
10882
|
+
return { ids: released };
|
|
10883
|
+
}
|
|
10884
|
+
},
|
|
10885
|
+
{
|
|
10886
|
+
...base,
|
|
10887
|
+
id: `${prefix}replaceSpine`,
|
|
10888
|
+
label: "Replace Spine",
|
|
10889
|
+
keywords: ["blend", "spine", "path", "follow", "along path"],
|
|
10890
|
+
description: "With one blend and one path given (or selected): the path becomes the blend's spine (it is removed) and the keys move onto it, spread evenly by arc length; the in-betweens follow it.",
|
|
10891
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
10892
|
+
undoable: true,
|
|
10893
|
+
run: (_ctx, params) => {
|
|
10894
|
+
const r = controller.replaceBlendSpine({ ids: params?.ids });
|
|
10895
|
+
if (!r.ok) fail(r.message);
|
|
10896
|
+
return { ok: true };
|
|
10897
|
+
}
|
|
10898
|
+
},
|
|
8298
10899
|
{
|
|
8299
10900
|
...base,
|
|
8300
10901
|
id: `${prefix}pathfinder`,
|
|
@@ -8642,6 +11243,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8642
11243
|
];
|
|
8643
11244
|
}
|
|
8644
11245
|
export {
|
|
11246
|
+
DEFAULT_VECTOR_BLEND_DISTANCE,
|
|
11247
|
+
DEFAULT_VECTOR_BLEND_STEPS,
|
|
8645
11248
|
DEFAULT_VECTOR_PATH_EDIT,
|
|
8646
11249
|
DEFAULT_VECTOR_SNAP,
|
|
8647
11250
|
IDENTITY_MATRIX,
|
|
@@ -8649,6 +11252,10 @@ export {
|
|
|
8649
11252
|
LazyMouse,
|
|
8650
11253
|
VECTOR_ARROWHEADS,
|
|
8651
11254
|
VECTOR_ARROWHEAD_LABELS,
|
|
11255
|
+
VECTOR_BLEND_MAX_SMOOTH_STEPS,
|
|
11256
|
+
VECTOR_BLEND_MAX_STEPS,
|
|
11257
|
+
VECTOR_BLEND_SPACINGS,
|
|
11258
|
+
VECTOR_BLEND_SPACING_LABELS,
|
|
8652
11259
|
VECTOR_BUILDER_HATCH_GAP,
|
|
8653
11260
|
VECTOR_CLIPBOARD_TYPE,
|
|
8654
11261
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
@@ -8656,8 +11263,14 @@ export {
|
|
|
8656
11263
|
VECTOR_CORNER_KIND_LABELS,
|
|
8657
11264
|
VECTOR_CORNER_WIDGET_INSET,
|
|
8658
11265
|
VECTOR_EDIT_TOGGLES,
|
|
11266
|
+
VECTOR_EFFECT_DEFAULTS,
|
|
11267
|
+
VECTOR_EFFECT_LABELS,
|
|
11268
|
+
VECTOR_EFFECT_MENU,
|
|
11269
|
+
VECTOR_EFFECT_PARAM_SPECS,
|
|
11270
|
+
VECTOR_EFFECT_TYPES,
|
|
8659
11271
|
VECTOR_FILL_GAP,
|
|
8660
11272
|
VECTOR_INK_MAX_CORNERS,
|
|
11273
|
+
VECTOR_MERGE_MODES,
|
|
8661
11274
|
VECTOR_OVERLAY_THEME,
|
|
8662
11275
|
VECTOR_PATHFINDER_ACTIONS,
|
|
8663
11276
|
VECTOR_PATHFINDER_LABELS,
|
|
@@ -8679,10 +11292,13 @@ export {
|
|
|
8679
11292
|
VECTOR_WIDTH_SAME_T,
|
|
8680
11293
|
VectorEditorController,
|
|
8681
11294
|
VectorSnapTargets,
|
|
11295
|
+
activeVectorEffects,
|
|
8682
11296
|
addBuilderHit,
|
|
8683
11297
|
applyMatrix,
|
|
8684
11298
|
applyMatrixLinear,
|
|
11299
|
+
applyVectorBlendOptions,
|
|
8685
11300
|
applyVectorCornersAttribute,
|
|
11301
|
+
applyVectorEffects,
|
|
8686
11302
|
arrowheadInset,
|
|
8687
11303
|
arrowheadPath,
|
|
8688
11304
|
arrowheadWeight,
|
|
@@ -8690,6 +11306,7 @@ export {
|
|
|
8690
11306
|
bezierToSubpaths,
|
|
8691
11307
|
boundsFromPoints,
|
|
8692
11308
|
boundsIntersect,
|
|
11309
|
+
clearVectorLiveBooleanCache,
|
|
8693
11310
|
cloneAnchor,
|
|
8694
11311
|
cloneSubpath,
|
|
8695
11312
|
cloneVectorDocument,
|
|
@@ -8700,26 +11317,40 @@ export {
|
|
|
8700
11317
|
cornerBisectorPoint,
|
|
8701
11318
|
cornerRadiusChange,
|
|
8702
11319
|
cornerSetback,
|
|
11320
|
+
correspondMorphContours,
|
|
8703
11321
|
createVectorCommands,
|
|
11322
|
+
createVectorEffect,
|
|
11323
|
+
createVectorMorph,
|
|
8704
11324
|
curvatureMoveAnchor,
|
|
8705
11325
|
cutSubpathAt,
|
|
8706
11326
|
dashIntervals,
|
|
8707
11327
|
decomposeRigid,
|
|
11328
|
+
effectPathLength,
|
|
11329
|
+
effectPathVertexCount,
|
|
8708
11330
|
effectiveStrokeAlign,
|
|
8709
11331
|
ellipseSubpath,
|
|
8710
11332
|
eraseFromFill,
|
|
8711
11333
|
eraseFromOutline,
|
|
8712
11334
|
eraserRegion,
|
|
11335
|
+
evaluateVectorPathKeyframes,
|
|
11336
|
+
expandVectorAppearance,
|
|
11337
|
+
expandVectorAppearances,
|
|
11338
|
+
expandVectorBlend,
|
|
8713
11339
|
expandVectorCorners,
|
|
8714
11340
|
expandVectorCornersOf,
|
|
8715
11341
|
findVectorNode,
|
|
8716
11342
|
fitCornerRadius,
|
|
11343
|
+
foldVectorBooleanOperands,
|
|
11344
|
+
generateVectorBlendChildren,
|
|
8717
11345
|
generateWidthProfile,
|
|
8718
11346
|
getAnchor,
|
|
8719
11347
|
getAnchorWorld,
|
|
8720
11348
|
getEditablePathIds,
|
|
8721
11349
|
getTransformHandlePositions,
|
|
11350
|
+
getVectorBlendControls,
|
|
8722
11351
|
getVectorCornerWidgets,
|
|
11352
|
+
getVectorEffectsPanel,
|
|
11353
|
+
getVectorEnteredGroups,
|
|
8723
11354
|
getVectorOverlayRenderData,
|
|
8724
11355
|
getVectorSceneRenderData,
|
|
8725
11356
|
getVectorSelectionBoxGeometry,
|
|
@@ -8728,8 +11359,10 @@ export {
|
|
|
8728
11359
|
getVectorWidthHandles,
|
|
8729
11360
|
getVisibleAnchors,
|
|
8730
11361
|
getVisibleHandles,
|
|
11362
|
+
hasOwnVectorAppearance,
|
|
8731
11363
|
hasPaint,
|
|
8732
11364
|
hasStrokeExtras,
|
|
11365
|
+
hasVectorAppearance,
|
|
8733
11366
|
hasVectorCorners,
|
|
8734
11367
|
hitTestVectorNode,
|
|
8735
11368
|
hitTestVectorNodes,
|
|
@@ -8737,41 +11370,66 @@ export {
|
|
|
8737
11370
|
inferAnchorType,
|
|
8738
11371
|
insertAnchor,
|
|
8739
11372
|
insertWidthPoint,
|
|
11373
|
+
interpolateMorphContours,
|
|
11374
|
+
interpolateRigidMatrix,
|
|
11375
|
+
interpolateVectorColor,
|
|
11376
|
+
interpolateVectorPaint,
|
|
11377
|
+
interpolateVectorPaths,
|
|
11378
|
+
interpolateVectorStyle,
|
|
11379
|
+
interpolateWidthProfiles,
|
|
8740
11380
|
invertMatrix,
|
|
8741
11381
|
isClosedBezierShape,
|
|
8742
11382
|
isIdentityMatrix,
|
|
8743
11383
|
isUniformWidthProfile,
|
|
11384
|
+
isVectorBlendGroup,
|
|
11385
|
+
isVectorLiveBoolean,
|
|
8744
11386
|
isVectorTextTarget,
|
|
8745
11387
|
joinSubpaths,
|
|
8746
11388
|
knifePath,
|
|
8747
11389
|
lineSubpath,
|
|
8748
11390
|
locateVectorNode,
|
|
8749
11391
|
matrixRotation,
|
|
11392
|
+
mergePaths,
|
|
8750
11393
|
mirrorWidthProfile,
|
|
11394
|
+
morphContourToSubpath,
|
|
11395
|
+
morphPathsCross,
|
|
8751
11396
|
moveAnchorHandle,
|
|
8752
11397
|
multiplyMatrices,
|
|
8753
11398
|
nearestSegment,
|
|
8754
11399
|
nearestStrokePosition,
|
|
8755
11400
|
nextVectorCornerKind,
|
|
8756
11401
|
nodeMatrix,
|
|
11402
|
+
normalizeVectorEffectParams,
|
|
11403
|
+
offsetPaths,
|
|
8757
11404
|
parseSvgTransform,
|
|
8758
11405
|
parseVectorSvg,
|
|
11406
|
+
pathsCenter,
|
|
8759
11407
|
planShapeBuilder,
|
|
8760
11408
|
polygonSubpath,
|
|
8761
11409
|
polylineBounds,
|
|
8762
11410
|
pressureWidthProfile,
|
|
11411
|
+
puckerBloatPaths,
|
|
8763
11412
|
recognizeInkShape,
|
|
8764
11413
|
rectSubpath,
|
|
11414
|
+
refineMorphContour,
|
|
11415
|
+
refreshVectorBlends,
|
|
11416
|
+
releaseVectorBlend,
|
|
8765
11417
|
removeAnchorKeepingShape,
|
|
8766
11418
|
removeAnchors,
|
|
8767
11419
|
removeAnchorsKeepingShape,
|
|
8768
11420
|
removeRedundantPoints,
|
|
8769
11421
|
removeWidthPoints,
|
|
11422
|
+
repeatPaths,
|
|
11423
|
+
resampleGradientStops,
|
|
11424
|
+
resolveVectorBlendOptions,
|
|
11425
|
+
reverseMorphContour,
|
|
8770
11426
|
reverseSubpath,
|
|
8771
11427
|
reverseWidthProfile,
|
|
8772
11428
|
rotateMatrix,
|
|
11429
|
+
roundCornerPaths,
|
|
8773
11430
|
roundSubpaths,
|
|
8774
11431
|
samplePolyline,
|
|
11432
|
+
sanitizeVectorEffects,
|
|
8775
11433
|
sanitizeWidthProfile,
|
|
8776
11434
|
scaleFor,
|
|
8777
11435
|
scaleMatrix,
|
|
@@ -8789,28 +11447,48 @@ export {
|
|
|
8789
11447
|
subpathPointAt,
|
|
8790
11448
|
subpathSegment,
|
|
8791
11449
|
subpathSegmentCount,
|
|
11450
|
+
subpathToMorphContour,
|
|
8792
11451
|
subpathsToBezier,
|
|
8793
11452
|
taperWaveFactor,
|
|
8794
11453
|
toVectorKeyInput,
|
|
8795
11454
|
toVectorModifiers,
|
|
8796
11455
|
toVectorPointerInput,
|
|
11456
|
+
transformVectorEffects,
|
|
8797
11457
|
transformVectorNode,
|
|
8798
11458
|
translateAnchor,
|
|
8799
11459
|
translateMatrix,
|
|
11460
|
+
trimIntervals,
|
|
11461
|
+
trimPaths,
|
|
11462
|
+
trimPathsIndividually,
|
|
11463
|
+
turnMorphContour,
|
|
11464
|
+
twistPaths,
|
|
8800
11465
|
unionBounds,
|
|
8801
11466
|
variableStrokeOutline,
|
|
8802
11467
|
variableStrokeRings,
|
|
11468
|
+
vectorAppearanceNodes,
|
|
11469
|
+
vectorAppearanceOutline,
|
|
11470
|
+
vectorAppearanceShapes,
|
|
11471
|
+
vectorBlendColorDistance,
|
|
11472
|
+
vectorBlendKeyTargets,
|
|
11473
|
+
vectorBlendKeys,
|
|
11474
|
+
vectorBlendRail,
|
|
11475
|
+
vectorBlendRailAt,
|
|
11476
|
+
vectorBlendSpine,
|
|
11477
|
+
vectorBlendStepCount,
|
|
8803
11478
|
vectorBuilderErasedEdges,
|
|
8804
11479
|
vectorCornerAt,
|
|
8805
11480
|
vectorCorners,
|
|
8806
11481
|
vectorCornersAttribute,
|
|
8807
11482
|
vectorDiamondPath,
|
|
8808
11483
|
vectorDocumentToSvg,
|
|
11484
|
+
vectorEnteredGroups,
|
|
8809
11485
|
vectorFillMap,
|
|
8810
11486
|
vectorInkShapeName,
|
|
11487
|
+
vectorLiveBooleanSteps,
|
|
8811
11488
|
vectorNodeBounds,
|
|
8812
11489
|
vectorNodeRotation,
|
|
8813
11490
|
vectorNodeToBezier,
|
|
11491
|
+
vectorOperandOperation,
|
|
8814
11492
|
vectorPaintToHex,
|
|
8815
11493
|
vectorPaintToSvg,
|
|
8816
11494
|
vectorParentMatrix,
|
|
@@ -8818,6 +11496,7 @@ export {
|
|
|
8818
11496
|
vectorPathEntries,
|
|
8819
11497
|
vectorPathWorldBezier,
|
|
8820
11498
|
vectorPathfinderShape,
|
|
11499
|
+
vectorRepeaterCopies,
|
|
8821
11500
|
vectorResizeCursor,
|
|
8822
11501
|
vectorResultStyle,
|
|
8823
11502
|
vectorShapeBuilderHit,
|
|
@@ -8837,6 +11516,8 @@ export {
|
|
|
8837
11516
|
widthProfileMaxScale,
|
|
8838
11517
|
widthProfilePreset,
|
|
8839
11518
|
widthProfilePresetId,
|
|
8840
|
-
widthProfileToAttribute
|
|
11519
|
+
widthProfileToAttribute,
|
|
11520
|
+
wigglePaths,
|
|
11521
|
+
zigzagPaths
|
|
8841
11522
|
};
|
|
8842
11523
|
//# sourceMappingURL=index.js.map
|